feat: finished up client cert generation

This commit is contained in:
Sheen Capadngan
2025-02-07 01:24:32 +08:00
parent 0269f57768
commit a3acfa65a2
21 changed files with 593 additions and 14 deletions
+8
View File
@@ -143,6 +143,9 @@ import {
TInternalKms, TInternalKms,
TInternalKmsInsert, TInternalKmsInsert,
TInternalKmsUpdate, TInternalKmsUpdate,
TKmipClientCertificates,
TKmipClientCertificatesInsert,
TKmipClientCertificatesUpdate,
TKmipClients, TKmipClients,
TKmipClientsInsert, TKmipClientsInsert,
TKmipClientsUpdate, TKmipClientsUpdate,
@@ -922,5 +925,10 @@ declare module "knex/types/tables" {
TKmipInstanceServerCertificatesInsert, TKmipInstanceServerCertificatesInsert,
TKmipInstanceServerCertificatesUpdate TKmipInstanceServerCertificatesUpdate
>; >;
[TableName.KmipClientCertificates]: KnexOriginal.CompositeTableType<
TKmipClientCertificates,
TKmipClientCertificatesInsert,
TKmipClientCertificatesUpdate
>;
} }
} }
@@ -63,6 +63,19 @@ export async function up(knex: Knex): Promise<void> {
t.binary("encryptedChain").notNullable(); 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<void> { export async function down(knex: Knex): Promise<void> {
@@ -81,4 +94,9 @@ export async function down(knex: Knex): Promise<void> {
if (hasKmipInstanceServerCertTable) { if (hasKmipInstanceServerCertTable) {
await knex.schema.dropTable(TableName.KmipInstanceServerCertificates); await knex.schema.dropTable(TableName.KmipInstanceServerCertificates);
} }
const hasKmipClientCertTable = await knex.schema.hasTable(TableName.KmipClientCertificates);
if (hasKmipClientCertTable) {
await knex.schema.dropTable(TableName.KmipClientCertificates);
}
} }
+1
View File
@@ -45,6 +45,7 @@ export * from "./incident-contacts";
export * from "./integration-auths"; export * from "./integration-auths";
export * from "./integrations"; export * from "./integrations";
export * from "./internal-kms"; export * from "./internal-kms";
export * from "./kmip-client-certificates";
export * from "./kmip-clients"; export * from "./kmip-clients";
export * from "./kmip-instance-configs"; export * from "./kmip-instance-configs";
export * from "./kmip-instance-server-certificates"; export * from "./kmip-instance-server-certificates";
@@ -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<typeof KmipClientCertificatesSchema>;
export type TKmipClientCertificatesInsert = Omit<z.input<typeof KmipClientCertificatesSchema>, TImmutableDBKeys>;
export type TKmipClientCertificatesUpdate = Partial<
Omit<z.input<typeof KmipClientCertificatesSchema>, TImmutableDBKeys>
>;
+2 -1
View File
@@ -135,7 +135,8 @@ export enum TableName {
SecretSync = "secret_syncs", SecretSync = "secret_syncs",
KmipClient = "kmip_clients", KmipClient = "kmip_clients",
KmipInstanceConfig = "kmip_instance_configs", KmipInstanceConfig = "kmip_instance_configs",
KmipInstanceServerCertificates = "kmip_instance_server_certificates" KmipInstanceServerCertificates = "kmip_instance_server_certificates",
KmipClientCertificates = "kmip_client_certificates"
} }
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt"; export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
+55
View File
@@ -1,3 +1,4 @@
import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { KmipClientsSchema } from "@app/db/schemas"; 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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
const KmipClientResponseSchema = KmipClientsSchema.pick({ const KmipClientResponseSchema = KmipClientsSchema.pick({
projectId: true, projectId: true,
@@ -230,4 +232,57 @@ export const registerKmipRouter = async (server: FastifyZodProvider) => {
return { kmipClients, totalCount }; 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;
}
});
}; };
@@ -258,7 +258,8 @@ export enum EventType {
UPDATE_KMIP_CLIENT = "update-kmip-client", UPDATE_KMIP_CLIENT = "update-kmip-client",
DELETE_KMIP_CLIENT = "delete-kmip-client", DELETE_KMIP_CLIENT = "delete-kmip-client",
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"
} }
interface UserActorMetadata { 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 = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@@ -2307,4 +2318,5 @@ export type Event =
| UpdateKmipClientEvent | UpdateKmipClientEvent
| DeleteKmipClientEvent | DeleteKmipClientEvent
| GetKmipClientEvent | GetKmipClientEvent
| GetKmipClientsEvent; | GetKmipClientsEvent
| CreateKmipClientCertificateEvent;
@@ -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<typeof kmipClientCertificateDALFactory>;
export const kmipClientCertificateDALFactory = (db: TDbClient) => {
const kmipClientCertOrm = ormify(db, TableName.KmipClientCertificates);
return {
...kmipClientCertOrm
};
};
@@ -0,0 +1 @@
export const INSTANCE_KMIP_CONFIG_ID = "00000000-0000-0000-0000-000000000000";
+183 -2
View File
@@ -1,11 +1,25 @@
import { ForbiddenError } from "@casl/ability"; 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 { 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 { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission"; import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission";
import { TKmipClientCertificateDALFactory } from "./kmip-client-certificate-dal";
import { TKmipClientDALFactory } from "./kmip-client-dal"; import { TKmipClientDALFactory } from "./kmip-client-dal";
import { INSTANCE_KMIP_CONFIG_ID } from "./kmip-constants";
import { TKmipInstanceConfigDALFactory } from "./kmip-instance-config-dal";
import { import {
TCreateKmipClientCertificateDTO,
TCreateKmipClientDTO, TCreateKmipClientDTO,
TDeleteKmipClientDTO, TDeleteKmipClientDTO,
TGetKmipClientDTO, TGetKmipClientDTO,
@@ -15,12 +29,21 @@ import {
type TKmipServiceFactoryDep = { type TKmipServiceFactoryDep = {
kmipClientDAL: TKmipClientDALFactory; kmipClientDAL: TKmipClientDALFactory;
kmipClientCertificateDAL: TKmipClientCertificateDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "decryptWithRootKey">;
kmipInstanceConfigDAL: TKmipInstanceConfigDALFactory;
}; };
export type TKmipServiceFactory = ReturnType<typeof kmipServiceFactory>; export type TKmipServiceFactory = ReturnType<typeof kmipServiceFactory>;
export const kmipServiceFactory = ({ kmipClientDAL, permissionService }: TKmipServiceFactoryDep) => { export const kmipServiceFactory = ({
kmipClientDAL,
permissionService,
kmipClientCertificateDAL,
kmipInstanceConfigDAL,
kmsService
}: TKmipServiceFactoryDep) => {
const createKmipClient = async ({ const createKmipClient = async ({
actor, actor,
actorId, actorId,
@@ -67,6 +90,12 @@ export const kmipServiceFactory = ({ kmipClientDAL, permissionService }: TKmipSe
}: TUpdateKmipClientDTO) => { }: TUpdateKmipClientDTO) => {
const kmipClient = await kmipClientDAL.findById(id); 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({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
@@ -93,6 +122,12 @@ export const kmipServiceFactory = ({ kmipClientDAL, permissionService }: TKmipSe
const deleteKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteKmipClientDTO) => { const deleteKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteKmipClientDTO) => {
const kmipClient = await kmipClientDAL.findById(id); 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({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
@@ -115,6 +150,12 @@ export const kmipServiceFactory = ({ kmipClientDAL, permissionService }: TKmipSe
const getKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetKmipClientDTO) => { const getKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetKmipClientDTO) => {
const kmipClient = await kmipClientDAL.findById(id); 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({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
@@ -151,5 +192,145 @@ export const kmipServiceFactory = ({ kmipClientDAL, permissionService }: TKmipSe
return kmipClientDAL.findByProjectId({ projectId, ...rest }); 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
};
}; };
@@ -1,7 +1,14 @@
import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { OrderByDirection, TProjectPermission } from "@app/lib/types";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { KmipPermission } from "./kmip-enum"; import { KmipPermission } from "./kmip-enum";
export type TCreateKmipClientCertificateDTO = {
clientId: string;
keyAlgorithm: CertKeyAlgorithm;
ttl: string;
} & Omit<TProjectPermission, "projectId">;
export type TCreateKmipClientDTO = { export type TCreateKmipClientDTO = {
name: string; name: string;
description?: string; description?: string;
@@ -48,7 +48,8 @@ export enum ProjectPermissionKmipActions {
CreateClients = "create-clients", CreateClients = "create-clients",
UpdateClients = "update-clients", UpdateClients = "update-clients",
DeleteClients = "delete-clients", DeleteClients = "delete-clients",
ReadClients = "read-clients" ReadClients = "read-clients",
GenerateClientCertificates = "generate-client-certificates"
} }
export enum ProjectPermissionSub { export enum ProjectPermissionSub {
@@ -596,7 +597,8 @@ const buildAdminPermissionRules = () => {
ProjectPermissionKmipActions.CreateClients, ProjectPermissionKmipActions.CreateClients,
ProjectPermissionKmipActions.UpdateClients, ProjectPermissionKmipActions.UpdateClients,
ProjectPermissionKmipActions.DeleteClients, ProjectPermissionKmipActions.DeleteClients,
ProjectPermissionKmipActions.ReadClients ProjectPermissionKmipActions.ReadClients,
ProjectPermissionKmipActions.GenerateClientCertificates
], ],
ProjectPermissionSub.Kmip ProjectPermissionSub.Kmip
); );
+6 -1
View File
@@ -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 { 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 { 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 { 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 { kmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
import { kmipInstanceConfigDALFactory } from "@app/ee/services/kmip/kmip-instance-config-dal"; import { kmipInstanceConfigDALFactory } from "@app/ee/services/kmip/kmip-instance-config-dal";
import { kmipInstanceServerCertificateDALFactory } from "@app/ee/services/kmip/kmip-instance-server-certificate-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 projectTemplateDAL = projectTemplateDALFactory(db);
const resourceMetadataDAL = resourceMetadataDALFactory(db); const resourceMetadataDAL = resourceMetadataDALFactory(db);
const kmipClientDAL = kmipClientDALFactory(db); const kmipClientDAL = kmipClientDALFactory(db);
const kmipClientCertificateDAL = kmipClientCertificateDALFactory(db);
const kmipInstanceConfigDAL = kmipInstanceConfigDALFactory(db); const kmipInstanceConfigDAL = kmipInstanceConfigDALFactory(db);
const kmipInstanceServerCertificateDAL = kmipInstanceServerCertificateDALFactory(db); const kmipInstanceServerCertificateDAL = kmipInstanceServerCertificateDALFactory(db);
@@ -1429,7 +1431,10 @@ export const registerRoutes = async (
const kmipService = kmipServiceFactory({ const kmipService = kmipServiceFactory({
kmipClientDAL, kmipClientDAL,
permissionService permissionService,
kmipClientCertificateDAL,
kmipInstanceConfigDAL,
kmsService
}); });
await superAdminService.initServerCfg(); await superAdminService.initServerCfg();
@@ -11,7 +11,7 @@ import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp"; 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 { isValidIp } from "@app/lib/ip";
import { TAuthLoginFactory } from "../auth/auth-login-service"; import { TAuthLoginFactory } from "../auth/auth-login-service";
@@ -562,7 +562,7 @@ export const superAdminServiceFactory = ({
}: TGenerateInstanceKmipServerCertificateDTO) => { }: TGenerateInstanceKmipServerCertificateDTO) => {
const kmipInstanceConfig = await kmipInstanceConfigDAL.findById(ADMIN_CONFIG_DB_UUID); const kmipInstanceConfig = await kmipInstanceConfigDAL.findById(ADMIN_CONFIG_DB_UUID);
if (!kmipInstanceConfig) { if (!kmipInstanceConfig) {
throw new BadRequestError({ throw new InternalServerError({
message: "KMIP has not been configured for the instance" message: "KMIP has not been configured for the instance"
}); });
} }
@@ -28,7 +28,8 @@ export enum ProjectPermissionKmipActions {
CreateClients = "create-clients", CreateClients = "create-clients",
UpdateClients = "update-clients", UpdateClients = "update-clients",
DeleteClients = "delete-clients", DeleteClients = "delete-clients",
ReadClients = "read-clients" ReadClients = "read-clients",
GenerateClientCertificates = "generate-client-certificates"
} }
export enum ProjectPermissionSecretSyncActions { export enum ProjectPermissionSecretSyncActions {
+20 -1
View File
@@ -3,7 +3,13 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { kmipKeys } from "./queries"; import { kmipKeys } from "./queries";
import { TCreateKmipClient, TDeleteKmipClient, TUpdateKmipClient } from "./types"; import {
KmipClientCertificate,
TCreateKmipClient,
TDeleteKmipClient,
TGenerateKmipClientCertificate,
TUpdateKmipClient
} from "./types";
export const useCreateKmipClient = () => { export const useCreateKmipClient = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -56,3 +62,16 @@ export const useDeleteKmipClients = () => {
} }
}); });
}; };
export const useGenerateKmipClientCertificate = () => {
return useMutation({
mutationFn: async (payload: TGenerateKmipClientCertificate) => {
const { data } = await apiRequest.post<KmipClientCertificate>(
`/api/v1/kmip/clients/${payload.clientId}/certificates`,
payload
);
return data;
}
});
};
+14
View File
@@ -1,3 +1,4 @@
import { CertKeyAlgorithm } from "../certificates/enums";
import { OrderByDirection } from "../generic/types"; import { OrderByDirection } from "../generic/types";
export enum KmipPermission { export enum KmipPermission {
@@ -30,6 +31,19 @@ export type TProjectKmipClientList = {
totalCount: number; 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 TDeleteKmipClient = KeyRef & ProjectRef;
export type TListProjectKmipClientsDTO = { export type TListProjectKmipClientsDTO = {
@@ -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<typeof formSchema>;
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
kmipClient?: TKmipClient | null;
displayNewClientCertificate: (certificate: KmipClientCertificate) => void;
};
type FormProps = Pick<Props, "kmipClient" | "displayNewClientCertificate"> & {
onComplete: () => void;
};
const KmipClientCertificateForm = ({
displayNewClientCertificate,
kmipClient,
onComplete
}: FormProps) => {
const { mutateAsync: createKmipClientCertificate } = useGenerateKmipClientCertificate();
const {
control,
handleSubmit,
formState: { isSubmitting }
} = useForm<FormData>({
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 (
<form onSubmit={handleSubmit(handleKmipClientSubmit)}>
<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 client 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="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Generate Client Certificate
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
);
};
export const CreateKmipClientCertificateModal = ({
isOpen,
onOpenChange,
kmipClient,
displayNewClientCertificate
}: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="Generate KMIP client certificate">
<KmipClientCertificateForm
onComplete={() => onOpenChange(false)}
displayNewClientCertificate={displayNewClientCertificate}
kmipClient={kmipClient}
/>
</ModalContent>
</Modal>
);
};
@@ -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 (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="KMIP client certificate">
<CertificateContent {...certificate} />
</ModalContent>
</Modal>
);
};
@@ -2,6 +2,7 @@ import {
faArrowDown, faArrowDown,
faArrowUp, faArrowUp,
faArrowUpRightFromSquare, faArrowUpRightFromSquare,
faCertificate,
faEdit, faEdit,
faEllipsis, faEllipsis,
faMagnifyingGlass, faMagnifyingGlass,
@@ -45,7 +46,9 @@ import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useGetKmipClientsByProjectId } from "@app/hooks/api/kmip"; import { useGetKmipClientsByProjectId } from "@app/hooks/api/kmip";
import { KmipClientOrderBy, TKmipClient } from "@app/hooks/api/kmip/types"; import { KmipClientOrderBy, TKmipClient } from "@app/hooks/api/kmip/types";
import { CreateKmipClientCertificateModal } from "./CreateKmipClientCertificateModal";
import { DeleteKmipClientModal } from "./DeleteKmipClientModal"; import { DeleteKmipClientModal } from "./DeleteKmipClientModal";
import { KmipClientCertificateModal } from "./KmipClientCertificateModal";
import { KmipClientModal } from "./KmipClientModal"; import { KmipClientModal } from "./KmipClientModal";
export const KmipClientTable = () => { export const KmipClientTable = () => {
@@ -88,7 +91,9 @@ export const KmipClientTable = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"upsertKmipClient", "upsertKmipClient",
"deleteKmipClient" "deleteKmipClient",
"generateKmipClientCert",
"displayKmipClientCert"
] as const); ] as const);
const handleSort = () => { const handleSort = () => {
@@ -107,6 +112,11 @@ export const KmipClientTable = () => {
ProjectPermissionSub.Kmip ProjectPermissionSub.Kmip
); );
const cannotGenerateKmipClientCertificate = permission.cannot(
ProjectPermissionKmipActions.GenerateClientCertificates,
ProjectPermissionSub.Kmip
);
return ( return (
<motion.div <motion.div
key="kmip-clients-tab" key="kmip-clients-tab"
@@ -206,6 +216,25 @@ export const KmipClientTable = () => {
</IconButton> </IconButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[160px]"> <DropdownMenuContent className="min-w-[160px]">
<Tooltip
content={
cannotGenerateKmipClientCertificate ? "Access Restricted" : ""
}
position="left"
>
<div>
<DropdownMenuItem
onClick={() =>
handlePopUpOpen("generateKmipClientCert", kmipClient)
}
icon={<FontAwesomeIcon icon={faCertificate} />}
iconPos="left"
isDisabled={cannotGenerateKmipClientCertificate}
>
Generate Certificate
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip <Tooltip
content={cannotEditKmipClient ? "Access Restricted" : ""} content={cannotEditKmipClient ? "Access Restricted" : ""}
position="left" position="left"
@@ -274,6 +303,19 @@ export const KmipClientTable = () => {
onOpenChange={(isOpen) => handlePopUpToggle("upsertKmipClient", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upsertKmipClient", isOpen)}
kmipClient={popUp.upsertKmipClient.data as TKmipClient | null} kmipClient={popUp.upsertKmipClient.data as TKmipClient | null}
/> />
<CreateKmipClientCertificateModal
isOpen={popUp.generateKmipClientCert.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("generateKmipClientCert", isOpen)}
kmipClient={popUp.generateKmipClientCert.data as TKmipClient | null}
displayNewClientCertificate={(certificate) =>
handlePopUpOpen("displayKmipClientCert", certificate)
}
/>
<KmipClientCertificateModal
isOpen={popUp.displayKmipClientCert.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("displayKmipClientCert", isOpen)}
certificate={popUp.displayKmipClientCert.data}
/>
</div> </div>
</motion.div> </motion.div>
); );
@@ -53,7 +53,8 @@ const KmipPolicyActionSchema = z.object({
[ProjectPermissionKmipActions.ReadClients]: z.boolean().optional(), [ProjectPermissionKmipActions.ReadClients]: z.boolean().optional(),
[ProjectPermissionKmipActions.CreateClients]: z.boolean().optional(), [ProjectPermissionKmipActions.CreateClients]: z.boolean().optional(),
[ProjectPermissionKmipActions.UpdateClients]: 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({ const SecretRollbackPolicyActionSchema = z.object({
@@ -373,6 +374,9 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
const canEditClients = action.includes(ProjectPermissionKmipActions.UpdateClients); const canEditClients = action.includes(ProjectPermissionKmipActions.UpdateClients);
const canDeleteClients = action.includes(ProjectPermissionKmipActions.DeleteClients); const canDeleteClients = action.includes(ProjectPermissionKmipActions.DeleteClients);
const canCreateClients = action.includes(ProjectPermissionKmipActions.CreateClients); const canCreateClients = action.includes(ProjectPermissionKmipActions.CreateClients);
const canGenerateClientCerts = action.includes(
ProjectPermissionKmipActions.GenerateClientCertificates
);
if (!formVal[subject]) formVal[subject] = [{}]; if (!formVal[subject]) formVal[subject] = [{}];
@@ -381,6 +385,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
if (canEditClients) formVal[subject]![0][ProjectPermissionKmipActions.UpdateClients] = true; if (canEditClients) formVal[subject]![0][ProjectPermissionKmipActions.UpdateClients] = true;
if (canCreateClients) formVal[subject]![0][ProjectPermissionKmipActions.CreateClients] = true; if (canCreateClients) formVal[subject]![0][ProjectPermissionKmipActions.CreateClients] = true;
if (canDeleteClients) formVal[subject]![0][ProjectPermissionKmipActions.DeleteClients] = true; if (canDeleteClients) formVal[subject]![0][ProjectPermissionKmipActions.DeleteClients] = true;
if (canGenerateClientCerts)
formVal[subject]![0][ProjectPermissionKmipActions.GenerateClientCertificates] = true;
return; return;
} }
@@ -783,6 +789,10 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
{ {
label: "Delete clients", label: "Delete clients",
value: ProjectPermissionKmipActions.DeleteClients value: ProjectPermissionKmipActions.DeleteClients
},
{
label: "Generate client certificates",
value: ProjectPermissionKmipActions.GenerateClientCertificates
} }
] ]
} }