diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index dabcfc060..27f6b894c 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -149,12 +149,12 @@ import { TKmipClients, TKmipClientsInsert, TKmipClientsUpdate, - TKmipInstanceConfigs, - TKmipInstanceConfigsInsert, - TKmipInstanceConfigsUpdate, - TKmipInstanceServerCertificates, - TKmipInstanceServerCertificatesInsert, - TKmipInstanceServerCertificatesUpdate, + TKmipOrgConfigs, + TKmipOrgConfigsInsert, + TKmipOrgConfigsUpdate, + TKmipOrgServerCertificates, + TKmipOrgServerCertificatesInsert, + TKmipOrgServerCertificatesUpdate, TKmsKeys, TKmsKeysInsert, TKmsKeysUpdate, @@ -915,15 +915,15 @@ declare module "knex/types/tables" { >; [TableName.SecretSync]: KnexOriginal.CompositeTableType; [TableName.KmipClient]: KnexOriginal.CompositeTableType; - [TableName.KmipInstanceConfig]: KnexOriginal.CompositeTableType< - TKmipInstanceConfigs, - TKmipInstanceConfigsInsert, - TKmipInstanceConfigsUpdate + [TableName.KmipOrgConfig]: KnexOriginal.CompositeTableType< + TKmipOrgConfigs, + TKmipOrgConfigsInsert, + TKmipOrgConfigsUpdate >; - [TableName.KmipInstanceServerCertificates]: KnexOriginal.CompositeTableType< - TKmipInstanceServerCertificates, - TKmipInstanceServerCertificatesInsert, - TKmipInstanceServerCertificatesUpdate + [TableName.KmipOrgServerCertificates]: KnexOriginal.CompositeTableType< + TKmipOrgServerCertificates, + TKmipOrgServerCertificatesInsert, + TKmipOrgServerCertificatesUpdate >; [TableName.KmipClientCertificates]: KnexOriginal.CompositeTableType< TKmipClientCertificates, diff --git a/backend/src/db/migrations/20250203141127_add-kmip.ts b/backend/src/db/migrations/20250203141127_add-kmip.ts index ecd35b8ec..ae63fbfe4 100644 --- a/backend/src/db/migrations/20250203141127_add-kmip.ts +++ b/backend/src/db/migrations/20250203141127_add-kmip.ts @@ -16,11 +16,15 @@ export async function up(knex: Knex): Promise { }); } - const hasKmipInstanceConfigTable = await knex.schema.hasTable(TableName.KmipInstanceConfig); - if (!hasKmipInstanceConfigTable) { - await knex.schema.createTable(TableName.KmipInstanceConfig, (t) => { + const hasKmipOrgPkiConfig = await knex.schema.hasTable(TableName.KmipOrgConfig); + if (!hasKmipOrgPkiConfig) { + await knex.schema.createTable(TableName.KmipOrgConfig, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("orgId").notNullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.unique("orgId"); + t.string("caKeyAlgorithm").notNullable(); t.datetime("rootCaIssuedAt").notNullable(); @@ -46,13 +50,15 @@ export async function up(knex: Knex): Promise { t.timestamps(true, true, true); }); - await createOnUpdateTrigger(knex, TableName.KmipInstanceConfig); + await createOnUpdateTrigger(knex, TableName.KmipOrgConfig); } - const hasKmipInstanceServerCertTable = await knex.schema.hasTable(TableName.KmipInstanceServerCertificates); - if (!hasKmipInstanceServerCertTable) { - await knex.schema.createTable(TableName.KmipInstanceServerCertificates, (t) => { + const hasKmipOrgServerCertTable = await knex.schema.hasTable(TableName.KmipOrgServerCertificates); + if (!hasKmipOrgServerCertTable) { + await knex.schema.createTable(TableName.KmipOrgServerCertificates, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("orgId").notNullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); t.string("commonName").notNullable(); t.string("altNames").notNullable(); t.string("serialNumber").notNullable(); @@ -79,15 +85,15 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { - const hasKmipInstanceConfigTable = await knex.schema.hasTable(TableName.KmipInstanceConfig); - if (hasKmipInstanceConfigTable) { - await knex.schema.dropTable(TableName.KmipInstanceConfig); - await dropOnUpdateTrigger(knex, TableName.KmipInstanceConfig); + const hasKmipOrgPkiConfig = await knex.schema.hasTable(TableName.KmipOrgConfig); + if (hasKmipOrgPkiConfig) { + await knex.schema.dropTable(TableName.KmipOrgConfig); + await dropOnUpdateTrigger(knex, TableName.KmipOrgConfig); } - const hasKmipInstanceServerCertTable = await knex.schema.hasTable(TableName.KmipInstanceServerCertificates); - if (hasKmipInstanceServerCertTable) { - await knex.schema.dropTable(TableName.KmipInstanceServerCertificates); + const hasKmipOrgServerCertTable = await knex.schema.hasTable(TableName.KmipOrgServerCertificates); + if (hasKmipOrgServerCertTable) { + await knex.schema.dropTable(TableName.KmipOrgServerCertificates); } const hasKmipClientCertTable = await knex.schema.hasTable(TableName.KmipClientCertificates); diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 03f1f1136..737193836 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -47,8 +47,8 @@ export * from "./integrations"; export * from "./internal-kms"; export * from "./kmip-client-certificates"; export * from "./kmip-clients"; -export * from "./kmip-instance-configs"; -export * from "./kmip-instance-server-certificates"; +export * from "./kmip-org-configs"; +export * from "./kmip-org-server-certificates"; export * from "./kms-key-versions"; export * from "./kms-keys"; export * from "./kms-root-config"; diff --git a/backend/src/db/schemas/kmip-instance-configs.ts b/backend/src/db/schemas/kmip-org-configs.ts similarity index 76% rename from backend/src/db/schemas/kmip-instance-configs.ts rename to backend/src/db/schemas/kmip-org-configs.ts index 62038fd8a..e75d76413 100644 --- a/backend/src/db/schemas/kmip-instance-configs.ts +++ b/backend/src/db/schemas/kmip-org-configs.ts @@ -9,8 +9,9 @@ import { zodBuffer } from "@app/lib/zod"; import { TImmutableDBKeys } from "./models"; -export const KmipInstanceConfigsSchema = z.object({ +export const KmipOrgConfigsSchema = z.object({ id: z.string().uuid(), + orgId: z.string().uuid(), caKeyAlgorithm: z.string(), rootCaIssuedAt: z.date(), rootCaExpiration: z.date(), @@ -33,6 +34,6 @@ export const KmipInstanceConfigsSchema = z.object({ updatedAt: z.date() }); -export type TKmipInstanceConfigs = z.infer; -export type TKmipInstanceConfigsInsert = Omit, TImmutableDBKeys>; -export type TKmipInstanceConfigsUpdate = Partial, TImmutableDBKeys>>; +export type TKmipOrgConfigs = z.infer; +export type TKmipOrgConfigsInsert = Omit, TImmutableDBKeys>; +export type TKmipOrgConfigsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/kmip-instance-server-certificates.ts b/backend/src/db/schemas/kmip-org-server-certificates.ts similarity index 55% rename from backend/src/db/schemas/kmip-instance-server-certificates.ts rename to backend/src/db/schemas/kmip-org-server-certificates.ts index a13188b06..66e5dcbd6 100644 --- a/backend/src/db/schemas/kmip-instance-server-certificates.ts +++ b/backend/src/db/schemas/kmip-org-server-certificates.ts @@ -9,8 +9,9 @@ import { zodBuffer } from "@app/lib/zod"; import { TImmutableDBKeys } from "./models"; -export const KmipInstanceServerCertificatesSchema = z.object({ +export const KmipOrgServerCertificatesSchema = z.object({ id: z.string().uuid(), + orgId: z.string().uuid(), commonName: z.string(), altNames: z.string(), serialNumber: z.string(), @@ -21,11 +22,8 @@ export const KmipInstanceServerCertificatesSchema = z.object({ encryptedChain: zodBuffer }); -export type TKmipInstanceServerCertificates = z.infer; -export type TKmipInstanceServerCertificatesInsert = Omit< - z.input, - TImmutableDBKeys ->; -export type TKmipInstanceServerCertificatesUpdate = Partial< - Omit, TImmutableDBKeys> +export type TKmipOrgServerCertificates = z.infer; +export type TKmipOrgServerCertificatesInsert = Omit, TImmutableDBKeys>; +export type TKmipOrgServerCertificatesUpdate = Partial< + Omit, TImmutableDBKeys> >; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 12f5768b1..3c5d1cad8 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -134,8 +134,8 @@ export enum TableName { AppConnection = "app_connections", SecretSync = "secret_syncs", KmipClient = "kmip_clients", - KmipInstanceConfig = "kmip_instance_configs", - KmipInstanceServerCertificates = "kmip_instance_server_certificates", + KmipOrgConfig = "kmip_org_configs", + KmipOrgServerCertificates = "kmip_org_server_certificates", KmipClientCertificates = "kmip_client_certificates" } diff --git a/backend/src/ee/routes/v1/kmip-router.ts b/backend/src/ee/routes/v1/kmip-router.ts index dcbc3a501..378592aba 100644 --- a/backend/src/ee/routes/v1/kmip-router.ts +++ b/backend/src/ee/routes/v1/kmip-router.ts @@ -10,6 +10,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; +import { validateAltNamesField } from "@app/services/certificate-authority/certificate-authority-validators"; const KmipClientResponseSchema = KmipClientsSchema.pick({ projectId: true, @@ -285,4 +286,92 @@ export const registerKmipRouter = async (server: FastifyZodProvider) => { return certificate; } }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm) + }), + response: { + 200: z.object({ + serverCertificateChain: z.string(), + clientCertificateChain: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + return server.services.kmip.setupOrgKmip({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + serverCertificateChain: z.string(), + clientCertificateChain: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + return server.services.kmip.getOrgKmip({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + } + }); + + server.route({ + method: "POST", + url: "/server-certificates", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + commonName: z.string().trim().min(1), + altNames: validateAltNamesField, + 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) => { + return server.services.kmip.generateOrgKmipServerCertificate({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + } + }); }; diff --git a/backend/src/ee/services/kmip/kmip-constants.ts b/backend/src/ee/services/kmip/kmip-constants.ts deleted file mode 100644 index 9e7518875..000000000 --- a/backend/src/ee/services/kmip/kmip-constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const INSTANCE_KMIP_CONFIG_ID = "00000000-0000-0000-0000-000000000000"; diff --git a/backend/src/ee/services/kmip/kmip-instance-config-dal.ts b/backend/src/ee/services/kmip/kmip-instance-config-dal.ts deleted file mode 100644 index 61b994d1a..000000000 --- a/backend/src/ee/services/kmip/kmip-instance-config-dal.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TKmipInstanceConfigDALFactory = ReturnType; - -export const kmipInstanceConfigDALFactory = (db: TDbClient) => { - const kmipInstanceConfigOrm = ormify(db, TableName.KmipInstanceConfig); - - return { - ...kmipInstanceConfigOrm - }; -}; diff --git a/backend/src/ee/services/kmip/kmip-instance-server-certificate-dal.ts b/backend/src/ee/services/kmip/kmip-instance-server-certificate-dal.ts deleted file mode 100644 index ebaeb1142..000000000 --- a/backend/src/ee/services/kmip/kmip-instance-server-certificate-dal.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TKmipInstanceServerCertificateDALFactory = ReturnType; - -export const kmipInstanceServerCertificateDALFactory = (db: TDbClient) => { - const kmipInstanceServerCertificateOrm = ormify(db, TableName.KmipInstanceServerCertificates); - - return { - ...kmipInstanceServerCertificateOrm - }; -}; diff --git a/backend/src/ee/services/kmip/kmip-org-config-dal.ts b/backend/src/ee/services/kmip/kmip-org-config-dal.ts new file mode 100644 index 000000000..7f1ebead3 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-org-config-dal.ts @@ -0,0 +1,12 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TKmipOrgConfigDALFactory = ReturnType; + +export const kmipOrgConfigDALFactory = (db: TDbClient) => { + const kmipOrgConfigOrm = ormify(db, TableName.KmipOrgConfig); + return { + ...kmipOrgConfigOrm + }; +}; diff --git a/backend/src/ee/services/kmip/kmip-org-server-certificate-dal.ts b/backend/src/ee/services/kmip/kmip-org-server-certificate-dal.ts new file mode 100644 index 000000000..956d2615c --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-org-server-certificate-dal.ts @@ -0,0 +1,13 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TKmipOrgServerCertificateDALFactory = ReturnType; + +export const kmipOrgServerCertificateDALFactory = (db: TDbClient) => { + const kmipOrgServerCertificateOrm = ormify(db, TableName.KmipOrgServerCertificates); + + return { + ...kmipOrgServerCertificateOrm + }; +}; diff --git a/backend/src/ee/services/kmip/kmip-service.ts b/backend/src/ee/services/kmip/kmip-service.ts index a108dbb41..1f0d403ea 100644 --- a/backend/src/ee/services/kmip/kmip-service.ts +++ b/backend/src/ee/services/kmip/kmip-service.ts @@ -5,36 +5,42 @@ import ms from "ms"; import { ActionProjectType } from "@app/db/schemas"; import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; +import { isValidIp } from "@app/lib/ip"; import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { createSerialNumber, keyAlgorithmToAlgCfg } from "@app/services/certificate-authority/certificate-authority-fns"; +import { hostnameRegex } from "@app/services/certificate-authority/certificate-authority-validators"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; +import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission"; import { TKmipClientCertificateDALFactory } from "./kmip-client-certificate-dal"; import { TKmipClientDALFactory } from "./kmip-client-dal"; -import { INSTANCE_KMIP_CONFIG_ID } from "./kmip-constants"; -import { TKmipInstanceConfigDALFactory } from "./kmip-instance-config-dal"; -import { TKmipInstanceServerCertificateDALFactory } from "./kmip-instance-server-certificate-dal"; +import { TKmipOrgConfigDALFactory } from "./kmip-org-config-dal"; +import { TKmipOrgServerCertificateDALFactory } from "./kmip-org-server-certificate-dal"; import { TCreateKmipClientCertificateDTO, TCreateKmipClientDTO, TDeleteKmipClientDTO, + TGenerateOrgKmipServerCertificateDTO, TGetKmipClientDTO, + TGetOrgKmipDTO, TListKmipClientsByProjectIdDTO, + TSetupOrgKmipDTO, TUpdateKmipClientDTO } from "./kmip-types"; type TKmipServiceFactoryDep = { kmipClientDAL: TKmipClientDALFactory; kmipClientCertificateDAL: TKmipClientCertificateDALFactory; - kmipInstanceServerCertificateDAL: TKmipInstanceServerCertificateDALFactory; - permissionService: Pick; - kmsService: Pick; - kmipInstanceConfigDAL: TKmipInstanceConfigDALFactory; + kmipOrgServerCertificateDAL: TKmipOrgServerCertificateDALFactory; + permissionService: Pick; + kmsService: Pick; + kmipOrgConfigDAL: TKmipOrgConfigDALFactory; }; export type TKmipServiceFactory = ReturnType; @@ -43,9 +49,9 @@ export const kmipServiceFactory = ({ kmipClientDAL, permissionService, kmipClientCertificateDAL, - kmipInstanceConfigDAL, + kmipOrgConfigDAL, kmsService, - kmipInstanceServerCertificateDAL + kmipOrgServerCertificateDAL }: TKmipServiceFactoryDep) => { const createKmipClient = async ({ actor, @@ -226,17 +232,23 @@ export const kmipServiceFactory = ({ ProjectPermissionSub.Kmip ); - const kmipInstanceConfig = await kmipInstanceConfigDAL.findById(INSTANCE_KMIP_CONFIG_ID); - if (!kmipInstanceConfig) { + const kmipConfig = await kmipOrgConfigDAL.findOne({ + orgId: actorOrgId + }); + + if (!kmipConfig) { throw new InternalServerError({ - message: "KMIP has not been configured for the instance." + message: "KMIP has not been configured for the organization" }); } - const decryptWithRoot = kmsService.decryptWithRootKey(); + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); const caCertObj = new x509.X509Certificate( - decryptWithRoot(kmipInstanceConfig.encryptedClientIntermediateCaCertificate) + decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaCertificate }) ); const notBeforeDate = new Date(); @@ -275,14 +287,14 @@ export const kmipServiceFactory = ({ new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true) ]; - const caAlg = keyAlgorithmToAlgCfg(kmipInstanceConfig.caKeyAlgorithm as CertKeyAlgorithm); + const caAlg = keyAlgorithmToAlgCfg(kmipConfig.caKeyAlgorithm as CertKeyAlgorithm); - const decryptedCaCertChain = decryptWithRoot(kmipInstanceConfig.encryptedClientIntermediateCaChain).toString( + const decryptedCaCertChain = decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaChain }).toString( "utf-8" ); const caSkObj = crypto.createPrivateKey({ - key: decryptWithRoot(kmipInstanceConfig.encryptedClientIntermediateCaPrivateKey), + key: decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaPrivateKey }), format: "der", type: "pkcs8" }); @@ -328,23 +340,350 @@ export const kmipServiceFactory = ({ }; }; - const getServerCertificateBySerialNumber = async (serialNumber: string) => { - const serverCert = await kmipInstanceServerCertificateDAL.findOne({ - serialNumber + const setupOrgKmip = async ({ caKeyAlgorithm, actorOrgId, actor, actorId, actorAuthMethod }: TSetupOrgKmipDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip); + + const kmipConfig = await kmipOrgConfigDAL.findOne({ + orgId: actorOrgId }); - if (!serverCert) { - throw new NotFoundError({ - message: "Server certificate not found" + if (kmipConfig) { + throw new BadRequestError({ + message: "KMIP has already been configured for the organization" }); } - const decryptWithRootKey = kmsService.decryptWithRootKey(); - const parsedCertificate = new x509.X509Certificate(decryptWithRootKey(serverCert.encryptedCertificate)); + const alg = keyAlgorithmToAlgCfg(caKeyAlgorithm); + + // generate root CA + const rootCaSerialNumber = createSerialNumber(); + const rootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const rootCaSkObj = KeyObject.from(rootCaKeys.privateKey); + const rootCaIssuedAt = new Date(); + const rootCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 20)); + + const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({ + name: `CN=KMIP Root CA,OU=${actorOrgId}`, + serialNumber: rootCaSerialNumber, + notBefore: rootCaIssuedAt, + notAfter: rootCaExpiration, + signingAlgorithm: alg, + keys: rootCaKeys, + extensions: [ + // eslint-disable-next-line no-bitwise + new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), + await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey) + ] + }); + + // generate intermediate server CA + const serverIntermediateCaSerialNumber = createSerialNumber(); + const serverIntermediateCaIssuedAt = new Date(); + const serverIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10)); + const serverIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const serverIntermediateCaSkObj = KeyObject.from(serverIntermediateCaKeys.privateKey); + + const serverIntermediateCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: serverIntermediateCaSerialNumber, + subject: `CN=KMIP Server Intermediate CA,OU=${actorOrgId}`, + issuer: rootCaCert.subject, + notBefore: serverIntermediateCaIssuedAt, + notAfter: serverIntermediateCaExpiration, + signingKey: rootCaKeys.privateKey, + publicKey: serverIntermediateCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(serverIntermediateCaKeys.publicKey) + ] + }); + + // generate intermediate client CA + const clientIntermediateCaSerialNumber = createSerialNumber(); + const clientIntermediateCaIssuedAt = new Date(); + const clientIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10)); + const clientIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const clientIntermediateCaSkObj = KeyObject.from(clientIntermediateCaKeys.privateKey); + + const clientIntermediateCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: clientIntermediateCaSerialNumber, + subject: `CN=KMIP Client Intermediate CA,OU=${actorOrgId}`, + issuer: rootCaCert.subject, + notBefore: clientIntermediateCaIssuedAt, + notAfter: clientIntermediateCaExpiration, + signingKey: rootCaKeys.privateKey, + publicKey: clientIntermediateCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(clientIntermediateCaKeys.publicKey) + ] + }); + + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + await kmipOrgConfigDAL.create({ + orgId: actorOrgId, + caKeyAlgorithm, + rootCaIssuedAt, + rootCaExpiration, + rootCaSerialNumber, + encryptedRootCaCertificate: encryptor({ plainText: Buffer.from(rootCaCert.rawData) }).cipherTextBlob, + encryptedRootCaPrivateKey: encryptor({ + plainText: rootCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + }).cipherTextBlob, + serverIntermediateCaIssuedAt, + serverIntermediateCaExpiration, + serverIntermediateCaSerialNumber, + encryptedServerIntermediateCaCertificate: encryptor({ + plainText: Buffer.from(new Uint8Array(serverIntermediateCaCert.rawData)) + }).cipherTextBlob, + encryptedServerIntermediateCaChain: encryptor({ plainText: Buffer.from(rootCaCert.toString("pem")) }) + .cipherTextBlob, + encryptedServerIntermediateCaPrivateKey: encryptor({ + plainText: serverIntermediateCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + }).cipherTextBlob, + clientIntermediateCaIssuedAt, + clientIntermediateCaExpiration, + clientIntermediateCaSerialNumber, + encryptedClientIntermediateCaCertificate: encryptor({ + plainText: Buffer.from(new Uint8Array(clientIntermediateCaCert.rawData)) + }).cipherTextBlob, + encryptedClientIntermediateCaChain: encryptor({ plainText: Buffer.from(rootCaCert.toString("pem")) }) + .cipherTextBlob, + encryptedClientIntermediateCaPrivateKey: encryptor({ + plainText: clientIntermediateCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + }).cipherTextBlob + }); return { - publicKey: parsedCertificate.publicKey.toString("pem"), - keyAlgorithm: serverCert.keyAlgorithm as CertKeyAlgorithm + serverCertificateChain: `${serverIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(), + clientCertificateChain: `${clientIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim() + }; + }; + + const getOrgKmip = async ({ actorOrgId }: TGetOrgKmipDTO) => { + const kmipConfig = await kmipOrgConfigDAL.findOne({ + orgId: actorOrgId + }); + + if (!kmipConfig) { + throw new BadRequestError({ + message: "KMIP has not been configured for the organization" + }); + } + + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + const rootCaCert = new x509.X509Certificate(decryptor({ cipherTextBlob: kmipConfig.encryptedRootCaCertificate })); + const serverIntermediateCaCert = new x509.X509Certificate( + decryptor({ cipherTextBlob: kmipConfig.encryptedServerIntermediateCaCertificate }) + ); + + const clientIntermediateCaCert = new x509.X509Certificate( + decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaCertificate }) + ); + + return { + 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, + 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 + }); + + if (!kmipOrgConfig) { + throw new BadRequestError({ + message: "KMIP has not been configured for the organization" + }); + } + + const { decryptor, encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + const caCertObj = new x509.X509Certificate( + decryptor({ cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaCertificate }) + ); + + 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], + true + ), + new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true) + ]; + + const altNamesArray: { + type: "email" | "dns" | "ip"; + value: string; + }[] = altNames + .split(",") + .map((name) => name.trim()) + .map((altName) => { + // check if the altName is a valid hostname + if (hostnameRegex.test(altName)) { + return { + type: "dns", + value: altName + }; + } + + // check if the altName is a valid IP + if (isValidIp(altName)) { + return { + type: "ip", + value: altName + }; + } + + throw new Error(`Invalid altName: ${altName}`); + }); + + const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); + extensions.push(altNamesExtension); + + const caAlg = keyAlgorithmToAlgCfg(kmipOrgConfig.caKeyAlgorithm as CertKeyAlgorithm); + + const decryptedCaCertChain = decryptor({ + cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaChain + }).toString("utf-8"); + + const caSkObj = crypto.createPrivateKey({ + key: decryptor({ cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaPrivateKey }), + 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: `CN=${commonName}`, + 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 kmipOrgServerCertificateDAL.create({ + orgId: actorOrgId, + keyAlgorithm, + issuedAt: notBeforeDate, + expiration: notAfterDate, + serialNumber, + commonName, + altNames, + encryptedCertificate: encryptor({ plainText: Buffer.from(new Uint8Array(leafCert.rawData)) }).cipherTextBlob, + encryptedChain: encryptor({ plainText: Buffer.from(certificateChain) }).cipherTextBlob + }); + + return { + serialNumber, + privateKey: skLeafObj.export({ format: "pem", type: "pkcs8" }) as string, + certificate: leafCert.toString("pem"), + certificateChain }; }; @@ -355,6 +694,8 @@ export const kmipServiceFactory = ({ getKmipClient, listKmipClientsByProjectId, createKmipClientCertificate, - getServerCertificateBySerialNumber + setupOrgKmip, + generateOrgKmipServerCertificate, + getOrgKmip }; }; diff --git a/backend/src/ee/services/kmip/kmip-types.ts b/backend/src/ee/services/kmip/kmip-types.ts index aafce7454..3e8ce7a3b 100644 --- a/backend/src/ee/services/kmip/kmip-types.ts +++ b/backend/src/ee/services/kmip/kmip-types.ts @@ -1,5 +1,5 @@ import { SymmetricEncryption } from "@app/lib/crypto/cipher"; -import { OrderByDirection, TProjectPermission } from "@app/lib/types"; +import { OrderByDirection, TOrgPermission, TProjectPermission } from "@app/lib/types"; import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; import { KmipPermission } from "./kmip-enum"; @@ -79,3 +79,16 @@ export type TKmipRegisterDTO = { key: string; algorithm: SymmetricEncryption; } & KmipOperationBaseDTO; + +export type TSetupOrgKmipDTO = { + caKeyAlgorithm: CertKeyAlgorithm; +} & Omit; + +export type TGetOrgKmipDTO = Omit; + +export type TGenerateOrgKmipServerCertificateDTO = { + commonName: string; + altNames: string; + keyAlgorithm: CertKeyAlgorithm; + ttl: string; +} & Omit; diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index c72008057..e71e251a1 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -23,6 +23,11 @@ export enum OrgPermissionAppConnectionActions { Connect = "connect" } +export enum OrgPermissionKmipActions { + Proxy = "proxy", + Setup = "setup" +} + export enum OrgPermissionAdminConsoleAction { AccessAllProjects = "access-all-projects" } @@ -44,7 +49,8 @@ export enum OrgPermissionSubjects { AdminConsole = "organization-admin-console", AuditLogs = "audit-logs", ProjectTemplates = "project-templates", - AppConnections = "app-connections" + AppConnections = "app-connections", + Kmip = "kmip" } export type AppConnectionSubjectFields = { @@ -74,7 +80,8 @@ export type OrgPermissionSet = | (ForcedSubject & AppConnectionSubjectFields) ) ] - | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]; + | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole] + | [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]; const AppConnectionConditionSchema = z .object({ @@ -167,6 +174,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [ action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionAdminConsoleAction).describe( "Describe what action an entity can take." ) + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.Kmip).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionKmipActions).describe( + "Describe what action an entity can take." + ) }) ]); @@ -253,6 +266,8 @@ const buildAdminPermission = () => { can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); + can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip); + return rules; }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index d9e3ff977..01967f061 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -37,9 +37,9 @@ import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/servic import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; import { kmipClientCertificateDALFactory } from "@app/ee/services/kmip/kmip-client-certificate-dal"; import { kmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal"; -import { kmipInstanceConfigDALFactory } from "@app/ee/services/kmip/kmip-instance-config-dal"; -import { kmipInstanceServerCertificateDALFactory } from "@app/ee/services/kmip/kmip-instance-server-certificate-dal"; import { kmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service"; +import { kmipOrgConfigDALFactory } from "@app/ee/services/kmip/kmip-org-config-dal"; +import { kmipOrgServerCertificateDALFactory } from "@app/ee/services/kmip/kmip-org-server-certificate-dal"; import { kmipServiceFactory } from "@app/ee/services/kmip/kmip-service"; import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal"; import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; @@ -388,8 +388,8 @@ export const registerRoutes = async ( const resourceMetadataDAL = resourceMetadataDALFactory(db); const kmipClientDAL = kmipClientDALFactory(db); const kmipClientCertificateDAL = kmipClientCertificateDALFactory(db); - const kmipInstanceConfigDAL = kmipInstanceConfigDALFactory(db); - const kmipInstanceServerCertificateDAL = kmipInstanceServerCertificateDALFactory(db); + const kmipOrgConfigDAL = kmipOrgConfigDALFactory(db); + const kmipOrgServerCertificateDAL = kmipOrgServerCertificateDALFactory(db); const permissionService = permissionServiceFactory({ permissionDAL, @@ -630,9 +630,7 @@ export const registerRoutes = async ( orgService, keyStore, licenseService, - kmsService, - kmipInstanceConfigDAL, - kmipInstanceServerCertificateDAL + kmsService }); const orgAdminService = orgAdminServiceFactory({ @@ -1434,9 +1432,9 @@ export const registerRoutes = async ( kmipClientDAL, permissionService, kmipClientCertificateDAL, - kmipInstanceConfigDAL, + kmipOrgConfigDAL, kmsService, - kmipInstanceServerCertificateDAL + kmipOrgServerCertificateDAL }); const kmipOperationService = kmipOperationServiceFactory({ diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 2ca79974b..6ecebb274 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -1,4 +1,3 @@ -import ms from "ms"; import { z } from "zod"; import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; @@ -8,8 +7,6 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; -import { validateAltNamesField } from "@app/services/certificate-authority/certificate-authority-validators"; import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { LoginMethod } from "@app/services/super-admin/super-admin-types"; @@ -319,91 +316,4 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }; } }); - - server.route({ - method: "POST", - url: "/kmip", - config: { - rateLimit: writeLimit - }, - schema: { - body: z.object({ - caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm) - }), - response: { - 200: z.object({ - serverCertificateChain: z.string(), - clientCertificateChain: z.string() - }) - } - }, - onRequest: (req, res, done) => { - verifyAuth([AuthMode.JWT])(req, res, () => { - verifySuperAdmin(req, res, done); - }); - }, - handler: async (req) => { - return server.services.superAdmin.setupInstanceKmip({ - ...req.body - }); - } - }); - - server.route({ - method: "GET", - url: "/kmip", - config: { - rateLimit: readLimit - }, - schema: { - response: { - 200: z.object({ - serverCertificateChain: z.string(), - clientCertificateChain: z.string() - }) - } - }, - onRequest: (req, res, done) => { - verifyAuth([AuthMode.JWT])(req, res, () => { - verifySuperAdmin(req, res, done); - }); - }, - handler: async () => { - return server.services.superAdmin.getInstanceKmip(); - } - }); - - server.route({ - method: "POST", - url: "/kmip/server-certificates", - config: { - rateLimit: writeLimit - }, - schema: { - body: z.object({ - commonName: z.string().trim().min(1), - altNames: validateAltNamesField, - 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: (req, res, done) => { - verifyAuth([AuthMode.JWT])(req, res, () => { - verifySuperAdmin(req, res, done); - }); - }, - handler: async (req) => { - return server.services.superAdmin.generateInstanceKmipServerCertificate({ - ...req.body - }); - } - }); }; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 278a359c3..b0fdd9c5c 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -1,24 +1,15 @@ -import * as x509 from "@peculiar/x509"; import bcrypt from "bcrypt"; -import crypto, { KeyObject } from "crypto"; -import ms from "ms"; import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; -import { TKmipInstanceConfigDALFactory } from "@app/ee/services/kmip/kmip-instance-config-dal"; -import { TKmipInstanceServerCertificateDALFactory } from "@app/ee/services/kmip/kmip-instance-server-certificate-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { getUserPrivateKey } from "@app/lib/crypto/srp"; -import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; -import { isValidIp } from "@app/lib/ip"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TAuthLoginFactory } from "../auth/auth-login-service"; import { AuthMethod } from "../auth/auth-type"; -import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificate/certificate-types"; -import { createSerialNumber, keyAlgorithmToAlgCfg } from "../certificate-authority/certificate-authority-fns"; -import { hostnameRegex } from "../certificate-authority/certificate-authority-validators"; import { KMS_ROOT_CONFIG_UUID } from "../kms/kms-fns"; import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; @@ -28,13 +19,7 @@ import { TUserDALFactory } from "../user/user-dal"; import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; import { UserAliasType } from "../user-alias/user-alias-types"; import { TSuperAdminDALFactory } from "./super-admin-dal"; -import { - LoginMethod, - TAdminGetUsersDTO, - TAdminSignUpDTO, - TGenerateInstanceKmipServerCertificateDTO, - TSetupInstanceKmipDTO -} from "./super-admin-types"; +import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types"; type TSuperAdminServiceFactoryDep = { serverCfgDAL: TSuperAdminDALFactory; @@ -46,8 +31,6 @@ type TSuperAdminServiceFactoryDep = { orgService: Pick; keyStore: Pick; licenseService: Pick; - kmipInstanceConfigDAL: TKmipInstanceConfigDALFactory; - kmipInstanceServerCertificateDAL: TKmipInstanceServerCertificateDALFactory; }; export type TSuperAdminServiceFactory = ReturnType; @@ -74,9 +57,7 @@ export const superAdminServiceFactory = ({ keyStore, kmsRootConfigDAL, kmsService, - licenseService, - kmipInstanceConfigDAL, - kmipInstanceServerCertificateDAL + licenseService }: TSuperAdminServiceFactoryDep) => { const initServerCfg = async () => { // TODO(akhilmhdh): bad pattern time less change this later to me itself @@ -388,309 +369,6 @@ export const superAdminServiceFactory = ({ await kmsService.updateEncryptionStrategy(strategy); }; - const setupInstanceKmip = async ({ caKeyAlgorithm }: TSetupInstanceKmipDTO) => { - const kmipInstanceConfig = await kmipInstanceConfigDAL.findById(ADMIN_CONFIG_DB_UUID); - if (kmipInstanceConfig) { - throw new BadRequestError({ - message: "KMIP has already been configured for the instance" - }); - } - - const alg = keyAlgorithmToAlgCfg(caKeyAlgorithm); - - // generate root CA - const rootCaSerialNumber = createSerialNumber(); - const rootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); - const rootCaSkObj = KeyObject.from(rootCaKeys.privateKey); - const rootCaIssuedAt = new Date(); - const rootCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 20)); - - const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({ - name: "CN=KMIP Root CA", - serialNumber: rootCaSerialNumber, - notBefore: rootCaIssuedAt, - notAfter: rootCaExpiration, - signingAlgorithm: alg, - keys: rootCaKeys, - extensions: [ - // eslint-disable-next-line no-bitwise - new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), - await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey) - ] - }); - - // generate intermediate server CA - const serverIntermediateCaSerialNumber = createSerialNumber(); - const serverIntermediateCaIssuedAt = new Date(); - const serverIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10)); - const serverIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); - const serverIntermediateCaSkObj = KeyObject.from(serverIntermediateCaKeys.privateKey); - - const serverIntermediateCaCert = await x509.X509CertificateGenerator.create({ - serialNumber: serverIntermediateCaSerialNumber, - subject: "CN=KMIP Server Intermediate CA", - issuer: rootCaCert.subject, - notBefore: serverIntermediateCaIssuedAt, - notAfter: serverIntermediateCaExpiration, - signingKey: rootCaKeys.privateKey, - publicKey: serverIntermediateCaKeys.publicKey, - signingAlgorithm: alg, - extensions: [ - new x509.KeyUsagesExtension( - // eslint-disable-next-line no-bitwise - x509.KeyUsageFlags.keyCertSign | - x509.KeyUsageFlags.cRLSign | - x509.KeyUsageFlags.digitalSignature | - x509.KeyUsageFlags.keyEncipherment, - true - ), - new x509.BasicConstraintsExtension(true, 0, true), - await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), - await x509.SubjectKeyIdentifierExtension.create(serverIntermediateCaKeys.publicKey) - ] - }); - - // generate intermediate client CA - const clientIntermediateCaSerialNumber = createSerialNumber(); - const clientIntermediateCaIssuedAt = new Date(); - const clientIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10)); - const clientIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); - const clientIntermediateCaSkObj = KeyObject.from(clientIntermediateCaKeys.privateKey); - - const clientIntermediateCaCert = await x509.X509CertificateGenerator.create({ - serialNumber: clientIntermediateCaSerialNumber, - subject: "CN=KMIP Client Intermediate CA", - issuer: rootCaCert.subject, - notBefore: clientIntermediateCaIssuedAt, - notAfter: clientIntermediateCaExpiration, - signingKey: rootCaKeys.privateKey, - publicKey: clientIntermediateCaKeys.publicKey, - signingAlgorithm: alg, - extensions: [ - new x509.KeyUsagesExtension( - // eslint-disable-next-line no-bitwise - x509.KeyUsageFlags.keyCertSign | - x509.KeyUsageFlags.cRLSign | - x509.KeyUsageFlags.digitalSignature | - x509.KeyUsageFlags.keyEncipherment, - true - ), - new x509.BasicConstraintsExtension(true, 0, true), - await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), - await x509.SubjectKeyIdentifierExtension.create(clientIntermediateCaKeys.publicKey) - ] - }); - - const encryptWithRoot = kmsService.encryptWithRootKey(); - - await kmipInstanceConfigDAL.create({ - // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition - id: ADMIN_CONFIG_DB_UUID, - caKeyAlgorithm, - rootCaIssuedAt, - rootCaExpiration, - rootCaSerialNumber, - encryptedRootCaCertificate: encryptWithRoot(Buffer.from(rootCaCert.rawData)), - encryptedRootCaPrivateKey: encryptWithRoot( - rootCaSkObj.export({ - type: "pkcs8", - format: "der" - }) - ), - serverIntermediateCaIssuedAt, - serverIntermediateCaExpiration, - serverIntermediateCaSerialNumber, - encryptedServerIntermediateCaCertificate: encryptWithRoot( - Buffer.from(new Uint8Array(serverIntermediateCaCert.rawData)) - ), - encryptedServerIntermediateCaChain: encryptWithRoot(Buffer.from(rootCaCert.toString("pem"))), - encryptedServerIntermediateCaPrivateKey: encryptWithRoot( - serverIntermediateCaSkObj.export({ - type: "pkcs8", - format: "der" - }) - ), - clientIntermediateCaIssuedAt, - clientIntermediateCaExpiration, - clientIntermediateCaSerialNumber, - encryptedClientIntermediateCaCertificate: encryptWithRoot( - Buffer.from(new Uint8Array(clientIntermediateCaCert.rawData)) - ), - encryptedClientIntermediateCaChain: encryptWithRoot(Buffer.from(rootCaCert.toString("pem"))), - encryptedClientIntermediateCaPrivateKey: encryptWithRoot( - clientIntermediateCaSkObj.export({ - type: "pkcs8", - format: "der" - }) - ) - }); - - return { - serverCertificateChain: `${serverIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(), - clientCertificateChain: `${clientIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim() - }; - }; - - const getInstanceKmip = async () => { - const kmipInstanceConfig = await kmipInstanceConfigDAL.findById(ADMIN_CONFIG_DB_UUID); - if (!kmipInstanceConfig) { - throw new BadRequestError({ - message: "KMIP has not been configured for the instance" - }); - } - - const decryptWithRoot = kmsService.decryptWithRootKey(); - const rootCaCert = new x509.X509Certificate(decryptWithRoot(kmipInstanceConfig.encryptedRootCaCertificate)); - const serverIntermediateCaCert = new x509.X509Certificate( - decryptWithRoot(kmipInstanceConfig.encryptedServerIntermediateCaCertificate) - ); - const clientIntermediateCaCert = new x509.X509Certificate( - decryptWithRoot(kmipInstanceConfig.encryptedClientIntermediateCaCertificate) - ); - - return { - serverCertificateChain: `${serverIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(), - clientCertificateChain: `${clientIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim() - }; - }; - - const generateInstanceKmipServerCertificate = async ({ - ttl, - commonName, - altNames, - keyAlgorithm - }: TGenerateInstanceKmipServerCertificateDTO) => { - const kmipInstanceConfig = await kmipInstanceConfigDAL.findById(ADMIN_CONFIG_DB_UUID); - 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.encryptedServerIntermediateCaCertificate) - ); - - 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], - true - ), - new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true) - ]; - - const altNamesArray: { - type: "email" | "dns" | "ip"; - value: string; - }[] = altNames - .split(",") - .map((name) => name.trim()) - .map((altName) => { - // check if the altName is a valid hostname - if (hostnameRegex.test(altName)) { - return { - type: "dns", - value: altName - }; - } - - // check if the altName is a valid IP - if (isValidIp(altName)) { - return { - type: "ip", - value: altName - }; - } - - throw new Error(`Invalid altName: ${altName}`); - }); - - const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); - extensions.push(altNamesExtension); - - const caAlg = keyAlgorithmToAlgCfg(kmipInstanceConfig.caKeyAlgorithm as CertKeyAlgorithm); - - const decryptedCaCertChain = decryptWithRoot(kmipInstanceConfig.encryptedServerIntermediateCaChain).toString( - "utf-8" - ); - - const caSkObj = crypto.createPrivateKey({ - key: decryptWithRoot(kmipInstanceConfig.encryptedServerIntermediateCaPrivateKey), - 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: `CN=${commonName}`, - issuer: caCertObj.subject, - notBefore: notBeforeDate, - notAfter: notAfterDate, - signingKey: caPrivateKey, - publicKey: leafKeys.publicKey, - signingAlgorithm: alg, - extensions - }); - - const encryptWithRoot = kmsService.encryptWithRootKey(); - const skLeafObj = KeyObject.from(leafKeys.privateKey); - const certificateChain = `${caCertObj.toString("pem")}\n${decryptedCaCertChain}`.trim(); - - await kmipInstanceServerCertificateDAL.create({ - keyAlgorithm, - issuedAt: notBeforeDate, - expiration: notAfterDate, - serialNumber, - commonName, - altNames, - encryptedCertificate: encryptWithRoot(Buffer.from(new Uint8Array(leafCert.rawData))), - encryptedChain: encryptWithRoot(Buffer.from(certificateChain)) - }); - - return { - serialNumber, - privateKey: skLeafObj.export({ format: "pem", type: "pkcs8" }) as string, - certificate: leafCert.toString("pem"), - certificateChain - }; - }; - return { initServerCfg, updateServerCfg, @@ -699,9 +377,6 @@ export const superAdminServiceFactory = ({ deleteUser, getAdminSlackConfig, updateRootEncryptionStrategy, - getConfiguredEncryptionStrategies, - setupInstanceKmip, - getInstanceKmip, - generateInstanceKmipServerCertificate + getConfiguredEncryptionStrategies }; }; diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index e246dfed8..2d10941b4 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -1,5 +1,3 @@ -import { CertKeyAlgorithm } from "../certificate/certificate-types"; - export type TAdminSignUpDTO = { email: string; password: string; @@ -33,14 +31,3 @@ export enum LoginMethod { LDAP = "ldap", OIDC = "oidc" } - -export type TSetupInstanceKmipDTO = { - caKeyAlgorithm: CertKeyAlgorithm; -}; - -export type TGenerateInstanceKmipServerCertificateDTO = { - commonName: string; - altNames: string; - keyAlgorithm: CertKeyAlgorithm; - ttl: string; -}; diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index ea5003c8c..46329abc6 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -24,7 +24,8 @@ export enum OrgPermissionSubjects { AdminConsole = "organization-admin-console", AuditLogs = "audit-logs", ProjectTemplates = "project-templates", - AppConnections = "app-connections" + AppConnections = "app-connections", + Kmip = "kmip" } export enum OrgPermissionAdminConsoleAction { @@ -39,6 +40,11 @@ export enum OrgPermissionAppConnectionActions { Connect = "connect" } +export enum OrgPermissionKmipActions { + Proxy = "proxy", + Setup = "setup" +} + export type AppConnectionSubjectFields = { connectionId: string; }; @@ -61,7 +67,8 @@ export type OrgPermissionSet = | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole] | [OrgPermissionActions, OrgPermissionSubjects.AuditLogs] | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] - | [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]; + | [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections] + | [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]; // TODO(scott): add back once org UI refactored // | [ // OrgPermissionAppConnectionActions, diff --git a/frontend/src/hooks/api/admin/index.ts b/frontend/src/hooks/api/admin/index.ts index 494426338..5405878c7 100644 --- a/frontend/src/hooks/api/admin/index.ts +++ b/frontend/src/hooks/api/admin/index.ts @@ -1,7 +1,6 @@ export { useAdminDeleteUser, useCreateAdminUser, - useSetupInstanceKmip, useUpdateAdminSlackConfig, useUpdateServerConfig, useUpdateServerEncryptionStrategy @@ -9,7 +8,6 @@ export { export { useAdminGetUsers, useGetAdminSlackConfig, - useGetInstanceKmipConfig, useGetServerConfig, useGetServerRootKmsEncryptionDetails } from "./queries"; diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index 4ad411de4..c68760fa4 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -7,12 +7,9 @@ import { User } from "../users/types"; import { adminQueryKeys, adminStandaloneKeys } from "./queries"; import { AdminSlackConfig, - InstanceKmipServerCert, RootKeyEncryptionStrategy, TCreateAdminUserDTO, - TGenerateInstanceKmipServerCertDTO, TServerConfig, - TSetupInstanceKmipDTO, TUpdateAdminSlackConfigDTO } from "./types"; @@ -101,26 +98,3 @@ export const useUpdateServerEncryptionStrategy = () => { } }); }; - -export const useSetupInstanceKmip = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async (payload: TSetupInstanceKmipDTO) => { - await apiRequest.post("/api/v1/admin/kmip", payload); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: adminQueryKeys.getInstanceKmip() }); - } - }); -}; - -export const useGenerateInstanceKmipServerCert = () => { - return useMutation({ - mutationFn: async (payload: TGenerateInstanceKmipServerCertDTO) => { - return apiRequest.post( - "/api/v1/admin/kmip/server-certificates", - payload - ); - } - }); -}; diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index 4c0676d50..6084645f9 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -6,7 +6,6 @@ import { User } from "../types"; import { AdminGetUsersFilters, AdminSlackConfig, - InstanceKmipConfig, TGetServerRootKmsEncryptionDetails, TServerConfig } from "./types"; @@ -95,14 +94,3 @@ export const useGetServerRootKmsEncryptionDetails = () => { } }); }; - -export const useGetInstanceKmipConfig = () => { - return useQuery({ - queryKey: adminQueryKeys.getInstanceKmip(), - queryFn: async () => { - const { data } = await apiRequest.get("/api/v1/admin/kmip"); - - return data; - } - }); -}; diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 809397497..60fa3ab98 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -1,5 +1,3 @@ -import { CertKeyAlgorithm } from "../certificates/enums"; - export enum LoginMethod { EMAIL = "email", GOOGLE = "google", @@ -64,30 +62,7 @@ export type TGetServerRootKmsEncryptionDetails = { }[]; }; -export type InstanceKmipConfig = { - serverCertificateChain: string; - clientCertificateChain: string; -}; - export enum RootKeyEncryptionStrategy { Software = "SOFTWARE", HSM = "HSM" } - -export type TSetupInstanceKmipDTO = { - caKeyAlgorithm: CertKeyAlgorithm; -}; - -export type TGenerateInstanceKmipServerCertDTO = { - commonName: string; - keyAlgorithm: CertKeyAlgorithm; - altNames: string; - ttl: string; -}; - -export type InstanceKmipServerCert = { - serialNumber: string; - certificate: string; - certificateChain: string; - privateKey: string; -}; diff --git a/frontend/src/hooks/api/kmip/mutation.ts b/frontend/src/hooks/api/kmip/mutation.ts index 7e42dc050..fed5e01b0 100644 --- a/frontend/src/hooks/api/kmip/mutation.ts +++ b/frontend/src/hooks/api/kmip/mutation.ts @@ -5,9 +5,12 @@ import { apiRequest } from "@app/config/request"; import { kmipKeys } from "./queries"; import { KmipClientCertificate, + OrgKmipServerCert, TCreateKmipClient, TDeleteKmipClient, TGenerateKmipClientCertificate, + TGenerateOrgKmipServerCertDTO, + TSetupOrgKmipDTO, TUpdateKmipClient } from "./types"; @@ -75,3 +78,23 @@ export const useGenerateKmipClientCertificate = () => { } }); }; + +export const useSetupOrgKmip = (orgId: string) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (payload: TSetupOrgKmipDTO) => { + await apiRequest.post("/api/v1/kmip", payload); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: kmipKeys.getOrgKmip(orgId) }); + } + }); +}; + +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/queries.tsx b/frontend/src/hooks/api/kmip/queries.tsx index 2816e6b75..f734420a4 100644 --- a/frontend/src/hooks/api/kmip/queries.tsx +++ b/frontend/src/hooks/api/kmip/queries.tsx @@ -3,11 +3,17 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { OrderByDirection } from "../generic/types"; -import { KmipClientOrderBy, TListProjectKmipClientsDTO, TProjectKmipClientList } from "./types"; +import { + KmipClientOrderBy, + OrgKmipConfig, + TListProjectKmipClientsDTO, + TProjectKmipClientList +} from "./types"; export const kmipKeys = { getKmipClientsByProjectId: ({ projectId, ...filters }: TListProjectKmipClientsDTO) => - [projectId, filters] as const + [projectId, filters] as const, + getOrgKmip: (orgId: string) => [{ orgId }, "org-kmip-config"] as const }; export const useGetKmipClientsByProjectId = ( @@ -50,3 +56,14 @@ export const useGetKmipClientsByProjectId = ( ...options }); }; + +export const useGetOrgKmipConfig = (orgId: string) => { + return useQuery({ + queryKey: kmipKeys.getOrgKmip(orgId), + queryFn: async () => { + const { data } = await apiRequest.get("/api/v1/kmip"); + + return data; + } + }); +}; diff --git a/frontend/src/hooks/api/kmip/types.ts b/frontend/src/hooks/api/kmip/types.ts index c090baa20..d8353aa61 100644 --- a/frontend/src/hooks/api/kmip/types.ts +++ b/frontend/src/hooks/api/kmip/types.ts @@ -63,3 +63,26 @@ export type TListProjectKmipClientsDTO = { export enum KmipClientOrderBy { Name = "name" } + +export type OrgKmipConfig = { + serverCertificateChain: string; + clientCertificateChain: string; +}; + +export type TSetupOrgKmipDTO = { + caKeyAlgorithm: CertKeyAlgorithm; +}; + +export type TGenerateOrgKmipServerCertDTO = { + commonName: string; + keyAlgorithm: CertKeyAlgorithm; + altNames: string; + ttl: string; +}; + +export type OrgKmipServerCert = { + serialNumber: string; + certificate: string; + certificateChain: string; + privateKey: string; +}; diff --git a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx index 79f5fdca0..45c2fa3ec 100644 --- a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx @@ -31,7 +31,6 @@ import { import { AuthPanel } from "./components/AuthPanel"; import { EncryptionPanel } from "./components/EncryptionPanel"; import { IntegrationPanel } from "./components/IntegrationPanel"; -import { KmipPanel } from "./components/KmipPanel"; import { RateLimitPanel } from "./components/RateLimitPanel"; import { UserPanel } from "./components/UserPanel"; @@ -151,7 +150,6 @@ export const OverviewPage = () => { Rate Limit Integrations Users - KMIP @@ -350,9 +348,6 @@ export const OverviewPage = () => { - - - )} diff --git a/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts b/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts index 5b73bcf77..209373285 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts +++ b/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts @@ -2,7 +2,10 @@ import { z } from "zod"; import { OrgPermissionSubjects } from "@app/context"; -import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types"; +import { + OrgPermissionAppConnectionActions, + OrgPermissionKmipActions +} from "@app/context/OrgPermissionContext/types"; import { TPermission } from "@app/hooks/api/roles/types"; const generalPermissionSchema = z @@ -24,6 +27,13 @@ const appConnectionsPermissionSchema = z }) .optional(); +const kmipPermissionSchema = z + .object({ + [OrgPermissionKmipActions.Proxy]: z.boolean().optional(), + [OrgPermissionKmipActions.Setup]: z.boolean().optional() + }) + .optional(); + const adminConsolePermissionSchmea = z .object({ "access-all-projects": z.boolean().optional() @@ -61,7 +71,8 @@ export const formSchema = z.object({ "organization-admin-console": adminConsolePermissionSchmea, [OrgPermissionSubjects.Kms]: generalPermissionSchema, [OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema, - "app-connections": appConnectionsPermissionSchema + "app-connections": appConnectionsPermissionSchema, + kmip: kmipPermissionSchema }) .optional() }); diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionKmipRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionKmipRow.tsx new file mode 100644 index 000000000..d4ed86c3c --- /dev/null +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionKmipRow.tsx @@ -0,0 +1,133 @@ +import { useEffect, useMemo } from "react"; +import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; +import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2"; +import { useToggle } from "@app/hooks"; + +import { TFormSchema } from "../OrgRoleModifySection.utils"; + +type Props = { + isEditable: boolean; + setValue: UseFormSetValue; + control: Control; +}; + +enum Permission { + NoAccess = "no-access", + Custom = "custom" +} + +const PERMISSION_ACTIONS = [ + { action: "proxy", label: "Proxy KMIP requests" }, + { action: "setup", label: "Setup KMIP" } +] as const; + +export const OrgPermissionKmipRow = ({ isEditable, control, setValue }: Props) => { + const [isRowExpanded, setIsRowExpanded] = useToggle(); + const [isCustom, setIsCustom] = useToggle(); + + const rule = useWatch({ + control, + name: "permissions.kmip" + }); + + const selectedPermissionCategory = useMemo(() => { + if (rule?.proxy || rule?.setup) { + return Permission.Custom; + } + return Permission.NoAccess; + }, [rule, isCustom]); + + useEffect(() => { + if (selectedPermissionCategory === Permission.Custom) setIsCustom.on(); + else setIsCustom.off(); + }, [selectedPermissionCategory]); + + useEffect(() => { + const isRowCustom = selectedPermissionCategory === Permission.Custom; + if (isRowCustom) { + setIsRowExpanded.on(); + } + }, []); + + const handlePermissionChange = (val: Permission) => { + if (!val) return; + if (val === Permission.Custom) { + setIsRowExpanded.on(); + setIsCustom.on(); + return; + } + setIsCustom.off(); + + if (val === Permission.NoAccess) { + setValue("permissions.kmip", { proxy: false, setup: false }, { shouldDirty: true }); + } + }; + + return ( + <> + setIsRowExpanded.toggle()} + > + + + + KMIP + + + + + {isRowExpanded && ( + + +
+ {PERMISSION_ACTIONS.map(({ action, label }) => { + return ( + ( + { + if (!isEditable) { + createNotification({ + type: "error", + text: "Failed to update default role" + }); + return; + } + field.onChange(e); + }} + id={`permissions.kmip.${action}`} + > + {label} + + )} + /> + ); + })} +
+ + + )} + + ); +}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx index 44f80c18d..9f66e358a 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -14,6 +14,7 @@ import { TFormSchema } from "../OrgRoleModifySection.utils"; import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow"; +import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow"; import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow"; import { RolePermissionRow } from "./RolePermissionRow"; @@ -180,6 +181,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => { setValue={setValue} isEditable={isCustomRole} /> + diff --git a/frontend/src/pages/admin/OverviewPage/components/KmipPanel.tsx b/frontend/src/pages/organization/SettingsPage/components/KmipTab/OrgKmipTab.tsx similarity index 89% rename from frontend/src/pages/admin/OverviewPage/components/KmipPanel.tsx rename to frontend/src/pages/organization/SettingsPage/components/KmipTab/OrgKmipTab.tsx index e22aa3626..c2c5dd5e6 100644 --- a/frontend/src/pages/admin/OverviewPage/components/KmipPanel.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/KmipTab/OrgKmipTab.tsx @@ -18,26 +18,30 @@ import { TextArea, Tooltip } from "@app/components/v2"; +import { useOrganization } from "@app/context"; import { downloadTxtFile } from "@app/helpers/download"; import { usePopUp, useTimedReset } from "@app/hooks"; -import { useGetInstanceKmipConfig, useSetupInstanceKmip } from "@app/hooks/api"; -import { useGenerateInstanceKmipServerCert } from "@app/hooks/api/admin/mutation"; -import { InstanceKmipConfig } from "@app/hooks/api/admin/types"; 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 { OrgKmipConfig } from "@app/hooks/api/kmip/types"; import { CertificateContent } from "@app/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateContent"; -const kmipInstanceConfigFormSchema = z.object({ +const orgConfigFormSchema = z.object({ caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm) }); -type TKmipInstanceConfigForm = z.infer; +type TKmipOrgConfigForm = z.infer; -const KmipInstanceConfigSection = ({ +const OrgConfigSection = ({ kmipConfig, isKmipConfigLoading }: { - kmipConfig?: InstanceKmipConfig; + kmipConfig?: OrgKmipConfig; isKmipConfigLoading: boolean; }) => { const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([ @@ -47,13 +51,15 @@ const KmipInstanceConfigSection = ({ handleSubmit, control, formState: { isSubmitting } - } = useForm({ - resolver: zodResolver(kmipInstanceConfigFormSchema) + } = useForm({ + resolver: zodResolver(orgConfigFormSchema) }); - const { mutateAsync: setupInstanceKmip } = useSetupInstanceKmip(); - const onFormSubmit = async (formData: TKmipInstanceConfigForm) => { - await setupInstanceKmip(formData); + const { currentOrg } = useOrganization(); + const { mutateAsync: setupOrgKmip } = useSetupOrgKmip(currentOrg.id); + + const onFormSubmit = async (formData: TKmipOrgConfigForm) => { + await setupOrgKmip(formData); createNotification({ type: "success", @@ -171,7 +177,7 @@ const KmipInstanceConfigSection = ({ )} {!isKmipConfigLoading && !kmipConfig && (
-
KMIP has not yet been configured for the instance.
+
KMIP has not yet been configured for the organization.