misc: finalize KMIP management

This commit is contained in:
Sheen Capadngan
2025-02-18 02:22:59 +08:00
parent 603b740bbe
commit 943c2b0e69
20 changed files with 490 additions and 317 deletions
+1 -1
View File
@@ -227,7 +227,7 @@ declare module "fastify" {
// everywhere else access using service layer // everywhere else access using service layer
store: { store: {
user: Pick<TUserDALFactory, "findById">; user: Pick<TUserDALFactory, "findById">;
kmipClient: Pick<TKmipClientDALFactory, "findOne">; kmipClient: Pick<TKmipClientDALFactory, "findByProjectAndClientId">;
}; };
} }
} }
@@ -1,5 +1,3 @@
import crypto from "crypto";
import jwt, { JwtPayload } from "jsonwebtoken";
import z from "zod"; import z from "zod";
import { KmsKeysSchema } from "@app/db/schemas"; 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 { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { writeLimit } from "@app/server/config/rateLimiter"; import { writeLimit } from "@app/server/config/rateLimiter";
import { ActorType } from "@app/services/auth/auth-type"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; import { ActorType, AuthMode } from "@app/services/auth/auth-type";
export const registerKmipOperationRouter = async (server: FastifyZodProvider) => { export const registerKmipOperationRouter = async (server: FastifyZodProvider) => {
server.decorateRequest("kmipUser", null); server.decorateRequest("kmipUser", null);
server.addHook("preHandler", async (req) => { server.addHook("onRequest", async (req) => {
const token = req.headers["x-kmip-jwt"] as string; const clientId = req.headers["x-kmip-client-id"] as string;
const serverCertSerialNumber = req.headers["x-server-certificate-serial-number"] as string; const projectId = req.headers["x-kmip-project-id"] as string;
const clientCertSerialNumber = req.headers["x-kmip-client-certificate-serial-number"] as string;
if (!jwt) { const serverCertSerialNumber = req.headers["x-kmip-server-certificate-serial-number"] as string;
throw new ForbiddenRequestError({
message: "Missing KMIP JWT"
});
}
if (!serverCertSerialNumber) { if (!serverCertSerialNumber) {
throw new ForbiddenRequestError({ 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 server certificate used is not revoked
// TODO: assert that client certificate used is not revoked // TODO: assert that client certificate used is not revoked
const publicKey = crypto.createPublicKey({ const kmipClient = await server.store.kmipClient.findByProjectAndClientId(projectId, clientId);
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
});
if (!kmipClient) { if (!kmipClient) {
throw new NotFoundError({ 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 = { req.kmipUser = {
projectId: decodedToken.projectId, projectId,
clientId: decodedToken.clientId, clientId,
name: kmipClient.name name: kmipClient.name
}; };
}); });
@@ -77,9 +80,14 @@ export const registerKmipOperationRouter = async (server: FastifyZodProvider) =>
200: KmsKeysSchema 200: KmsKeysSchema
} }
}, },
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const object = await server.services.kmipOperation.create({ const object = await server.services.kmipOperation.create({
...req.kmipUser, ...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
algorithm: req.body.algorithm algorithm: req.body.algorithm
}); });
@@ -124,9 +132,14 @@ export const registerKmipOperationRouter = async (server: FastifyZodProvider) =>
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const object = await server.services.kmipOperation.get({ const object = await server.services.kmipOperation.get({
...req.kmipUser, ...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.body.id id: req.body.id
}); });
@@ -172,9 +185,14 @@ export const registerKmipOperationRouter = async (server: FastifyZodProvider) =>
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const object = await server.services.kmipOperation.getAttributes({ const object = await server.services.kmipOperation.getAttributes({
...req.kmipUser, ...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.body.id id: req.body.id
}); });
@@ -216,9 +234,14 @@ export const registerKmipOperationRouter = async (server: FastifyZodProvider) =>
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const object = await server.services.kmipOperation.deleteOp({ const object = await server.services.kmipOperation.deleteOp({
...req.kmipUser, ...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.body.id id: req.body.id
}); });
@@ -261,9 +284,14 @@ export const registerKmipOperationRouter = async (server: FastifyZodProvider) =>
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const object = await server.services.kmipOperation.activate({ const object = await server.services.kmipOperation.activate({
...req.kmipUser, ...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.body.id id: req.body.id
}); });
@@ -306,9 +334,14 @@ export const registerKmipOperationRouter = async (server: FastifyZodProvider) =>
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const object = await server.services.kmipOperation.revoke({ const object = await server.services.kmipOperation.revoke({
...req.kmipUser, ...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.body.id id: req.body.id
}); });
@@ -356,9 +389,14 @@ export const registerKmipOperationRouter = async (server: FastifyZodProvider) =>
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const objects = await server.services.kmipOperation.locate({ 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({ await server.services.auditLog.createAuditLog({
@@ -403,10 +441,15 @@ export const registerKmipOperationRouter = async (server: FastifyZodProvider) =>
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const object = await server.services.kmipOperation.register({ const object = await server.services.kmipOperation.register({
...req.kmipUser, ...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({ await server.services.auditLog.createAuditLog({
+52 -9
View File
@@ -306,13 +306,26 @@ export const registerKmipRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
return server.services.kmip.setupOrgKmip({ const chains = await server.services.kmip.setupOrgKmip({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
...req.body ...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]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
return server.services.kmip.getOrgKmip({ const kmip = await server.services.kmip.getOrgKmip({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId 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({ server.route({
method: "POST", method: "POST",
url: "/server-certificates", url: "/server-registration",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
body: z.object({ body: z.object({
commonName: z.string().trim().min(1), hostnamesOrIps: validateAltNamesField,
altNames: validateAltNamesField, commonName: z.string().trim().min(1).optional(),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm), keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional().default(CertKeyAlgorithm.RSA_2048),
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number") ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number")
}), }),
response: { response: {
200: z.object({ 200: z.object({
serialNumber: z.string(), clientCertificateChain: z.string(),
certificateChain: z.string(), certificateChain: z.string(),
certificate: z.string(), certificate: z.string(),
privateKey: z.string() privateKey: z.string()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
return server.services.kmip.generateOrgKmipServerCertificate({ const configs = await server.services.kmip.registerServer({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
...req.body ...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;
} }
}); });
}; };
@@ -267,6 +267,11 @@ export enum EventType {
GET_KMIP_CLIENT = "get-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", 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_CREATE = "kmip-operation-create",
KMIP_OPERATION_GET = "kmip-operation-get", KMIP_OPERATION_GET = "kmip-operation-get",
KMIP_OPERATION_DELETE = "kmip-operation-delete", 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 = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@@ -2404,6 +2434,9 @@ export type Event =
| GetKmipClientEvent | GetKmipClientEvent
| GetKmipClientsEvent | GetKmipClientsEvent
| CreateKmipClientCertificateEvent | CreateKmipClientCertificateEvent
| SetupKmipEvent
| GetKmipEvent
| RegisterKmipServerEvent
| KmipOperationGetEvent | KmipOperationGetEvent
| KmipOperationDeleteEvent | KmipOperationDeleteEvent
| KmipOperationCreateEvent | KmipOperationCreateEvent
@@ -13,6 +13,26 @@ export type TKmipClientDALFactory = ReturnType<typeof kmipClientDALFactory>;
export const kmipClientDALFactory = (db: TDbClient) => { export const kmipClientDALFactory = (db: TDbClient) => {
const kmipClientOrm = ormify(db, TableName.KmipClient); 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 ( const findByProjectId = async (
{ {
projectId, projectId,
@@ -60,6 +80,7 @@ export const kmipClientDALFactory = (db: TDbClient) => {
return { return {
...kmipClientOrm, ...kmipClientOrm,
findByProjectId findByProjectId,
findByProjectAndClientId
}; };
}; };
@@ -1,9 +1,12 @@
import { ProjectType } from "@app/db/schemas"; import { ForbiddenError } from "@casl/ability";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal"; 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 { TKmipClientDALFactory } from "./kmip-client-dal";
import { KmipPermission } from "./kmip-enum"; import { KmipPermission } from "./kmip-enum";
import { import {
@@ -21,6 +24,7 @@ type TKmipOperationServiceFactoryDep = {
kmsDAL: TKmsKeyDALFactory; kmsDAL: TKmsKeyDALFactory;
kmipClientDAL: TKmipClientDALFactory; kmipClientDAL: TKmipClientDALFactory;
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId" | "findById">; projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId" | "findById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
}; };
export type TKmipOperationServiceFactory = ReturnType<typeof kmipOperationServiceFactory>; export type TKmipOperationServiceFactory = ReturnType<typeof kmipOperationServiceFactory>;
@@ -29,16 +33,28 @@ export const kmipOperationServiceFactory = ({
kmsService, kmsService,
kmsDAL, kmsDAL,
projectDAL, projectDAL,
kmipClientDAL kmipClientDAL,
permissionService
}: TKmipOperationServiceFactoryDep) => { }: TKmipOperationServiceFactoryDep) => {
const create = async ({ projectId: preSplitProjectId, clientId, algorithm }: TKmipCreateDTO) => { const create = async ({
let projectId = preSplitProjectId; projectId,
const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); clientId,
if (cmekProjectFromSplit) { algorithm,
projectId = cmekProjectFromSplit.id; 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({ const kmipClient = await kmipClientDAL.findOne({
id: clientId, id: clientId,
projectId projectId
@@ -52,7 +68,7 @@ export const kmipOperationServiceFactory = ({
const kmsKey = await kmsService.generateKmsKey({ const kmsKey = await kmsService.generateKmsKey({
encryptionAlgorithm: algorithm, encryptionAlgorithm: algorithm,
orgId: project.orgId, orgId: actorOrgId,
projectId, projectId,
isReserved: false isReserved: false
}); });
@@ -60,13 +76,16 @@ export const kmipOperationServiceFactory = ({
return kmsKey; return kmsKey;
}; };
const deleteOp = async ({ projectId: preSplitProjectId, id, clientId }: TKmipDeleteDTO) => { const deleteOp = async ({ projectId, id, clientId, actor, actorId, actorOrgId, actorAuthMethod }: TKmipDeleteDTO) => {
let projectId = preSplitProjectId; const { permission } = await permissionService.getOrgPermission(
const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
if (cmekProjectFromSplit) { ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
projectId = cmekProjectFromSplit.id;
}
const kmipClient = await kmipClientDAL.findOne({ const kmipClient = await kmipClientDAL.findOne({
id: clientId, id: clientId,
@@ -104,13 +123,16 @@ export const kmipOperationServiceFactory = ({
return kms; return kms;
}; };
const get = async ({ projectId: preSplitProjectId, id, clientId }: TKmipGetDTO) => { const get = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => {
let projectId = preSplitProjectId; const { permission } = await permissionService.getOrgPermission(
const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
if (cmekProjectFromSplit) { ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
projectId = cmekProjectFromSplit.id;
}
const kmipClient = await kmipClientDAL.findOne({ const kmipClient = await kmipClientDAL.findOne({
id: clientId, id: clientId,
@@ -158,13 +180,16 @@ export const kmipOperationServiceFactory = ({
}; };
}; };
const activate = async ({ projectId: preSplitProjectId, id, clientId }: TKmipGetDTO) => { const activate = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => {
let projectId = preSplitProjectId; const { permission } = await permissionService.getOrgPermission(
const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
if (cmekProjectFromSplit) { ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
projectId = cmekProjectFromSplit.id;
}
const kmipClient = await kmipClientDAL.findOne({ const kmipClient = await kmipClientDAL.findOne({
id: clientId, id: clientId,
@@ -192,13 +217,16 @@ export const kmipOperationServiceFactory = ({
}; };
}; };
const revoke = async ({ projectId: preSplitProjectId, id, clientId }: TKmipRevokeDTO) => { const revoke = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipRevokeDTO) => {
let projectId = preSplitProjectId; const { permission } = await permissionService.getOrgPermission(
const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
if (cmekProjectFromSplit) { ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
projectId = cmekProjectFromSplit.id;
}
const kmipClient = await kmipClientDAL.findOne({ const kmipClient = await kmipClientDAL.findOne({
id: clientId, id: clientId,
@@ -242,13 +270,24 @@ export const kmipOperationServiceFactory = ({
}; };
}; };
const getAttributes = async ({ projectId: preSplitProjectId, id, clientId }: TKmipGetAttributesDTO) => { const getAttributes = async ({
let projectId = preSplitProjectId; projectId,
const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); id,
clientId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TKmipGetAttributesDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
if (cmekProjectFromSplit) { ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
projectId = cmekProjectFromSplit.id;
}
const kmipClient = await kmipClientDAL.findOne({ const kmipClient = await kmipClientDAL.findOne({
id: clientId, id: clientId,
@@ -291,13 +330,16 @@ export const kmipOperationServiceFactory = ({
}; };
}; };
const locate = async ({ projectId: preSplitProjectId, clientId }: TKmipLocateDTO) => { const locate = async ({ projectId, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipLocateDTO) => {
let projectId = preSplitProjectId; const { permission } = await permissionService.getOrgPermission(
const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
if (cmekProjectFromSplit) { ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
projectId = cmekProjectFromSplit.id;
}
const kmipClient = await kmipClientDAL.findOne({ const kmipClient = await kmipClientDAL.findOne({
id: clientId, id: clientId,
@@ -315,13 +357,26 @@ export const kmipOperationServiceFactory = ({
return keys; return keys;
}; };
const register = async ({ projectId: preSplitProjectId, clientId, key, algorithm, name }: TKmipRegisterDTO) => { const register = async ({
let projectId = preSplitProjectId; projectId,
const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); clientId,
key,
algorithm,
name,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TKmipRegisterDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
if (cmekProjectFromSplit) { ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
projectId = cmekProjectFromSplit.id;
}
const kmipClient = await kmipClientDAL.findOne({ const kmipClient = await kmipClientDAL.findOne({
id: clientId, id: clientId,
+137 -19
View File
@@ -15,6 +15,7 @@ import { hostnameRegex } from "@app/services/certificate-authority/certificate-a
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission"; import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission";
@@ -30,6 +31,7 @@ import {
TGetKmipClientDTO, TGetKmipClientDTO,
TGetOrgKmipDTO, TGetOrgKmipDTO,
TListKmipClientsByProjectIdDTO, TListKmipClientsByProjectIdDTO,
TRegisterServerDTO,
TSetupOrgKmipDTO, TSetupOrgKmipDTO,
TUpdateKmipClientDTO TUpdateKmipClientDTO
} from "./kmip-types"; } from "./kmip-types";
@@ -41,6 +43,7 @@ type TKmipServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
kmipOrgConfigDAL: TKmipOrgConfigDALFactory; kmipOrgConfigDAL: TKmipOrgConfigDALFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
export type TKmipServiceFactory = ReturnType<typeof kmipServiceFactory>; export type TKmipServiceFactory = ReturnType<typeof kmipServiceFactory>;
@@ -51,7 +54,8 @@ export const kmipServiceFactory = ({
kmipClientCertificateDAL, kmipClientCertificateDAL,
kmipOrgConfigDAL, kmipOrgConfigDAL,
kmsService, kmsService,
kmipOrgServerCertificateDAL kmipOrgServerCertificateDAL,
licenseService
}: TKmipServiceFactoryDep) => { }: TKmipServiceFactoryDep) => {
const createKmipClient = async ({ const createKmipClient = async ({
actor, actor,
@@ -77,6 +81,12 @@ export const kmipServiceFactory = ({
ProjectPermissionSub.Kmip 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({ const kmipClient = await kmipClientDAL.create({
projectId, projectId,
name, 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({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
@@ -151,6 +167,12 @@ export const kmipServiceFactory = ({
ProjectPermissionSub.Kmip 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); const deletedKmipClient = await kmipClientDAL.deleteById(id);
return deletedKmipClient; 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({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, 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 setupOrgKmip = async ({ caKeyAlgorithm, actorOrgId, actor, actorId, actorAuthMethod }: TSetupOrgKmipDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, 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); const alg = keyAlgorithmToAlgCfg(caKeyAlgorithm);
// generate root CA // 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({ const kmipConfig = await kmipOrgConfigDAL.findOne({
orgId: actorOrgId orgId: actorOrgId
}); });
@@ -525,32 +586,21 @@ export const kmipServiceFactory = ({
); );
return { return {
id: kmipConfig.id,
serverCertificateChain: `${serverIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(), serverCertificateChain: `${serverIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(),
clientCertificateChain: `${clientIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim() clientCertificateChain: `${clientIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim()
}; };
}; };
const generateOrgKmipServerCertificate = async ({ const generateOrgKmipServerCertificate = async ({
actorOrgId, orgId,
actor,
actorId,
actorAuthMethod,
ttl, ttl,
commonName, commonName,
altNames, altNames,
keyAlgorithm keyAlgorithm
}: TGenerateOrgKmipServerCertificateDTO) => { }: TGenerateOrgKmipServerCertificateDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
const kmipOrgConfig = await kmipOrgConfigDAL.findOne({ const kmipOrgConfig = await kmipOrgConfigDAL.findOne({
orgId: actorOrgId orgId
}); });
if (!kmipOrgConfig) { 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({ const { decryptor, encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId: actorOrgId orgId
}); });
const caCertObj = new x509.X509Certificate( const caCertObj = new x509.X509Certificate(
@@ -668,7 +724,7 @@ export const kmipServiceFactory = ({
const certificateChain = `${caCertObj.toString("pem")}\n${decryptedCaCertChain}`.trim(); const certificateChain = `${caCertObj.toString("pem")}\n${decryptedCaCertChain}`.trim();
await kmipOrgServerCertificateDAL.create({ await kmipOrgServerCertificateDAL.create({
orgId: actorOrgId, orgId,
keyAlgorithm, keyAlgorithm,
issuedAt: notBeforeDate, issuedAt: notBeforeDate,
expiration: notAfterDate, 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 { return {
createKmipClient, createKmipClient,
updateKmipClient, updateKmipClient,
@@ -696,6 +812,8 @@ export const kmipServiceFactory = ({
createKmipClientCertificate, createKmipClientCertificate,
setupOrgKmip, setupOrgKmip,
generateOrgKmipServerCertificate, generateOrgKmipServerCertificate,
getOrgKmip getOrgKmip,
getServerCertificateBySerialNumber,
registerServer
}; };
}; };
+9 -1
View File
@@ -46,7 +46,7 @@ export type TListKmipClientsByProjectIdDTO = {
type KmipOperationBaseDTO = { type KmipOperationBaseDTO = {
clientId: string; clientId: string;
projectId: string; projectId: string;
}; } & Omit<TOrgPermission, "orgId">;
export type TKmipCreateDTO = { export type TKmipCreateDTO = {
algorithm: SymmetricEncryption; algorithm: SymmetricEncryption;
@@ -91,4 +91,12 @@ export type TGenerateOrgKmipServerCertificateDTO = {
altNames: string; altNames: string;
keyAlgorithm: CertKeyAlgorithm; keyAlgorithm: CertKeyAlgorithm;
ttl: string; ttl: string;
orgId: string;
};
export type TRegisterServerDTO = {
hostnamesOrIps: string;
commonName?: string;
keyAlgorithm?: CertKeyAlgorithm;
ttl: string;
} & Omit<TOrgPermission, "orgId">; } & Omit<TOrgPermission, "orgId">;
@@ -50,7 +50,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
}, },
pkiEst: false, pkiEst: false,
enforceMfa: false, enforceMfa: false,
projectTemplates: false projectTemplates: false,
kmip: false
}); });
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
@@ -68,6 +68,7 @@ export type TFeatureSet = {
pkiEst: boolean; pkiEst: boolean;
enforceMfa: boolean; enforceMfa: boolean;
projectTemplates: false; projectTemplates: false;
kmip: false;
}; };
export type TOrgPlansTableDTO = { export type TOrgPlansTableDTO = {
@@ -268,6 +268,9 @@ const buildAdminPermission = () => {
can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip); 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; return rules;
}; };
+4 -2
View File
@@ -1434,14 +1434,16 @@ export const registerRoutes = async (
kmipClientCertificateDAL, kmipClientCertificateDAL,
kmipOrgConfigDAL, kmipOrgConfigDAL,
kmsService, kmsService,
kmipOrgServerCertificateDAL kmipOrgServerCertificateDAL,
licenseService
}); });
const kmipOperationService = kmipOperationServiceFactory({ const kmipOperationService = kmipOperationServiceFactory({
kmsService, kmsService,
kmsDAL, kmsDAL,
projectDAL, projectDAL,
kmipClientDAL kmipClientDAL,
permissionService
}); });
await superAdminService.initServerCfg(); await superAdminService.initServerCfg();
@@ -125,6 +125,9 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.GET_KMIP_CLIENT]: "Get KMIP client", [EventType.GET_KMIP_CLIENT]: "Get KMIP client",
[EventType.GET_KMIP_CLIENTS]: "Get KMIP clients", [EventType.GET_KMIP_CLIENTS]: "Get KMIP clients",
[EventType.CREATE_KMIP_CLIENT_CERTIFICATE]: "Create KMIP client certificate", [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_CREATE]: "KMIP operation create",
[EventType.KMIP_OPERATION_GET]: "KMIP operation get", [EventType.KMIP_OPERATION_GET]: "KMIP operation get",
[EventType.KMIP_OPERATION_DELETE]: "KMIP operation delete", [EventType.KMIP_OPERATION_DELETE]: "KMIP operation delete",
@@ -137,6 +137,9 @@ export enum EventType {
GET_KMIP_CLIENT = "get-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", 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_CREATE = "kmip-operation-create",
KMIP_OPERATION_GET = "kmip-operation-get", KMIP_OPERATION_GET = "kmip-operation-get",
KMIP_OPERATION_DELETE = "kmip-operation-delete", KMIP_OPERATION_DELETE = "kmip-operation-delete",
-10
View File
@@ -5,11 +5,9 @@ import { apiRequest } from "@app/config/request";
import { kmipKeys } from "./queries"; import { kmipKeys } from "./queries";
import { import {
KmipClientCertificate, KmipClientCertificate,
OrgKmipServerCert,
TCreateKmipClient, TCreateKmipClient,
TDeleteKmipClient, TDeleteKmipClient,
TGenerateKmipClientCertificate, TGenerateKmipClientCertificate,
TGenerateOrgKmipServerCertDTO,
TSetupOrgKmipDTO, TSetupOrgKmipDTO,
TUpdateKmipClient TUpdateKmipClient
} from "./types"; } from "./types";
@@ -90,11 +88,3 @@ export const useSetupOrgKmip = (orgId: string) => {
} }
}); });
}; };
export const useGenerateOrgKmipServerCert = () => {
return useMutation({
mutationFn: async (payload: TGenerateOrgKmipServerCertDTO) => {
return apiRequest.post<OrgKmipServerCert>("/api/v1/kmip/server-certificates", payload);
}
});
};
-7
View File
@@ -73,13 +73,6 @@ export type TSetupOrgKmipDTO = {
caKeyAlgorithm: CertKeyAlgorithm; caKeyAlgorithm: CertKeyAlgorithm;
}; };
export type TGenerateOrgKmipServerCertDTO = {
commonName: string;
keyAlgorithm: CertKeyAlgorithm;
altNames: string;
ttl: string;
};
export type OrgKmipServerCert = { export type OrgKmipServerCert = {
serialNumber: string; serialNumber: string;
certificate: string; certificate: string;
@@ -46,4 +46,5 @@ export type SubscriptionPlan = {
pkiEst: boolean; pkiEst: boolean;
enforceMfa: boolean; enforceMfa: boolean;
projectTemplates: boolean; projectTemplates: boolean;
kmip: boolean;
}; };
@@ -107,7 +107,7 @@ const KmipClientCertificateForm = ({
</FormControl> </FormControl>
)} )}
/> />
<div className="flex items-center"> <div className="mt-8 flex items-center">
<Button <Button
className="mr-4" className="mr-4"
size="sm" size="sm"
@@ -13,6 +13,7 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
@@ -39,6 +40,7 @@ import {
ProjectPermissionKmipActions, ProjectPermissionKmipActions,
ProjectPermissionSub, ProjectPermissionSub,
useProjectPermission, useProjectPermission,
useSubscription,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
@@ -81,6 +83,7 @@ export const KmipClientTable = () => {
}); });
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const { subscription } = useSubscription();
const { kmipClients = [], totalCount = 0 } = data ?? {}; const { kmipClients = [], totalCount = 0 } = data ?? {};
useResetPageHelper({ useResetPageHelper({
@@ -93,7 +96,8 @@ export const KmipClientTable = () => {
"upsertKmipClient", "upsertKmipClient",
"deleteKmipClient", "deleteKmipClient",
"generateKmipClientCert", "generateKmipClientCert",
"displayKmipClientCert" "displayKmipClientCert",
"upgradePlan"
] as const); ] as const);
const handleSort = () => { const handleSort = () => {
@@ -152,7 +156,14 @@ export const KmipClientTable = () => {
colorSchema="primary" colorSchema="primary"
type="submit" type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("upsertKmipClient", null)} onClick={() => {
if (subscription && !subscription.kmip) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("upsertKmipClient", null);
}}
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
Add KMIP Client Add KMIP Client
@@ -202,7 +213,7 @@ export const KmipClientTable = () => {
<Tr className="group h-10 hover:bg-mineshaft-700" key={`st-v3-${id}`}> <Tr className="group h-10 hover:bg-mineshaft-700" key={`st-v3-${id}`}>
<Td>{name}</Td> <Td>{name}</Td>
<Td className="max-w-80 break-all">{description}</Td> <Td className="max-w-80 break-all">{description}</Td>
<Td className="max-w-40 break-all">{[permissions.join(", ")]}</Td> <Td className="max-w-40">{[permissions.join(", ")]}</Td>
<Td className="flex justify-end"> <Td className="flex justify-end">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -316,6 +327,11 @@ export const KmipClientTable = () => {
onOpenChange={(isOpen) => handlePopUpToggle("displayKmipClientCert", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("displayKmipClientCert", isOpen)}
certificate={popUp.displayKmipClientCert.data} certificate={popUp.displayKmipClientCert.data}
/> />
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="KMIP requires an enterprise plan."
/>
</div> </div>
</motion.div> </motion.div>
); );
@@ -4,12 +4,12 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import {
Button, Button,
FormControl, FormControl,
IconButton, IconButton,
Input,
Modal, Modal,
ModalContent, ModalContent,
Select, Select,
@@ -18,18 +18,13 @@ import {
TextArea, TextArea,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { downloadTxtFile } from "@app/helpers/download"; import { downloadTxtFile } from "@app/helpers/download";
import { usePopUp, useTimedReset } from "@app/hooks"; import { usePopUp, useTimedReset } from "@app/hooks";
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants"; import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums"; import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
import { import { useGetOrgKmipConfig, useSetupOrgKmip } from "@app/hooks/api/kmip";
useGenerateOrgKmipServerCert,
useGetOrgKmipConfig,
useSetupOrgKmip
} from "@app/hooks/api/kmip";
import { OrgKmipConfig } from "@app/hooks/api/kmip/types"; import { OrgKmipConfig } from "@app/hooks/api/kmip/types";
import { CertificateContent } from "@app/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateContent";
const orgConfigFormSchema = z.object({ const orgConfigFormSchema = z.object({
caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm) caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
@@ -45,8 +40,11 @@ const OrgConfigSection = ({
isKmipConfigLoading: boolean; isKmipConfigLoading: boolean;
}) => { }) => {
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([ const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
"configureKmip" "configureKmip",
"upgradePlan"
] as const); ] as const);
const { subscription } = useSubscription();
const { const {
handleSubmit, handleSubmit,
control, control,
@@ -181,6 +179,11 @@ const OrgConfigSection = ({
<Button <Button
className="mt-2" className="mt-2"
onClick={() => { onClick={() => {
if (subscription && !subscription.kmip) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("configureKmip"); handlePopUpOpen("configureKmip");
}} }}
> >
@@ -244,178 +247,15 @@ const OrgConfigSection = ({
</form> </form>
</ModalContent> </ModalContent>
</Modal> </Modal>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="KMIP requires an enterprise plan."
/>
</> </>
); );
}; };
const orgServerCertFormSchema = z.object({
commonName: z.string(),
altNames: z.string(),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
ttl: z.string()
});
type TOrgServerCertForm = z.infer<typeof orgServerCertFormSchema>;
export const KmipServerConfigSection = () => {
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
"configureKmipServerCert",
"showCertificate"
] as const);
const certificateData = popUp.showCertificate?.data as {
serialNumber: string;
certificate: string;
certificateChain: string;
privateKey: string;
};
const {
handleSubmit,
control,
formState: { isSubmitting }
} = useForm<TOrgServerCertForm>({
resolver: zodResolver(orgServerCertFormSchema)
});
const { mutateAsync: generateKmipServerCert } = useGenerateOrgKmipServerCert();
const onFormSubmit = async (formData: TOrgServerCertForm) => {
const { data: certificate } = await generateKmipServerCert(formData);
handlePopUpOpen("showCertificate", certificate);
createNotification({
type: "success",
text: "Successfully created KMIP server certificate"
});
handlePopUpClose("configureKmipServerCert");
};
return (
<div className="mt-8 flex flex-col justify-start">
<div className="text-lg">KMIP Server Certificate</div>
<div className="mt-2 max-w-lg text-sm text-mineshaft-400">
These certificates should be used to configure TLS for the KMIP servers.
</div>
<Button
className="mt-2 w-fit"
onClick={() => {
handlePopUpOpen("configureKmipServerCert");
}}
>
Generate KMIP server certificate
</Button>
<Modal
isOpen={popUp.configureKmipServerCert.isOpen}
onOpenChange={(state) => handlePopUpToggle("configureKmipServerCert", state)}
>
<ModalContent title="Configure KMIP for the organization">
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="commonName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Common Name (CN)"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="service.acme.com" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="altNames"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Alternative Names (SANs)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="app1.acme.com, app2.acme.com, ..." />
</FormControl>
)}
/>
<Controller
control={control}
name="ttl"
render={({ field, fieldState: { error } }) => (
<FormControl
label="TTL"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
</FormControl>
)}
/>
<Controller
control={control}
name="keyAlgorithm"
defaultValue={CertKeyAlgorithm.RSA_2048}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Key Algorithm"
errorText={error?.message}
isError={Boolean(error)}
helperText="This defines the key algorithm to use for signing the server certificate."
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{certKeyAlgorithms.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="mt-6 flex w-full gap-4">
<Button
className=""
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Continue
</Button>
<Button
className=""
size="sm"
variant="outline_bg"
type="button"
onClick={() => handlePopUpClose("configureKmipServerCert")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
<Modal
isOpen={popUp.showCertificate.isOpen}
onOpenChange={(state) => handlePopUpToggle("showCertificate", state)}
>
<ModalContent title="Configure KMIP for the organization">
<CertificateContent {...certificateData} />
</ModalContent>
</Modal>
</div>
);
};
export const KmipTab = () => { export const KmipTab = () => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { data: kmipConfig, isPending } = useGetOrgKmipConfig(currentOrg.id); const { data: kmipConfig, isPending } = useGetOrgKmipConfig(currentOrg.id);
@@ -423,7 +263,6 @@ export const KmipTab = () => {
return ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<OrgConfigSection kmipConfig={kmipConfig} isKmipConfigLoading={isPending} /> <OrgConfigSection kmipConfig={kmipConfig} isKmipConfigLoading={isPending} />
{kmipConfig && <KmipServerConfigSection />}
</div> </div>
); );
}; };