From 1e5a9a602046a1ec4fd1b3e19285aa5e3afde57d Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Thu, 30 Jan 2025 03:36:46 +0800 Subject: [PATCH 01/31] doc: added section for egress ips --- docs/documentation/setup/networking.mdx | 36 +++++++++++++++++++++++++ docs/mint.json | 4 +++ 2 files changed, 40 insertions(+) create mode 100644 docs/documentation/setup/networking.mdx diff --git a/docs/documentation/setup/networking.mdx b/docs/documentation/setup/networking.mdx new file mode 100644 index 000000000..931127894 --- /dev/null +++ b/docs/documentation/setup/networking.mdx @@ -0,0 +1,36 @@ +--- +title: "Networking" +sidebarTitle: "Networking" +description: "Network configuration details for Infisical Cloud" +--- + +## Overview + +When integrating your infrastructure with Infisical Cloud, you may need to configure network access controls. This page provides the IP addresses that Infisical uses to communicate with your services. + +## Egress IP Addresses + +Infisical Cloud operates from two regions: US and EU. Depending on your region and security requirements, you may need to whitelist specific IP addresses to allow Infisical to communicate with your services. + +### US Region + +If you need Infisical US to reach your network, whitelist these IP addresses: + +- `3.213.63.16` +- `54.164.68.7` + +### EU Region + +If you need Infisical EU to reach your network, whitelist these IP addresses: + +- `3.77.89.19` +- `3.125.209.189` + +## Common Use Cases + +You might need to configure these IP addresses if you operate in a restricted network with the following Infisical features: + +- Secret rotation - When Infisical needs to send requests to your systems to automatically rotate credentials +- Dynamic secrets - When Infisical generates and manages temporary credentials for your cloud services +- Secret integrations - When syncing secrets with third-party services like Azure Key Vault +- Native authentication with machine identities - When using methods like Kubernetes authentication diff --git a/docs/mint.json b/docs/mint.json index 31c5d7b14..42f30c784 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -85,6 +85,10 @@ "documentation/guides/microsoft-power-apps", "documentation/guides/organization-structure" ] + }, + { + "group": "Setup", + "pages": ["documentation/setup/networking"] } ] }, From ecf2cb6e513abd50ddce44cc1475564ecebe48c7 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Thu, 30 Jan 2025 13:09:29 +0800 Subject: [PATCH 02/31] misc: made improvements to wording --- docs/documentation/setup/networking.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/documentation/setup/networking.mdx b/docs/documentation/setup/networking.mdx index 931127894..4a666b73c 100644 --- a/docs/documentation/setup/networking.mdx +++ b/docs/documentation/setup/networking.mdx @@ -10,25 +10,25 @@ When integrating your infrastructure with Infisical Cloud, you may need to confi ## Egress IP Addresses -Infisical Cloud operates from two regions: US and EU. Depending on your region and security requirements, you may need to whitelist specific IP addresses to allow Infisical to communicate with your services. +Infisical Cloud operates from two regions: US and EU. If your infrastructure has strict network policies, you may need to allow traffic from Infisical by adding the following IP addresses to your ingress rules. These are the egress IPs Infisical uses when making outbound requests to your services. ### US Region -If you need Infisical US to reach your network, whitelist these IP addresses: +To allow connections from Infisical US, add these IP addresses to your ingress rules: - `3.213.63.16` - `54.164.68.7` ### EU Region -If you need Infisical EU to reach your network, whitelist these IP addresses: +To allow connections from Infisical EU, add these IP addresses to your ingress rules: - `3.77.89.19` - `3.125.209.189` ## Common Use Cases -You might need to configure these IP addresses if you operate in a restricted network with the following Infisical features: +You may need to allow Infisical’s egress IPs if your services require inbound connections for: - Secret rotation - When Infisical needs to send requests to your systems to automatically rotate credentials - Dynamic secrets - When Infisical generates and manages temporary credentials for your cloud services From c7f80f7d9e1e7fd9d1d3f5fa9688a5f2cabe5722 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 4 Feb 2025 01:34:30 +0800 Subject: [PATCH 03/31] 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 04/31] 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 05/31] 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 ( +
+ + + + +