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"),