From c7f80f7d9e1e7fd9d1d3f5fa9688a5f2cabe5722 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 4 Feb 2025 01:34:30 +0800 Subject: [PATCH 01/19] feat: kmip client backend setup --- backend/src/@types/fastify.d.ts | 2 + backend/src/@types/knex.d.ts | 4 + .../db/migrations/20250203141127_add-kmip.ts | 24 +++ backend/src/db/schemas/index.ts | 1 + backend/src/db/schemas/kmip-clients.ts | 20 ++ backend/src/db/schemas/models.ts | 3 +- backend/src/ee/routes/v1/index.ts | 2 + backend/src/ee/routes/v1/kmip-router.ts | 183 ++++++++++++++++++ .../ee/services/audit-log/audit-log-types.ts | 46 ++++- .../src/ee/services/kmip/kmip-client-dal.ts | 11 ++ backend/src/ee/services/kmip/kmip-enum.ts | 6 + backend/src/ee/services/kmip/kmip-service.ts | 127 ++++++++++++ backend/src/ee/services/kmip/kmip-types.ts | 24 +++ .../services/permission/project-permission.ts | 22 ++- backend/src/server/routes/index.ts | 11 +- 15 files changed, 481 insertions(+), 5 deletions(-) create mode 100644 backend/src/db/migrations/20250203141127_add-kmip.ts create mode 100644 backend/src/db/schemas/kmip-clients.ts create mode 100644 backend/src/ee/routes/v1/kmip-router.ts create mode 100644 backend/src/ee/services/kmip/kmip-client-dal.ts create mode 100644 backend/src/ee/services/kmip/kmip-enum.ts create mode 100644 backend/src/ee/services/kmip/kmip-service.ts create mode 100644 backend/src/ee/services/kmip/kmip-types.ts diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index f3298625e..d56c13431 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -16,6 +16,7 @@ import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/extern import { TGroupServiceFactory } from "@app/ee/services/group/group-service"; import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; +import { TKmipServiceFactory } from "@app/ee/services/kmip/kmip-service"; import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service"; @@ -212,6 +213,7 @@ declare module "fastify" { totp: TTotpServiceFactory; appConnection: TAppConnectionServiceFactory; secretSync: TSecretSyncServiceFactory; + kmip: TKmipServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 2dad77392..8b3c8b0ed 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -143,6 +143,9 @@ import { TInternalKms, TInternalKmsInsert, TInternalKmsUpdate, + TKmipClients, + TKmipClientsInsert, + TKmipClientsUpdate, TKmsKeys, TKmsKeysInsert, TKmsKeysUpdate, @@ -902,5 +905,6 @@ declare module "knex/types/tables" { TAppConnectionsUpdate >; [TableName.SecretSync]: KnexOriginal.CompositeTableType; + [TableName.KmipClient]: KnexOriginal.CompositeTableType; } } diff --git a/backend/src/db/migrations/20250203141127_add-kmip.ts b/backend/src/db/migrations/20250203141127_add-kmip.ts new file mode 100644 index 000000000..c80059b9d --- /dev/null +++ b/backend/src/db/migrations/20250203141127_add-kmip.ts @@ -0,0 +1,24 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasKmipClientTable = await knex.schema.hasTable(TableName.KmipClient); + if (!hasKmipClientTable) { + await knex.schema.createTable(TableName.KmipClient, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("name").notNullable(); + t.specificType("permissions", "text[]"); + t.string("description"); + t.string("projectId").notNullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasKmipClientTable = await knex.schema.hasTable(TableName.KmipClient); + if (hasKmipClientTable) { + await knex.schema.dropTable(TableName.KmipClient); + } +} diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 9bcfdd49f..f3e687676 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -45,6 +45,7 @@ export * from "./incident-contacts"; export * from "./integration-auths"; export * from "./integrations"; export * from "./internal-kms"; +export * from "./kmip-clients"; export * from "./kms-key-versions"; export * from "./kms-keys"; export * from "./kms-root-config"; diff --git a/backend/src/db/schemas/kmip-clients.ts b/backend/src/db/schemas/kmip-clients.ts new file mode 100644 index 000000000..eb8f31bfb --- /dev/null +++ b/backend/src/db/schemas/kmip-clients.ts @@ -0,0 +1,20 @@ +// 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 KmipClientsSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + permissions: z.string().array().nullable().optional(), + description: z.string().nullable().optional(), + projectId: z.string() +}); + +export type TKmipClients = z.infer; +export type TKmipClientsInsert = Omit, TImmutableDBKeys>; +export type TKmipClientsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 3ead85530..fdaad428b 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -132,7 +132,8 @@ export enum TableName { SlackIntegrations = "slack_integrations", ProjectSlackConfigs = "project_slack_configs", AppConnection = "app_connections", - SecretSync = "secret_syncs" + SecretSync = "secret_syncs", + KmipClient = "kmip_clients" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt"; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 5f931440c..cb2e7e0da 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -9,6 +9,7 @@ import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerExternalKmsRouter } from "./external-kms-router"; import { registerGroupRouter } from "./group-router"; import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router"; +import { registerKmipRouter } from "./kmip-router"; import { registerLdapRouter } from "./ldap-router"; import { registerLicenseRouter } from "./license-router"; import { registerOidcRouter } from "./oidc-router"; @@ -110,4 +111,5 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { }); await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" }); + await server.register(registerKmipRouter, { prefix: "/kmip" }); }; diff --git a/backend/src/ee/routes/v1/kmip-router.ts b/backend/src/ee/routes/v1/kmip-router.ts new file mode 100644 index 000000000..66d3e7e28 --- /dev/null +++ b/backend/src/ee/routes/v1/kmip-router.ts @@ -0,0 +1,183 @@ +import { z } from "zod"; + +import { KmipClientsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { KmipPermission } from "@app/ee/services/kmip/kmip-enum"; +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"; + +const KmipClientResponseSchema = KmipClientsSchema.pick({ + projectId: true, + name: true, + id: true, + description: true, + permissions: true +}); + +export const registerKmipRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/clients", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + projectId: z.string(), + name: z.string().trim().min(1), + description: z.string().optional(), + permissions: z.nativeEnum(KmipPermission).array() + }), + response: { + 200: KmipClientResponseSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const kmipClient = await server.services.kmip.createKmipClient({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: kmipClient.projectId, + event: { + type: EventType.CREATE_KMIP_CLIENT, + metadata: { + id: kmipClient.id, + name: kmipClient.name, + permissions: (kmipClient.permissions ?? []) as KmipPermission[] + } + } + }); + } + }); + + server.route({ + method: "PATCH", + url: "/clients/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + body: z.object({ + name: z.string().trim().min(1), + description: z.string().optional(), + permissions: z.nativeEnum(KmipPermission).array() + }), + response: { + 200: KmipClientResponseSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const kmipClient = await server.services.kmip.updateKmipClient({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.params, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: kmipClient.projectId, + event: { + type: EventType.UPDATE_KMIP_CLIENT, + metadata: { + id: kmipClient.id, + name: kmipClient.name, + permissions: (kmipClient.permissions ?? []) as KmipPermission[] + } + } + }); + } + }); + + server.route({ + method: "DELETE", + url: "/clients/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + response: { + 200: KmipClientResponseSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const kmipClient = await server.services.kmip.deleteKmipClient({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.params + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: kmipClient.projectId, + event: { + type: EventType.DELETE_KMIP_CLIENT, + metadata: { + id: kmipClient.id + } + } + }); + } + }); + + server.route({ + method: "GET", + url: "/clients/:id", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + response: { + 200: KmipClientResponseSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const kmipClient = await server.services.kmip.getKmipClient({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.params + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: kmipClient.projectId, + event: { + type: EventType.GET_KMIP_CLIENT, + metadata: { + id: kmipClient.id + } + } + }); + } + }); +}; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index ffabb3cc4..fca2975be 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -21,6 +21,8 @@ import { TUpdateSecretSyncDTO } from "@app/services/secret-sync/secret-sync-types"; +import { KmipPermission } from "../kmip/kmip-enum"; + export type TListProjectAuditLogDTO = { filter: { userAgentType?: UserAgentType; @@ -251,7 +253,11 @@ export enum EventType { SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets", SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets", OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user", - OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user" + OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user", + CREATE_KMIP_CLIENT = "create-kmip-client", + UPDATE_KMIP_CLIENT = "update-kmip-client", + DELETE_KMIP_CLIENT = "delete-kmip-client", + GET_KMIP_CLIENT = "get-kmip-client" } interface UserActorMetadata { @@ -2066,6 +2072,38 @@ interface OidcGroupMembershipMappingRemoveUserEvent { }; } +interface CreateKmipClientEvent { + type: EventType.CREATE_KMIP_CLIENT; + metadata: { + name: string; + id: string; + permissions: KmipPermission[]; + }; +} + +interface UpdateKmipClientEvent { + type: EventType.UPDATE_KMIP_CLIENT; + metadata: { + name: string; + id: string; + permissions: KmipPermission[]; + }; +} + +interface DeleteKmipClientEvent { + type: EventType.DELETE_KMIP_CLIENT; + metadata: { + id: string; + }; +} + +interface GetKmipClientEvent { + type: EventType.GET_KMIP_CLIENT; + metadata: { + id: string; + }; +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -2256,4 +2294,8 @@ export type Event = | SecretSyncImportSecretsEvent | SecretSyncRemoveSecretsEvent | OidcGroupMembershipMappingAssignUserEvent - | OidcGroupMembershipMappingRemoveUserEvent; + | OidcGroupMembershipMappingRemoveUserEvent + | CreateKmipClientEvent + | UpdateKmipClientEvent + | DeleteKmipClientEvent + | GetKmipClientEvent; diff --git a/backend/src/ee/services/kmip/kmip-client-dal.ts b/backend/src/ee/services/kmip/kmip-client-dal.ts new file mode 100644 index 000000000..e4f1d3408 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-client-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TKmipClientDALFactory = ReturnType; + +export const kmipClientDALFactory = (db: TDbClient) => { + const kmipClient = ormify(db, TableName.KmipClient); + + return kmipClient; +}; diff --git a/backend/src/ee/services/kmip/kmip-enum.ts b/backend/src/ee/services/kmip/kmip-enum.ts new file mode 100644 index 000000000..aca3421bb --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-enum.ts @@ -0,0 +1,6 @@ +export enum KmipPermission { + Create = "create", + Locate = "locate", + Check = "check", + Get = "get" +} diff --git a/backend/src/ee/services/kmip/kmip-service.ts b/backend/src/ee/services/kmip/kmip-service.ts new file mode 100644 index 000000000..59390b4cc --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-service.ts @@ -0,0 +1,127 @@ +import { ForbiddenError } from "@casl/ability"; + +import { ActionProjectType } from "@app/db/schemas"; + +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission"; +import { TKmipClientDALFactory } from "./kmip-client-dal"; +import { TCreateKmipClientDTO, TDeleteKmipClientDTO, TGetKmipClientDTO, TUpdateKmipClientDTO } from "./kmip-types"; + +type TKmipServiceFactoryDep = { + kmipClientDAL: TKmipClientDALFactory; + permissionService: Pick; +}; + +export type TKmipServiceFactory = ReturnType; + +export const kmipServiceFactory = ({ kmipClientDAL, permissionService }: TKmipServiceFactoryDep) => { + const createKmipClient = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + projectId, + name, + description, + permissions + }: TCreateKmipClientDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionKmipActions.CreateClients, + ProjectPermissionSub.Kmip + ); + + const kmipClient = await kmipClientDAL.create({ + projectId, + name, + description, + permissions + }); + + return kmipClient; + }; + + const updateKmipClient = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + name, + description, + permissions, + id + }: TUpdateKmipClientDTO) => { + const kmipClient = await kmipClientDAL.findById(id); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: kmipClient.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionKmipActions.UpdateClients, + ProjectPermissionSub.Kmip + ); + + const updatedKmipClient = await kmipClientDAL.updateById(id, { + name, + description, + permissions + }); + + return updatedKmipClient; + }; + + const deleteKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteKmipClientDTO) => { + const kmipClient = await kmipClientDAL.findById(id); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: kmipClient.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionKmipActions.DeleteClients, + ProjectPermissionSub.Kmip + ); + + const deletedKmipClient = await kmipClientDAL.deleteById(id); + + return deletedKmipClient; + }; + + const getKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetKmipClientDTO) => { + const kmipClient = await kmipClientDAL.findById(id); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: kmipClient.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip); + + return kmipClient; + }; + + return { createKmipClient, updateKmipClient, deleteKmipClient, getKmipClient }; +}; diff --git a/backend/src/ee/services/kmip/kmip-types.ts b/backend/src/ee/services/kmip/kmip-types.ts new file mode 100644 index 000000000..4b43abf14 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-types.ts @@ -0,0 +1,24 @@ +import { TProjectPermission } from "@app/lib/types"; + +import { KmipPermission } from "./kmip-enum"; + +export type TCreateKmipClientDTO = { + name: string; + description?: string; + permissions: KmipPermission[]; +} & TProjectPermission; + +export type TUpdateKmipClientDTO = { + id: string; + name?: string; + description?: string; + permissions?: KmipPermission[]; +} & Omit; + +export type TDeleteKmipClientDTO = { + id: string; +} & Omit; + +export type TGetKmipClientDTO = { + id: string; +} & Omit; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index e9ba49127..ef7ef34ae 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -44,6 +44,13 @@ export enum ProjectPermissionSecretSyncActions { RemoveSecrets = "remove-secrets" } +export enum ProjectPermissionKmipActions { + CreateClients = "create-clients", + UpdateClients = "update-clients", + DeleteClients = "delete-clients", + ReadClients = "read-clients" +} + export enum ProjectPermissionSub { Role = "role", Member = "member", @@ -75,7 +82,8 @@ export enum ProjectPermissionSub { PkiCollections = "pki-collections", Kms = "kms", Cmek = "cmek", - SecretSyncs = "secret-syncs" + SecretSyncs = "secret-syncs", + Kmip = "kmip" } export type SecretSubjectFields = { @@ -156,6 +164,7 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs] + | [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip] | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Project] @@ -575,6 +584,17 @@ const buildAdminPermissionRules = () => { ], ProjectPermissionSub.SecretSyncs ); + + can( + [ + ProjectPermissionKmipActions.CreateClients, + ProjectPermissionKmipActions.UpdateClients, + ProjectPermissionKmipActions.DeleteClients, + ProjectPermissionKmipActions.ReadClients + ], + ProjectPermissionSub.Kmip + ); + return rules; }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 3e5947956..eabbb8e0a 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -35,6 +35,8 @@ import { HsmModule } from "@app/ee/services/hsm/hsm-types"; import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal"; import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; +import { kmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-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"; import { ldapGroupMapDALFactory } from "@app/ee/services/ldap-config/ldap-group-map-dal"; @@ -380,6 +382,7 @@ export const registerRoutes = async ( const projectTemplateDAL = projectTemplateDALFactory(db); const resourceMetadataDAL = resourceMetadataDALFactory(db); + const kmipClientDAL = kmipClientDALFactory(db); const permissionService = permissionServiceFactory({ permissionDAL, @@ -1418,6 +1421,11 @@ export const registerRoutes = async ( keyStore }); + const kmipService = kmipServiceFactory({ + kmipClientDAL, + permissionService + }); + await superAdminService.initServerCfg(); // setup the communication with license key server @@ -1516,7 +1524,8 @@ export const registerRoutes = async ( projectTemplate: projectTemplateService, totp: totpService, appConnection: appConnectionService, - secretSync: secretSyncService + secretSync: secretSyncService, + kmip: kmipService }); const cronJobs: CronJob[] = []; From 64c2fba350b6443f7fc3338ea66d1af9960e1c66 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 4 Feb 2025 02:44:59 +0800 Subject: [PATCH 02/19] feat: added list support and overview page --- backend/src/ee/routes/v1/kmip-router.ts | 54 ++++++++++++++++ .../ee/services/audit-log/audit-log-types.ts | 13 +++- .../src/ee/services/kmip/kmip-client-dal.ts | 62 +++++++++++++++++-- backend/src/ee/services/kmip/kmip-service.ts | 32 +++++++++- backend/src/ee/services/kmip/kmip-types.ts | 14 ++++- .../ProjectPermissionContext/index.tsx | 1 + .../context/ProjectPermissionContext/types.ts | 14 ++++- frontend/src/context/index.tsx | 1 + .../layouts/ProjectLayout/ProjectLayout.tsx | 14 +++++ frontend/src/pages/kms/KmipPage/KmipPage.tsx | 31 ++++++++++ frontend/src/pages/kms/KmipPage/route.tsx | 9 +++ frontend/src/routeTree.gen.ts | 29 ++++++++- frontend/src/routes.ts | 1 + 13 files changed, 263 insertions(+), 12 deletions(-) create mode 100644 frontend/src/pages/kms/KmipPage/KmipPage.tsx create mode 100644 frontend/src/pages/kms/KmipPage/route.tsx diff --git a/backend/src/ee/routes/v1/kmip-router.ts b/backend/src/ee/routes/v1/kmip-router.ts index 66d3e7e28..4c7a39097 100644 --- a/backend/src/ee/routes/v1/kmip-router.ts +++ b/backend/src/ee/routes/v1/kmip-router.ts @@ -3,6 +3,8 @@ import { z } from "zod"; import { KmipClientsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { KmipPermission } from "@app/ee/services/kmip/kmip-enum"; +import { KmipClientOrderBy } from "@app/ee/services/kmip/kmip-types"; +import { OrderByDirection } from "@app/lib/types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -180,4 +182,56 @@ export const registerKmipRouter = async (server: FastifyZodProvider) => { }); } }); + + server.route({ + method: "GET", + url: "/clients", + config: { + rateLimit: readLimit + }, + schema: { + description: "List KMIP clients", + querystring: z.object({ + projectId: z.string(), + offset: z.coerce.number().min(0).optional().default(0), + limit: z.coerce.number().min(1).max(100).optional().default(100), + orderBy: z.nativeEnum(KmipClientOrderBy).optional().default(KmipClientOrderBy.Name), + orderDirection: z.nativeEnum(OrderByDirection).optional().default(OrderByDirection.ASC), + search: z.string().trim().optional() + }), + response: { + 200: z.object({ + kmipClients: KmipClientResponseSchema.array(), + totalCount: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + query: { projectId } + } = req; + + const { kmipClients, totalCount } = await server.services.kmip.listKmipClientsByProjectId({ + projectId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.GET_KMIP_CLIENTS, + metadata: { + ids: kmipClients.map((key) => key.id) + } + } + }); + + return { kmipClients, totalCount }; + } + }); }; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index fca2975be..422c9b1ed 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -257,7 +257,8 @@ export enum EventType { CREATE_KMIP_CLIENT = "create-kmip-client", UPDATE_KMIP_CLIENT = "update-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" } interface UserActorMetadata { @@ -2104,6 +2105,13 @@ interface GetKmipClientEvent { }; } +interface GetKmipClientsEvent { + type: EventType.GET_KMIP_CLIENTS; + metadata: { + ids: string[]; + }; +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -2298,4 +2306,5 @@ export type Event = | CreateKmipClientEvent | UpdateKmipClientEvent | DeleteKmipClientEvent - | GetKmipClientEvent; + | GetKmipClientEvent + | GetKmipClientsEvent; diff --git a/backend/src/ee/services/kmip/kmip-client-dal.ts b/backend/src/ee/services/kmip/kmip-client-dal.ts index e4f1d3408..25043d35c 100644 --- a/backend/src/ee/services/kmip/kmip-client-dal.ts +++ b/backend/src/ee/services/kmip/kmip-client-dal.ts @@ -1,11 +1,65 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { TableName, TKmipClients } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { OrderByDirection } from "@app/lib/types"; + +import { KmipClientOrderBy } from "./kmip-types"; export type TKmipClientDALFactory = ReturnType; export const kmipClientDALFactory = (db: TDbClient) => { - const kmipClient = ormify(db, TableName.KmipClient); + const kmipClientOrm = ormify(db, TableName.KmipClient); - return kmipClient; + const findByProjectId = async ( + { + projectId, + offset = 0, + limit, + orderBy = KmipClientOrderBy.Name, + orderDirection = OrderByDirection.ASC, + search + }: { + projectId: string; + offset?: number; + limit?: number; + orderBy?: KmipClientOrderBy; + orderDirection?: OrderByDirection; + search?: string; + }, + tx?: Knex + ) => { + try { + const query = (tx || db.replicaNode())(TableName.KmipClient) + .where("projectId", projectId) + .where((qb) => { + if (search) { + void qb.whereILike("name", `%${search}%`); + } + }) + .select< + (TKmipClients & { + total_count: number; + })[] + >(selectAllTableCols(TableName.KmipClient), db.raw(`count(*) OVER() as total_count`)) + .orderBy(orderBy, orderDirection); + + if (limit) { + void query.limit(limit).offset(offset); + } + + const data = await query; + + return { kmipClients: data, totalCount: Number(data?.[0]?.total_count ?? 0) }; + } catch (error) { + throw new DatabaseError({ error, name: "Find KMIP clients by project id" }); + } + }; + + return { + ...kmipClientOrm, + findByProjectId + }; }; diff --git a/backend/src/ee/services/kmip/kmip-service.ts b/backend/src/ee/services/kmip/kmip-service.ts index 59390b4cc..e1dcc185f 100644 --- a/backend/src/ee/services/kmip/kmip-service.ts +++ b/backend/src/ee/services/kmip/kmip-service.ts @@ -5,7 +5,13 @@ import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission"; import { TKmipClientDALFactory } from "./kmip-client-dal"; -import { TCreateKmipClientDTO, TDeleteKmipClientDTO, TGetKmipClientDTO, TUpdateKmipClientDTO } from "./kmip-types"; +import { + TCreateKmipClientDTO, + TDeleteKmipClientDTO, + TGetKmipClientDTO, + TListKmipClientsByProjectIdDTO, + TUpdateKmipClientDTO +} from "./kmip-types"; type TKmipServiceFactoryDep = { kmipClientDAL: TKmipClientDALFactory; @@ -123,5 +129,27 @@ export const kmipServiceFactory = ({ kmipClientDAL, permissionService }: TKmipSe return kmipClient; }; - return { createKmipClient, updateKmipClient, deleteKmipClient, getKmipClient }; + const listKmipClientsByProjectId = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + projectId, + ...rest + }: TListKmipClientsByProjectIdDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip); + + return kmipClientDAL.findByProjectId({ projectId, ...rest }); + }; + + return { createKmipClient, updateKmipClient, deleteKmipClient, getKmipClient, listKmipClientsByProjectId }; }; diff --git a/backend/src/ee/services/kmip/kmip-types.ts b/backend/src/ee/services/kmip/kmip-types.ts index 4b43abf14..c0eee29b6 100644 --- a/backend/src/ee/services/kmip/kmip-types.ts +++ b/backend/src/ee/services/kmip/kmip-types.ts @@ -1,4 +1,4 @@ -import { TProjectPermission } from "@app/lib/types"; +import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { KmipPermission } from "./kmip-enum"; @@ -22,3 +22,15 @@ export type TDeleteKmipClientDTO = { export type TGetKmipClientDTO = { id: string; } & Omit; + +export enum KmipClientOrderBy { + Name = "name" +} + +export type TListKmipClientsByProjectIdDTO = { + offset?: number; + limit?: number; + orderBy?: KmipClientOrderBy; + orderDirection?: OrderByDirection; + search?: string; +} & TProjectPermission; diff --git a/frontend/src/context/ProjectPermissionContext/index.tsx b/frontend/src/context/ProjectPermissionContext/index.tsx index 009bc9451..69bcd4f99 100644 --- a/frontend/src/context/ProjectPermissionContext/index.tsx +++ b/frontend/src/context/ProjectPermissionContext/index.tsx @@ -4,5 +4,6 @@ export { ProjectPermissionActions, ProjectPermissionCmekActions, ProjectPermissionDynamicSecretActions, + ProjectPermissionKmipActions, ProjectPermissionSub } from "./types"; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 279e69889..e05506dde 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -24,6 +24,13 @@ export enum ProjectPermissionCmekActions { Decrypt = "decrypt" } +export enum ProjectPermissionKmipActions { + CreateClients = "create-clients", + UpdateClients = "update-clients", + DeleteClients = "delete-clients", + ReadClients = "read-clients" +} + export enum ProjectPermissionSecretSyncActions { Read = "read", Create = "create", @@ -102,7 +109,8 @@ export enum ProjectPermissionSub { PkiCollections = "pki-collections", Kms = "kms", Cmek = "cmek", - SecretSyncs = "secret-syncs" + SecretSyncs = "secret-syncs", + Kmip = "kmip" } export type SecretSubjectFields = { @@ -190,5 +198,7 @@ export type ProjectPermissionSet = | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback] | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek] - | [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]; + | [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms] + | [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip]; + export type TProjectPermission = MongoAbility; diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx index 70d00dd74..fb9d8c385 100644 --- a/frontend/src/context/index.tsx +++ b/frontend/src/context/index.tsx @@ -10,6 +10,7 @@ export { ProjectPermissionActions, ProjectPermissionCmekActions, ProjectPermissionDynamicSecretActions, + ProjectPermissionKmipActions, ProjectPermissionSub, useProjectPermission } from "./ProjectPermissionContext"; diff --git a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx index d2d49822f..18fd2aa14 100644 --- a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx +++ b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx @@ -105,6 +105,20 @@ export const ProjectLayout = () => { )} )} + {isCmek && ( + + {({ isActive }) => ( + + KMIP + + )} + + )} {isSSH && ( { + const { t } = useTranslation(); + + return ( +
+ + {t("common.head-title", { title: "KMS" })} + +
+
+ + +
KMIP clients here
+
+
+
+
+ ); +}; diff --git a/frontend/src/pages/kms/KmipPage/route.tsx b/frontend/src/pages/kms/KmipPage/route.tsx new file mode 100644 index 000000000..02a8a000c --- /dev/null +++ b/frontend/src/pages/kms/KmipPage/route.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { KmipPage } from "./KmipPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip" +)({ + component: KmipPage +}); diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index cc89058c3..c77152caf 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -82,6 +82,7 @@ import { Route as secretManagerSecretApprovalsPageRouteImport } from './pages/se import { Route as secretManagerIPAllowlistPageRouteImport } from './pages/secret-manager/IPAllowlistPage/route' import { Route as kmsSettingsPageRouteImport } from './pages/kms/SettingsPage/route' import { Route as kmsOverviewPageRouteImport } from './pages/kms/OverviewPage/route' +import { Route as kmsKmipPageRouteImport } from './pages/kms/KmipPage/route' import { Route as certManagerSettingsPageRouteImport } from './pages/cert-manager/SettingsPage/route' import { Route as certManagerCertificatesPageRouteImport } from './pages/cert-manager/CertificatesPage/route' import { Route as projectRoleDetailsBySlugPageRouteSshImport } from './pages/project/RoleDetailsBySlugPage/route-ssh' @@ -782,6 +783,12 @@ const kmsOverviewPageRouteRoute = kmsOverviewPageRouteImport.update({ getParentRoute: () => kmsLayoutRoute, } as any) +const kmsKmipPageRouteRoute = kmsKmipPageRouteImport.update({ + id: '/kmip', + path: '/kmip', + getParentRoute: () => kmsLayoutRoute, +} as any) + const certManagerSettingsPageRouteRoute = certManagerSettingsPageRouteImport.update({ id: '/settings', @@ -1964,6 +1971,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof certManagerSettingsPageRouteImport parentRoute: typeof certManagerLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip': { + id: '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip' + path: '/kmip' + fullPath: '/kms/$projectId/kmip' + preLoaderRoute: typeof kmsKmipPageRouteImport + parentRoute: typeof kmsLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview': { id: '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview' path: '/overview' @@ -2937,6 +2951,7 @@ const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren ) interface kmsLayoutRouteChildren { + kmsKmipPageRouteRoute: typeof kmsKmipPageRouteRoute kmsOverviewPageRouteRoute: typeof kmsOverviewPageRouteRoute kmsSettingsPageRouteRoute: typeof kmsSettingsPageRouteRoute projectAccessControlPageRouteKmsRoute: typeof projectAccessControlPageRouteKmsRoute @@ -2946,6 +2961,7 @@ interface kmsLayoutRouteChildren { } const kmsLayoutRouteChildren: kmsLayoutRouteChildren = { + kmsKmipPageRouteRoute: kmsKmipPageRouteRoute, kmsOverviewPageRouteRoute: kmsOverviewPageRouteRoute, kmsSettingsPageRouteRoute: kmsSettingsPageRouteRoute, projectAccessControlPageRouteKmsRoute: projectAccessControlPageRouteKmsRoute, @@ -3551,6 +3567,7 @@ export interface FileRoutesByFullPath { '/organization/ssh/overview': typeof organizationSshOverviewPageRouteRoute '/cert-manager/$projectId/overview': typeof certManagerCertificatesPageRouteRoute '/cert-manager/$projectId/settings': typeof certManagerSettingsPageRouteRoute + '/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute '/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute '/kms/$projectId/settings': typeof kmsSettingsPageRouteRoute '/secret-manager/$projectId/allowlist': typeof secretManagerIPAllowlistPageRouteRoute @@ -3718,6 +3735,7 @@ export interface FileRoutesByTo { '/organization/ssh/overview': typeof organizationSshOverviewPageRouteRoute '/cert-manager/$projectId/overview': typeof certManagerCertificatesPageRouteRoute '/cert-manager/$projectId/settings': typeof certManagerSettingsPageRouteRoute + '/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute '/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute '/kms/$projectId/settings': typeof kmsSettingsPageRouteRoute '/secret-manager/$projectId/allowlist': typeof secretManagerIPAllowlistPageRouteRoute @@ -3898,6 +3916,7 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout': typeof sshLayoutRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview': typeof certManagerCertificatesPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings': typeof certManagerSettingsPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip': typeof kmsKmipPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview': typeof kmsOverviewPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/settings': typeof kmsSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/allowlist': typeof secretManagerIPAllowlistPageRouteRoute @@ -4071,6 +4090,7 @@ export interface FileRouteTypes { | '/organization/ssh/overview' | '/cert-manager/$projectId/overview' | '/cert-manager/$projectId/settings' + | '/kms/$projectId/kmip' | '/kms/$projectId/overview' | '/kms/$projectId/settings' | '/secret-manager/$projectId/allowlist' @@ -4237,6 +4257,7 @@ export interface FileRouteTypes { | '/organization/ssh/overview' | '/cert-manager/$projectId/overview' | '/cert-manager/$projectId/settings' + | '/kms/$projectId/kmip' | '/kms/$projectId/overview' | '/kms/$projectId/settings' | '/secret-manager/$projectId/allowlist' @@ -4415,6 +4436,7 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings' + | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/settings' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/allowlist' @@ -4891,6 +4913,7 @@ export const routeTree = rootRoute "filePath": "kms/layout.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/kms/$projectId", "children": [ + "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip", "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview", "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/settings", "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/access-management", @@ -4937,6 +4960,10 @@ export const routeTree = rootRoute "filePath": "cert-manager/SettingsPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" }, + "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip": { + "filePath": "kms/KmipPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout" + }, "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview": { "filePath": "kms/OverviewPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout" @@ -5491,4 +5518,4 @@ export const routeTree = rootRoute } } } -ROUTE_MANIFEST_END */ \ No newline at end of file +ROUTE_MANIFEST_END */ diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 5a7f3645b..b81b13165 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -289,6 +289,7 @@ const certManagerRoutes = route("/cert-manager/$projectId", [ const kmsRoutes = route("/kms/$projectId", [ layout("kms-layout", "kms/layout.tsx", [ route("/overview", "kms/OverviewPage/route.tsx"), + route("/kmip", "kms/KmipPage/route.tsx"), route("/settings", "kms/SettingsPage/route.tsx"), route("/access-management", "project/AccessControlPage/route-kms.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-kms.tsx"), From dd9a7755bc951586c746717df5ad67bd35f6061b Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 4 Feb 2025 18:49:48 +0800 Subject: [PATCH 03/19] feat: completed KMIP client overview --- backend/src/ee/routes/v1/kmip-router.ts | 10 +- frontend/src/hooks/api/kmip/index.ts | 2 + frontend/src/hooks/api/kmip/mutation.ts | 58 ++++ frontend/src/hooks/api/kmip/queries.tsx | 52 ++++ frontend/src/hooks/api/kmip/types.ts | 46 +++ frontend/src/pages/kms/KmipPage/KmipPage.tsx | 15 +- .../components/DeleteKmipClientModal.tsx | 53 ++++ .../KmipPage/components/KmipClientModal.tsx | 184 ++++++++++++ .../KmipPage/components/KmipClientTable.tsx | 280 ++++++++++++++++++ 9 files changed, 688 insertions(+), 12 deletions(-) create mode 100644 frontend/src/hooks/api/kmip/index.ts create mode 100644 frontend/src/hooks/api/kmip/mutation.ts create mode 100644 frontend/src/hooks/api/kmip/queries.tsx create mode 100644 frontend/src/hooks/api/kmip/types.ts create mode 100644 frontend/src/pages/kms/KmipPage/components/DeleteKmipClientModal.tsx create mode 100644 frontend/src/pages/kms/KmipPage/components/KmipClientModal.tsx create mode 100644 frontend/src/pages/kms/KmipPage/components/KmipClientTable.tsx diff --git a/backend/src/ee/routes/v1/kmip-router.ts b/backend/src/ee/routes/v1/kmip-router.ts index 4c7a39097..3bab6d7c0 100644 --- a/backend/src/ee/routes/v1/kmip-router.ts +++ b/backend/src/ee/routes/v1/kmip-router.ts @@ -208,21 +208,17 @@ export const registerKmipRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const { - query: { projectId } - } = req; - const { kmipClients, totalCount } = await server.services.kmip.listKmipClientsByProjectId({ - projectId, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId + actorOrgId: req.permission.orgId, + ...req.query }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId, + projectId: req.query.projectId, event: { type: EventType.GET_KMIP_CLIENTS, metadata: { diff --git a/frontend/src/hooks/api/kmip/index.ts b/frontend/src/hooks/api/kmip/index.ts new file mode 100644 index 000000000..f4e57d7cc --- /dev/null +++ b/frontend/src/hooks/api/kmip/index.ts @@ -0,0 +1,2 @@ +export * from "./mutation"; +export * from "./queries"; diff --git a/frontend/src/hooks/api/kmip/mutation.ts b/frontend/src/hooks/api/kmip/mutation.ts new file mode 100644 index 000000000..736c4c5cb --- /dev/null +++ b/frontend/src/hooks/api/kmip/mutation.ts @@ -0,0 +1,58 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { kmipKeys } from "./queries"; +import { TCreateKmipClient, TDeleteKmipClient, TUpdateKmipClient } from "./types"; + +export const useCreateKmipClient = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (payload: TCreateKmipClient) => { + const { data } = await apiRequest.post("/api/v1/kmip/clients", payload); + + return data; + }, + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ + queryKey: kmipKeys.getKmipClientsByProjectId({ projectId }) + }); + } + }); +}; + +export const useUpdateKmipClient = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ id, name, description, permissions }: TUpdateKmipClient) => { + const { data } = await apiRequest.patch(`/api/v1/kmip/clients/${id}`, { + name, + description, + permissions + }); + + return data; + }, + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ + queryKey: kmipKeys.getKmipClientsByProjectId({ projectId }) + }); + } + }); +}; + +export const useDeleteKmipClients = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ id }: TDeleteKmipClient) => { + const { data } = await apiRequest.delete(`/api/v1/kmip/clients/${id}`); + + return data; + }, + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ + queryKey: kmipKeys.getKmipClientsByProjectId({ projectId }) + }); + } + }); +}; diff --git a/frontend/src/hooks/api/kmip/queries.tsx b/frontend/src/hooks/api/kmip/queries.tsx new file mode 100644 index 000000000..2816e6b75 --- /dev/null +++ b/frontend/src/hooks/api/kmip/queries.tsx @@ -0,0 +1,52 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { OrderByDirection } from "../generic/types"; +import { KmipClientOrderBy, TListProjectKmipClientsDTO, TProjectKmipClientList } from "./types"; + +export const kmipKeys = { + getKmipClientsByProjectId: ({ projectId, ...filters }: TListProjectKmipClientsDTO) => + [projectId, filters] as const +}; + +export const useGetKmipClientsByProjectId = ( + { + projectId, + offset = 0, + limit = 100, + orderBy = KmipClientOrderBy.Name, + orderDirection = OrderByDirection.ASC, + search = "" + }: TListProjectKmipClientsDTO, + options?: Omit< + UseQueryOptions< + TProjectKmipClientList, + unknown, + TProjectKmipClientList, + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: kmipKeys.getKmipClientsByProjectId({ + projectId, + offset, + limit, + orderBy, + orderDirection, + search + }), + queryFn: async () => { + const { data } = await apiRequest.get("/api/v1/kmip/clients", { + params: { projectId, offset, limit, search, orderBy, orderDirection } + }); + + return data; + }, + enabled: Boolean(projectId) && (options?.enabled ?? true), + placeholderData: (previousData) => previousData, + ...options + }); +}; diff --git a/frontend/src/hooks/api/kmip/types.ts b/frontend/src/hooks/api/kmip/types.ts new file mode 100644 index 000000000..fd2450174 --- /dev/null +++ b/frontend/src/hooks/api/kmip/types.ts @@ -0,0 +1,46 @@ +import { OrderByDirection } from "../generic/types"; + +export enum KmipPermission { + Create = "create", + Locate = "locate", + Check = "check", + Get = "get" +} + +export type TKmipClient = { + id: string; + name: string; + description?: string; + permissions: KmipPermission[]; + projectId: string; +}; + +type ProjectRef = { projectId: string }; +type KeyRef = { id: string }; + +export type TCreateKmipClient = Pick & + ProjectRef; + +export type TUpdateKmipClient = KeyRef & + Partial> & + ProjectRef; + +export type TProjectKmipClientList = { + kmipClients: TKmipClient[]; + totalCount: number; +}; + +export type TDeleteKmipClient = KeyRef & ProjectRef; + +export type TListProjectKmipClientsDTO = { + projectId: string; + offset?: number; + limit?: number; + orderBy?: KmipClientOrderBy; + orderDirection?: OrderByDirection; + search?: string; +}; + +export enum KmipClientOrderBy { + Name = "name" +} diff --git a/frontend/src/pages/kms/KmipPage/KmipPage.tsx b/frontend/src/pages/kms/KmipPage/KmipPage.tsx index 7fa9fca75..6297c5eef 100644 --- a/frontend/src/pages/kms/KmipPage/KmipPage.tsx +++ b/frontend/src/pages/kms/KmipPage/KmipPage.tsx @@ -3,7 +3,9 @@ import { useTranslation } from "react-i18next"; import { ProjectPermissionCan } from "@app/components/permissions"; import { PageHeader } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { ProjectPermissionKmipActions, ProjectPermissionSub } from "@app/context"; + +import { KmipClientTable } from "./components/KmipClientTable"; export const KmipPage = () => { const { t } = useTranslation(); @@ -15,14 +17,17 @@ export const KmipPage = () => {
- + -
KMIP clients here
+
diff --git a/frontend/src/pages/kms/KmipPage/components/DeleteKmipClientModal.tsx b/frontend/src/pages/kms/KmipPage/components/DeleteKmipClientModal.tsx new file mode 100644 index 000000000..d9f833543 --- /dev/null +++ b/frontend/src/pages/kms/KmipPage/components/DeleteKmipClientModal.tsx @@ -0,0 +1,53 @@ +import { createNotification } from "@app/components/notifications"; +import { DeleteActionModal } from "@app/components/v2"; +import { useDeleteKmipClients } from "@app/hooks/api/kmip"; +import { TKmipClient } from "@app/hooks/api/kmip/types"; + +type Props = { + kmipClient: TKmipClient; + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +export const DeleteKmipClientModal = ({ isOpen, onOpenChange, kmipClient }: Props) => { + const deleteKmipClients = useDeleteKmipClients(); + + if (!kmipClient) return null; + + const { id, projectId, name } = kmipClient; + + const handleDeleteKmipClient = async () => { + try { + await deleteKmipClients.mutateAsync({ + id, + projectId + }); + + createNotification({ + text: "KMIP client successfully deleted", + type: "success" + }); + + onOpenChange(false); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to delete KMIP client"; + + createNotification({ + text, + type: "error" + }); + } + }; + + return ( + + ); +}; diff --git a/frontend/src/pages/kms/KmipPage/components/KmipClientModal.tsx b/frontend/src/pages/kms/KmipPage/components/KmipClientModal.tsx new file mode 100644 index 000000000..74a0143ce --- /dev/null +++ b/frontend/src/pages/kms/KmipPage/components/KmipClientModal.tsx @@ -0,0 +1,184 @@ +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, + Checkbox, + FormControl, + Input, + Modal, + ModalClose, + ModalContent, + TextArea +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useCreateKmipClient, useUpdateKmipClient } from "@app/hooks/api/kmip"; +import { KmipPermission, TKmipClient } from "@app/hooks/api/kmip/types"; + +const KMIP_PERMISSIONS_OPTIONS = [ + { value: KmipPermission.Check, label: "Check" }, + { value: KmipPermission.Create, label: "Create" }, + { value: KmipPermission.Get, label: "Get" }, + { value: KmipPermission.Locate, label: "Locate" } +] as const; + +const formSchema = z.object({ + name: z.string().trim().min(1), + description: z.string().max(500).optional(), + permissions: z.object({ + [KmipPermission.Check]: z.boolean().optional(), + [KmipPermission.Create]: z.boolean().optional(), + [KmipPermission.Get]: z.boolean().optional(), + [KmipPermission.Locate]: z.boolean().optional() + }) +}); + +export type FormData = z.infer; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + kmipClient?: TKmipClient | null; +}; + +type FormProps = Pick & { + onComplete: () => void; +}; + +const KmipClientForm = ({ onComplete, kmipClient }: FormProps) => { + const createKmipClient = useCreateKmipClient(); + const updateKmipClient = useUpdateKmipClient(); + const { currentWorkspace } = useWorkspace(); + const projectId = currentWorkspace.id; + const isUpdate = !!kmipClient; + + const { + control, + handleSubmit, + register, + formState: { isSubmitting, errors } + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: kmipClient?.name, + description: kmipClient?.description, + permissions: Object.fromEntries((kmipClient?.permissions || []).map((name) => [name, true])) + } + }); + + const handleKmipClientSubmit = async ({ permissions, name, description }: FormData) => { + const mutation = isUpdate + ? updateKmipClient.mutateAsync({ + id: kmipClient.id, + projectId, + name, + description, + permissions: Object.entries(permissions) + .filter(([, value]) => value) + .map(([key]) => key as KmipPermission) + }) + : createKmipClient.mutateAsync({ + projectId, + name, + description, + permissions: Object.entries(permissions) + .filter(([, value]) => value) + .map(([key]) => key as KmipPermission) + }); + + try { + await mutation; + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "added"} KMIP client`, + type: "success" + }); + onComplete(); + } catch (err) { + console.error(err); + createNotification({ + text: `Failed to ${isUpdate ? "update" : "add"} KMIP client`, + type: "error" + }); + } + }; + + return ( +
+ + + + +