feat: added list support and overview page

This commit is contained in:
Sheen Capadngan
2025-02-04 02:44:59 +08:00
parent c7f80f7d9e
commit 64c2fba350
13 changed files with 263 additions and 12 deletions

View File

@@ -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 };
}
});
};

View File

@@ -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;

View File

@@ -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<typeof kmipClientDALFactory>;
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
};
};

View File

@@ -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 };
};

View File

@@ -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<TProjectPermission, "projectId">;
export enum KmipClientOrderBy {
Name = "name"
}
export type TListKmipClientsByProjectIdDTO = {
offset?: number;
limit?: number;
orderBy?: KmipClientOrderBy;
orderDirection?: OrderByDirection;
search?: string;
} & TProjectPermission;

View File

@@ -4,5 +4,6 @@ export {
ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionKmipActions,
ProjectPermissionSub
} from "./types";

View File

@@ -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<ProjectPermissionSet>;

View File

@@ -10,6 +10,7 @@ export {
ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionKmipActions,
ProjectPermissionSub,
useProjectPermission
} from "./ProjectPermissionContext";

View File

@@ -105,6 +105,20 @@ export const ProjectLayout = () => {
)}
</Link>
)}
{isCmek && (
<Link
to={`/${ProjectType.KMS}/$projectId/kmip` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
KMIP
</MenuItem>
)}
</Link>
)}
{isSSH && (
<Link
to={`/${ProjectType.SSH}/$projectId/overview` as const}

View File

@@ -0,0 +1,31 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
export const KmipPage = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "KMS" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title="KMIP clients" description="Manage KMIP clients" />
<ProjectPermissionCan
passThrough={false}
renderGuardBanner
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.Cmek}
>
<div>KMIP clients here</div>
</ProjectPermissionCan>
</div>
</div>
</div>
);
};

View File

@@ -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
});

View File

@@ -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 */
ROUTE_MANIFEST_END */

View File

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