From 433c16c732bde8e8022d72fdcd9937c2e9816e57 Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 7 Nov 2025 05:44:38 -0500 Subject: [PATCH 01/15] pam: accounts table server-side filter, search, pagination --- .../pam-account-routers/pam-account-router.ts | 42 ++- .../services/pam-account/pam-account-dal.ts | 114 ++++++-- .../pam-account/pam-account-service.ts | 115 ++++++-- .../services/pam-account/pam-account-types.ts | 21 ++ .../ee/services/pam-folder/pam-folder-dal.ts | 99 ++++++- frontend/src/hooks/api/pam/enums.ts | 9 + frontend/src/hooks/api/pam/mutations.tsx | 12 +- frontend/src/hooks/api/pam/queries.tsx | 32 ++- frontend/src/hooks/api/pam/types/index.ts | 14 +- .../components/AccountViewToggle.tsx | 18 +- .../components/PamAccountsSection.tsx | 16 +- .../components/PamAccountsTable.tsx | 263 ++++++++---------- .../src/pages/pam/PamAccountsPage/route.tsx | 5 +- 13 files changed, 514 insertions(+), 246 deletions(-) diff --git a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts index 286e0896f..626daa898 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts @@ -2,11 +2,14 @@ import { z } from "zod"; import { PamFoldersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { PamAccountOrderBy, PamAccountView } from "@app/ee/services/pam-account/pam-account-types"; import { SanitizedMySQLAccountWithResourceSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas"; import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; import { BadRequestError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; import { ms } from "@app/lib/ms"; +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"; @@ -26,33 +29,58 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { schema: { description: "List PAM accounts", querystring: z.object({ - projectId: z.string().uuid() + projectId: z.string().uuid(), + accountPath: z.string().trim().default("/").transform(removeTrailingSlash), + accountView: z.nativeEnum(PamAccountView).default(PamAccountView.Flat), + offset: z.coerce.number().min(0).default(0), + limit: z.coerce.number().min(1).max(100).default(100), + orderBy: z.nativeEnum(PamAccountOrderBy).default(PamAccountOrderBy.Name), + orderDirection: z.nativeEnum(OrderByDirection).default(OrderByDirection.ASC), + search: z.string().trim().optional() }), response: { 200: z.object({ accounts: SanitizedAccountSchema.array(), - folders: PamFoldersSchema.array() + folders: PamFoldersSchema.array(), + totalCount: z.number().default(0), + folderId: z.string().optional(), + folderPaths: z.record(z.string(), z.string()) }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const response = await server.services.pamAccount.list(req.query.projectId, req.permission); + const { projectId, accountPath, accountView, limit, offset, search, orderBy, orderDirection } = req.query; + + const { accounts, folders, totalCount, folderId, folderPaths } = await server.services.pamAccount.list({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId, + accountPath, + accountView, + limit, + offset, + search, + orderBy, + orderDirection + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: req.query.projectId, + projectId, event: { type: EventType.PAM_ACCOUNT_LIST, metadata: { - accountCount: response.accounts.length, - folderCount: response.folders.length + accountCount: accounts.length, + folderCount: folders.length } } }); - return response; + return { accounts, folders, totalCount, folderId, folderPaths }; } }); diff --git a/backend/src/ee/services/pam-account/pam-account-dal.ts b/backend/src/ee/services/pam-account/pam-account-dal.ts index 6ef7df76e..562567172 100644 --- a/backend/src/ee/services/pam-account/pam-account-dal.ts +++ b/backend/src/ee/services/pam-account/pam-account-dal.ts @@ -1,46 +1,100 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TPamAccounts } from "@app/db/schemas"; -import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex"; +import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { OrderByDirection } from "@app/lib/types"; + +import { PamAccountOrderBy, PamAccountView } from "./pam-account-types"; export type TPamAccountDALFactory = ReturnType; -type PamAccountFindFilter = Parameters>[0]; - export const pamAccountDALFactory = (db: TDbClient) => { const orm = ormify(db, TableName.PamAccount); - const findWithResourceDetails = async (filter: PamAccountFindFilter, tx?: Knex) => { - const query = (tx || db.replicaNode())(TableName.PamAccount) - .leftJoin(TableName.PamResource, `${TableName.PamAccount}.resourceId`, `${TableName.PamResource}.id`) - .select(selectAllTableCols(TableName.PamAccount)) - .select( + const findByProjectIdWithResourceDetails = async ( + { + projectId, + folderId, + accountView = PamAccountView.Nested, + search, + limit, + offset = 0, + orderBy = PamAccountOrderBy.Name, + orderDirection = OrderByDirection.ASC + }: { + projectId: string; + folderId?: string | null; + accountView?: PamAccountView; + search?: string; + limit?: number; + offset?: number; + orderBy?: PamAccountOrderBy; + orderDirection?: OrderByDirection; + }, + tx?: Knex + ) => { + try { + const dbInstance = tx || db.replicaNode(); + const query = dbInstance(TableName.PamAccount) + .leftJoin(TableName.PamResource, `${TableName.PamAccount}.resourceId`, `${TableName.PamResource}.id`) + .where(`${TableName.PamAccount}.projectId`, projectId); + + if (accountView === PamAccountView.Nested) { + if (folderId) { + void query.where(`${TableName.PamAccount}.folderId`, folderId); + } else { + void query.whereNull(`${TableName.PamAccount}.folderId`); + } + } + + if (search) { + void query.where((q) => { + void q + .whereILike(`${TableName.PamAccount}.name`, `%${search}%`) + .orWhereILike(`${TableName.PamResource}.name`, `%${search}%`) + .orWhereILike(`${TableName.PamAccount}.description`, `%${search}%`); + }); + } + + const countQuery = query.clone().count("*", { as: "count" }).first(); + + void query.select(selectAllTableCols(TableName.PamAccount)).select( // resource db.ref("name").withSchema(TableName.PamResource).as("resourceName"), db.ref("resourceType").withSchema(TableName.PamResource), db.ref("encryptedRotationAccountCredentials").withSchema(TableName.PamResource) ); - if (filter) { - /* eslint-disable @typescript-eslint/no-misused-promises */ - void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.PamAccount, filter))); + const direction = orderDirection === OrderByDirection.ASC ? "ASC" : "DESC"; + + void query.orderByRaw(`${TableName.PamAccount}.?? COLLATE "en-x-icu" ${direction}`, [orderBy]); + + if (typeof limit === "number") { + void query.limit(limit).offset(offset); + } + + const [results, countResult] = await Promise.all([query, countQuery]); + const totalCount = Number(countResult?.count || 0); + + const accounts = results.map( + // @ts-expect-error resourceName, resourceType, encryptedRotationAccountCredentials are from joined table + ({ resourceId, resourceName, resourceType, encryptedRotationAccountCredentials, ...account }) => ({ + ...account, + resourceId, + resource: { + id: resourceId, + name: resourceName as string, + resourceType, + encryptedRotationAccountCredentials + } + }) + ); + return { accounts, totalCount }; + } catch (error) { + throw new DatabaseError({ error, name: "Find PAM accounts with resource details" }); } - - const accounts = await query; - - return accounts.map( - ({ resourceId, resourceName, resourceType, encryptedRotationAccountCredentials, ...account }) => ({ - ...account, - resourceId, - resource: { - id: resourceId, - name: resourceName, - resourceType, - encryptedRotationAccountCredentials - } - }) - ); }; const findAccountsDueForRotation = async (tx?: Knex) => { @@ -59,5 +113,9 @@ export const pamAccountDALFactory = (db: TDbClient) => { return accounts; }; - return { ...orm, findWithResourceDetails, findAccountsDueForRotation }; + return { + ...orm, + findByProjectIdWithResourceDetails, + findAccountsDueForRotation + }; }; diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts index 2f66d28d7..bfc6c7e7f 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ActionProjectType, OrganizationActionScope, TPamAccounts, TPamResources } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope, TPamAccounts, TPamFolders, TPamResources } from "@app/db/schemas"; import { PAM_RESOURCE_FACTORY_MAP } from "@app/ee/services/pam-resource/pam-resource-factory"; import { decryptResource, decryptResourceConnectionDetails } from "@app/ee/services/pam-resource/pam-resource-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -32,7 +32,13 @@ import { PamSessionStatus } from "../pam-session/pam-session-enums"; import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPamAccountDALFactory } from "./pam-account-dal"; import { decryptAccount, decryptAccountCredentials, encryptAccountCredentials } from "./pam-account-fns"; -import { TAccessAccountDTO, TCreateAccountDTO, TUpdateAccountDTO } from "./pam-account-types"; +import { + PamAccountView, + TAccessAccountDTO, + TCreateAccountDTO, + TListAccountsDTO, + TUpdateAccountDTO +} from "./pam-account-types"; type TPamAccountServiceFactoryDep = { pamResourceDAL: TPamResourceDALFactory; @@ -334,21 +340,86 @@ export const pamAccountServiceFactory = ({ }; }; - const list = async (projectId: string, actor: OrgServiceActor) => { + const list = async ({ + projectId, + accountPath, + accountView, + actor, + actorId, + actorAuthMethod, + actorOrgId, + ...params + }: TListAccountsDTO) => { const { permission } = await permissionService.getProjectPermission({ - actor: actor.type, - actorAuthMethod: actor.authMethod, - actorId: actor.id, - actorOrgId: actor.orgId, + actor, + actorId, projectId, + actorAuthMethod, + actorOrgId, actionProjectType: ActionProjectType.PAM }); - const accountsWithResourceDetails = await pamAccountDAL.findWithResourceDetails({ projectId }); + const limit = params.limit || 20; + const offset = params.offset || 0; const canReadFolders = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.PamFolders); - const folders = canReadFolders ? await pamFolderDAL.find({ projectId }) : []; + const folder = accountPath === "/" ? null : await pamFolderDAL.findByPath(projectId, accountPath); + if (accountPath !== "/" && !folder) { + return { accounts: [], folders: [], totalCount: 0, folderPaths: {} }; + } + const folderId = folder?.id; + + let totalFolderCount = 0; + if (canReadFolders && accountView === PamAccountView.Nested) { + const { totalCount } = await pamFolderDAL.findByProjectId({ + projectId, + parentId: folderId + }); + totalFolderCount = totalCount; + } + const { totalCount: totalAccountCount } = await pamAccountDAL.findByProjectIdWithResourceDetails({ + projectId, + folderId, + accountView + }); + + const totalCount = totalFolderCount + totalAccountCount; + + let folders: TPamFolders[] = []; + if (canReadFolders && accountView === PamAccountView.Nested && offset < totalFolderCount) { + const folderLimit = Math.min(limit, totalFolderCount - offset); + const { folders: foldersResp } = await pamFolderDAL.findByProjectId({ + projectId, + parentId: folderId, + limit: folderLimit, + offset, + search: params.search, + orderBy: params.orderBy, + orderDirection: params.orderDirection + }); + + folders = foldersResp; + } + + let accountsWithResourceDetails: Awaited< + ReturnType + >["accounts"] = []; + const accountsToFetch = limit - folders.length; + if (accountsToFetch > 0) { + const accountOffset = Math.max(0, offset - totalFolderCount); + const { accounts: accountsResp } = await pamAccountDAL.findByProjectIdWithResourceDetails({ + projectId, + folderId, + accountView, + offset: accountOffset, + limit: accountsToFetch, + search: params.search, + orderBy: params.orderBy, + orderDirection: params.orderDirection + }); + accountsWithResourceDetails = accountsResp; + } const decryptedAndPermittedAccounts: Array< TPamAccounts & { @@ -359,12 +430,6 @@ export const pamAccountServiceFactory = ({ > = []; for await (const account of accountsWithResourceDetails) { - const accountPath = await getFullPamFolderPath({ - pamFolderDAL, - folderId: account.folderId, - projectId: account.projectId - }); - // Check permission for each individual account if ( permission.can( @@ -391,9 +456,27 @@ export const pamAccountServiceFactory = ({ } } + const folderPaths: Record = {}; + const accountFolderIds = [ + ...new Set(decryptedAndPermittedAccounts.flatMap((a) => (a.folderId ? [a.folderId] : []))) + ]; + + await Promise.all( + accountFolderIds.map(async (fId) => { + folderPaths[fId] = await getFullPamFolderPath({ + pamFolderDAL, + folderId: fId, + projectId + }); + }) + ); + return { accounts: decryptedAndPermittedAccounts, - folders + folders, + totalCount, + folderId, + folderPaths }; }; diff --git a/backend/src/ee/services/pam-account/pam-account-types.ts b/backend/src/ee/services/pam-account/pam-account-types.ts index 4bbccc6fa..775ef1f78 100644 --- a/backend/src/ee/services/pam-account/pam-account-types.ts +++ b/backend/src/ee/services/pam-account/pam-account-types.ts @@ -1,3 +1,5 @@ +import { OrderByDirection, TProjectPermission } from "@app/lib/types"; + import { TPamAccount } from "../pam-resource/pam-resource-types"; // DTOs @@ -18,3 +20,22 @@ export type TAccessAccountDTO = { actorUserAgent: string; duration: number; }; + +export type TListAccountsDTO = { + accountPath: string; + accountView: PamAccountView; + search?: string; + orderBy?: PamAccountOrderBy; + orderDirection?: OrderByDirection; + limit?: number; + offset?: number; +} & TProjectPermission; + +export enum PamAccountOrderBy { + Name = "name" +} + +export enum PamAccountView { + Flat = "flat", + Nested = "nested" +} diff --git a/backend/src/ee/services/pam-folder/pam-folder-dal.ts b/backend/src/ee/services/pam-folder/pam-folder-dal.ts index aa334618d..9d0ca6cd2 100644 --- a/backend/src/ee/services/pam-folder/pam-folder-dal.ts +++ b/backend/src/ee/services/pam-folder/pam-folder-dal.ts @@ -1,9 +1,104 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { OrderByDirection } from "@app/lib/types"; + +import { PamAccountOrderBy } from "../pam-account/pam-account-types"; export type TPamFolderDALFactory = ReturnType; export const pamFolderDALFactory = (db: TDbClient) => { const orm = ormify(db, TableName.PamFolder); - return { ...orm }; + + const findByProjectId = async ( + { + projectId, + parentId, + search, + limit, + offset = 0, + orderBy = PamAccountOrderBy.Name, + orderDirection = OrderByDirection.ASC + }: { + projectId: string; + parentId?: string | null; + search?: string; + limit?: number; + offset?: number; + orderBy?: PamAccountOrderBy; + orderDirection?: OrderByDirection; + }, + tx?: Knex + ) => { + try { + const dbInstance = tx || db.replicaNode(); + const query = dbInstance(TableName.PamFolder).where(`${TableName.PamFolder}.projectId`, projectId); + + if (parentId) { + void query.where(`${TableName.PamFolder}.parentId`, parentId); + } else { + void query.whereNull(`${TableName.PamFolder}.parentId`); + } + + if (search) { + void query.whereILike(`${TableName.PamFolder}.name`, `%${search}%`); + } + + const countQuery = query.clone().count("*", { as: "count" }).first(); + + void query.select(selectAllTableCols(TableName.PamFolder)); + const direction = orderDirection === OrderByDirection.ASC ? "ASC" : "DESC"; + + void query.orderByRaw(`${TableName.PamFolder}.?? COLLATE "en-x-icu" ${direction}`, [orderBy]); + + if (typeof limit === "number") { + void query.limit(limit).offset(offset); + } + + const [folders, countResult] = await Promise.all([query, countQuery]); + const totalCount = Number(countResult?.count || 0); + + return { folders, totalCount }; + } catch (error) { + throw new DatabaseError({ error, name: "Find PAM folders" }); + } + }; + + const findByPath = async (projectId: string, path: string, tx?: Knex) => { + try { + const dbInstance = tx || db.replicaNode(); + const pathSegments = path.split("/").filter(Boolean); + + let parentId: string | null = null; + let currentFolder: Awaited> | undefined; + + for await (const segment of pathSegments) { + const query = dbInstance(TableName.PamFolder) + .where(`${TableName.PamFolder}.projectId`, projectId) + .where(`${TableName.PamFolder}.name`, segment); + + if (parentId) { + void query.where(`${TableName.PamFolder}.parentId`, parentId); + } else { + void query.whereNull(`${TableName.PamFolder}.parentId`); + } + + currentFolder = await query.first(); + + if (!currentFolder) { + return undefined; + } + + parentId = currentFolder.id; + } + + return currentFolder; + } catch (error) { + throw new DatabaseError({ error, name: "Find PAM folder by path" }); + } + }; + + return { ...orm, findByProjectId, findByPath }; }; diff --git a/frontend/src/hooks/api/pam/enums.ts b/frontend/src/hooks/api/pam/enums.ts index 0684f6073..bd5779487 100644 --- a/frontend/src/hooks/api/pam/enums.ts +++ b/frontend/src/hooks/api/pam/enums.ts @@ -12,3 +12,12 @@ export enum PamSessionStatus { Ended = "ended", Terminated = "terminated" } + +export enum PamAccountOrderBy { + Name = "name" +} + +export enum PamAccountView { + Flat = "flat", + Nested = "nested" +} diff --git a/frontend/src/hooks/api/pam/mutations.tsx b/frontend/src/hooks/api/pam/mutations.tsx index 99a89b425..e6ee62e02 100644 --- a/frontend/src/hooks/api/pam/mutations.tsx +++ b/frontend/src/hooks/api/pam/mutations.tsx @@ -82,7 +82,7 @@ export const useCreatePamAccount = () => { return data.account; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) }); } }); }; @@ -99,7 +99,7 @@ export const useUpdatePamAccount = () => { return data.account; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) }); } }); }; @@ -115,7 +115,7 @@ export const useDeletePamAccount = () => { return data.account; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) }); } }); }; @@ -130,7 +130,7 @@ export const useCreatePamFolder = () => { return data.folder; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) }); } }); }; @@ -147,7 +147,7 @@ export const useUpdatePamFolder = () => { return data.folder; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) }); } }); }; @@ -163,7 +163,7 @@ export const useDeletePamFolder = () => { return data.folder; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) }); } }); }; diff --git a/frontend/src/hooks/api/pam/queries.tsx b/frontend/src/hooks/api/pam/queries.tsx index 6339b4761..08861d1e2 100644 --- a/frontend/src/hooks/api/pam/queries.tsx +++ b/frontend/src/hooks/api/pam/queries.tsx @@ -4,7 +4,7 @@ import { apiRequest } from "@app/config/request"; import { TPamResourceOption } from "./types/resource-options"; import { PamResourceType } from "./enums"; -import { TPamAccount, TPamFolder, TPamResource, TPamSession } from "./types"; +import { TListPamAccountsDTO, TPamAccount, TPamFolder, TPamResource, TPamSession } from "./types"; export const pamKeys = { all: ["pam"] as const, @@ -19,7 +19,12 @@ export const pamKeys = { resourceType, resourceId ], - listAccounts: (projectId: string) => [...pamKeys.account(), "list", projectId], + listAccounts: ({ projectId, ...params }: TListPamAccountsDTO) => [ + ...pamKeys.account(), + "list", + projectId, + params + ], getSession: (sessionId: string) => [...pamKeys.session(), "get", sessionId], listSessions: (projectId: string) => [...pamKeys.session(), "list", projectId] }; @@ -98,25 +103,32 @@ export const useGetPamResourceById = ( }; // Accounts +type TListPamAccountsResponse = { + accounts: TPamAccount[]; + folders: TPamFolder[]; + totalCount: number; + folderId?: string; + folderPaths: Record; +}; + export const useListPamAccounts = ( - projectId: string, + params: TListPamAccountsDTO, options?: Omit< UseQueryOptions< - { accounts: TPamAccount[]; folders: TPamFolder[] }, + TListPamAccountsResponse, unknown, - { accounts: TPamAccount[]; folders: TPamFolder[] }, + TListPamAccountsResponse, ReturnType >, "queryKey" | "queryFn" > ) => { return useQuery({ - queryKey: pamKeys.listAccounts(projectId), + queryKey: pamKeys.listAccounts(params), queryFn: async () => { - const { data } = await apiRequest.get<{ accounts: TPamAccount[]; folders: TPamFolder[] }>( - "/api/v1/pam/accounts", - { params: { projectId } } - ); + const { data } = await apiRequest.get("/api/v1/pam/accounts", { + params + }); return data; }, diff --git a/frontend/src/hooks/api/pam/types/index.ts b/frontend/src/hooks/api/pam/types/index.ts index 1b1890cbd..0b91cb9b0 100644 --- a/frontend/src/hooks/api/pam/types/index.ts +++ b/frontend/src/hooks/api/pam/types/index.ts @@ -1,4 +1,5 @@ -import { PamResourceType, PamSessionStatus } from "../enums"; +import { OrderByDirection } from "../../generic/types"; +import { PamAccountOrderBy, PamAccountView, PamResourceType, PamSessionStatus } from "../enums"; import { TMySQLAccount, TMySQLResource } from "./mysql-resource"; import { TPostgresAccount, TPostgresResource } from "./postgres-resource"; @@ -63,6 +64,17 @@ export type TDeletePamResourceDTO = { }; // Account DTOs +export type TListPamAccountsDTO = { + projectId: string; + accountPath?: string | null; + accountView?: PamAccountView; + offset?: number; + limit?: number; + orderBy?: PamAccountOrderBy; + orderDirection?: OrderByDirection; + search?: string; +}; + export type TCreatePamAccountDTO = Pick< TPamAccount, "name" | "description" | "credentials" | "projectId" | "resourceId" | "folderId" diff --git a/frontend/src/pages/pam/PamAccountsPage/components/AccountViewToggle.tsx b/frontend/src/pages/pam/PamAccountsPage/components/AccountViewToggle.tsx index 92b2f7859..51e568551 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/AccountViewToggle.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/AccountViewToggle.tsx @@ -1,13 +1,9 @@ import { Button } from "@app/components/v2"; - -export enum AccountView { - Flat = "flat", - Nested = "nested" -} +import { PamAccountView } from "@app/hooks/api/pam"; type Props = { - value: AccountView; - onChange: (value: AccountView) => void; + value: PamAccountView; + onChange: (value: PamAccountView) => void; }; export const AccountViewToggle = ({ value, onChange }: Props) => { @@ -16,11 +12,11 @@ export const AccountViewToggle = ({ value, onChange }: Props) => { - {accountView !== AccountView.Flat && ( + {accountView !== PamAccountView.Flat && ( handlePopUpToggle("misc", isOpen)} @@ -414,11 +378,11 @@ export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => { Accounts handleSort(OrderBy.Name)} + onClick={() => handleSort(PamAccountOrderBy.Name)} > - + @@ -426,45 +390,48 @@ export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => { - {accountView !== AccountView.Flat && - foldersToRender.map((folder) => ( - handleFolderClick(folder)} - onUpdate={(e) => handlePopUpOpen("updateFolder", e)} - onDelete={(e) => handlePopUpOpen("deleteFolder", e)} - /> - ))} - {currentPageData.map((account) => ( - { - handlePopUpOpen("accessAccount", e); - }} - onUpdate={(e) => handlePopUpOpen("updateAccount", e)} - onDelete={(e) => handlePopUpOpen("deleteAccount", e)} - /> - ))} + {isLoading && } + {!isLoading && ( + <> + {accountView !== PamAccountView.Flat && + foldersToRender.map((folder) => ( + handleFolderClick(folder)} + onUpdate={(e) => handlePopUpOpen("updateFolder", e)} + onDelete={(e) => handlePopUpOpen("deleteFolder", e)} + /> + ))} + {filteredAccounts.map((account) => ( + { + handlePopUpOpen("accessAccount", e); + }} + onUpdate={(e) => handlePopUpOpen("updateAccount", e)} + onDelete={(e) => handlePopUpOpen("deleteAccount", e)} + /> + ))} + + )} - {Boolean(filteredAccounts.length) && ( + {Boolean(totalCount) && !isLoading && ( setPage(newPage)} + onChangePerPage={handlePerPageChange} /> )} - {isContentEmpty && ( + {!isLoading && isContentEmpty && ( { isOpen={popUp.addFolder.isOpen} onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)} projectId={projectId} - currentFolderId={effectiveFolderIdForFiltering} + currentFolderId={currentFolderId} /> { isOpen={popUp.addAccount.isOpen} onOpenChange={(isOpen) => handlePopUpToggle("addAccount", isOpen)} projectId={projectId} - currentFolderId={effectiveFolderIdForFiltering} + currentFolderId={currentFolderId} /> ); diff --git a/frontend/src/pages/pam/PamAccountsPage/route.tsx b/frontend/src/pages/pam/PamAccountsPage/route.tsx index 7fbf81b45..65a8c567e 100644 --- a/frontend/src/pages/pam/PamAccountsPage/route.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/route.tsx @@ -2,12 +2,13 @@ import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; -import { AccountView } from "./components/AccountViewToggle"; +import { PamAccountView } from "@app/hooks/api/pam"; + import { PamAccountsPage } from "./PamAccountsPage"; const PamAccountsPageQueryParamsSchema = z.object({ search: z.string().optional(), - accountView: z.nativeEnum(AccountView).optional(), + accountView: z.nativeEnum(PamAccountView).optional(), accountPath: z.string().catch("/") }); From 87f30328134579f663e2618837f9cbd3dfe2f814 Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 7 Nov 2025 16:54:24 -0500 Subject: [PATCH 02/15] pam: resources table server-side filter, search, pagination --- .../pam-resource-router.ts | 31 +++- .../services/pam-resource/pam-resource-dal.ts | 57 +++++++- .../pam-resource/pam-resource-enums.ts | 4 + .../pam-resource/pam-resource-service.ts | 17 +-- .../pam-resource/pam-resource-types.ts | 12 +- frontend/src/hooks/api/pam/enums.ts | 7 + frontend/src/hooks/api/pam/mutations.tsx | 6 +- frontend/src/hooks/api/pam/queries.tsx | 38 +++-- frontend/src/hooks/api/pam/types/index.ts | 17 ++- .../components/ResourceSelect.tsx | 32 +++-- .../components/PamResourcesSection.tsx | 10 +- .../components/PamResourcesTable.tsx | 135 +++++++++--------- 12 files changed, 251 insertions(+), 115 deletions(-) diff --git a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts index 6563c86c7..255e11aa8 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts @@ -5,10 +5,12 @@ import { MySQLResourceListItemSchema, SanitizedMySQLResourceSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas"; +import { PamResourceOrderBy } from "@app/ee/services/pam-resource/pam-resource-enums"; import { PostgresResourceListItemSchema, SanitizedPostgresResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; +import { OrderByDirection } from "@app/lib/types"; import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -52,17 +54,36 @@ export const registerPamResourceRouter = async (server: FastifyZodProvider) => { schema: { description: "List PAM resources", querystring: z.object({ - projectId: z.string().uuid() + projectId: z.string().uuid(), + offset: z.coerce.number().min(0).default(0), + limit: z.coerce.number().min(1).max(100).default(100), + orderBy: z.nativeEnum(PamResourceOrderBy).default(PamResourceOrderBy.Name), + orderDirection: z.nativeEnum(OrderByDirection).default(OrderByDirection.ASC), + search: z.string().trim().optional() }), response: { 200: z.object({ - resources: SanitizedResourceSchema.array() + resources: SanitizedResourceSchema.array(), + totalCount: z.number().default(0) }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const response = await server.services.pamResource.list(req.query.projectId, req.permission); + const { projectId, limit, offset, search, orderBy, orderDirection } = req.query; + + const { resources, totalCount } = await server.services.pamResource.list({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId, + limit, + offset, + search, + orderBy, + orderDirection + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, @@ -71,12 +92,12 @@ export const registerPamResourceRouter = async (server: FastifyZodProvider) => { event: { type: EventType.PAM_RESOURCE_LIST, metadata: { - count: response.resources.length + count: totalCount } } }); - return response; + return { resources, totalCount }; } }); }; diff --git a/backend/src/ee/services/pam-resource/pam-resource-dal.ts b/backend/src/ee/services/pam-resource/pam-resource-dal.ts index 1a408ca27..3b7e17499 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-dal.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-dal.ts @@ -2,7 +2,11 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { OrderByDirection } from "@app/lib/types"; + +import { PamResourceOrderBy } from "./pam-resource-enums"; export type TPamResourceDALFactory = ReturnType; export const pamResourceDALFactory = (db: TDbClient) => { @@ -20,5 +24,56 @@ export const pamResourceDALFactory = (db: TDbClient) => { return doc; }; - return { ...orm, findById }; + const findByProjectId = async ( + { + projectId, + search, + limit, + offset = 0, + orderBy = PamResourceOrderBy.Name, + orderDirection = OrderByDirection.ASC + }: { + projectId: string; + search?: string; + limit?: number; + offset?: number; + orderBy?: PamResourceOrderBy; + orderDirection?: OrderByDirection; + }, + tx?: Knex + ) => { + try { + const dbInstance = tx || db.replicaNode(); + const query = dbInstance(TableName.PamResource).where(`${TableName.PamResource}.projectId`, projectId); + + if (search) { + void query.where((q) => { + void q + .whereILike(`${TableName.PamResource}.name`, `%${search}%`) + .orWhereILike(`${TableName.PamResource}.resourceType`, `%${search}%`); + }); + } + + const countQuery = query.clone().count("*", { as: "count" }).first(); + + void query.select(selectAllTableCols(TableName.PamResource)); + + const direction = orderDirection === OrderByDirection.ASC ? "ASC" : "DESC"; + + void query.orderByRaw(`${TableName.PamResource}.?? COLLATE "en-x-icu" ${direction}`, [orderBy]); + + if (typeof limit === "number") { + void query.limit(limit).offset(offset); + } + + const [resources, countResult] = await Promise.all([query, countQuery]); + const totalCount = Number(countResult?.count || 0); + + return { resources, totalCount }; + } catch (error) { + throw new DatabaseError({ error, name: "Find PAM resources" }); + } + }; + + return { ...orm, findById, findByProjectId }; }; diff --git a/backend/src/ee/services/pam-resource/pam-resource-enums.ts b/backend/src/ee/services/pam-resource/pam-resource-enums.ts index dff1cc650..e377b8172 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-enums.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-enums.ts @@ -2,3 +2,7 @@ export enum PamResource { Postgres = "postgres", MySQL = "mysql" } + +export enum PamResourceOrderBy { + Name = "name" +} diff --git a/backend/src/ee/services/pam-resource/pam-resource-service.ts b/backend/src/ee/services/pam-resource/pam-resource-service.ts index d97905dbe..f3ab43944 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-service.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-service.ts @@ -20,7 +20,7 @@ import { encryptResourceConnectionDetails, listResourceOptions } from "./pam-resource-fns"; -import { TCreateResourceDTO, TUpdateResourceDTO } from "./pam-resource-types"; +import { TCreateResourceDTO, TListResourcesDTO, TUpdateResourceDTO } from "./pam-resource-types"; type TPamResourceServiceFactoryDep = { pamResourceDAL: TPamResourceDALFactory; @@ -268,22 +268,23 @@ export const pamResourceServiceFactory = ({ } }; - const list = async (projectId: string, actor: OrgServiceActor) => { + const list = async ({ projectId, actor, actorId, actorAuthMethod, actorOrgId, ...params }: TListResourcesDTO) => { const { permission } = await permissionService.getProjectPermission({ - actor: actor.type, - actorAuthMethod: actor.authMethod, - actorId: actor.id, - actorOrgId: actor.orgId, + actor, + actorId, + actorAuthMethod, + actorOrgId, projectId, actionProjectType: ActionProjectType.PAM }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PamResources); - const resources = await pamResourceDAL.find({ projectId }); + const { resources, totalCount } = await pamResourceDAL.findByProjectId({ projectId, ...params }); return { - resources: await Promise.all(resources.map((resource) => decryptResource(resource, projectId, kmsService))) + resources: await Promise.all(resources.map((resource) => decryptResource(resource, projectId, kmsService))), + totalCount }; }; diff --git a/backend/src/ee/services/pam-resource/pam-resource-types.ts b/backend/src/ee/services/pam-resource/pam-resource-types.ts index 1ca9db3e2..8d1f8052b 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-types.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-types.ts @@ -1,3 +1,5 @@ +import { OrderByDirection, TProjectPermission } from "@app/lib/types"; + import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; import { TMySQLAccount, @@ -5,7 +7,7 @@ import { TMySQLResource, TMySQLResourceConnectionDetails } from "./mysql/mysql-resource-types"; -import { PamResource } from "./pam-resource-enums"; +import { PamResource, PamResourceOrderBy } from "./pam-resource-enums"; import { TPostgresAccount, TPostgresAccountCredentials, @@ -32,6 +34,14 @@ export type TUpdateResourceDTO = Partial = () => Promise; export type TPamResourceFactoryValidateAccountCredentials = ( diff --git a/frontend/src/hooks/api/pam/enums.ts b/frontend/src/hooks/api/pam/enums.ts index bd5779487..d0d88edb8 100644 --- a/frontend/src/hooks/api/pam/enums.ts +++ b/frontend/src/hooks/api/pam/enums.ts @@ -1,3 +1,4 @@ +// Resources export enum PamResourceType { Postgres = "postgres", MySQL = "mysql", @@ -6,6 +7,11 @@ export enum PamResourceType { Kubernetes = "kubernetes" } +export enum PamResourceOrderBy { + Name = "name" +} + +// Sessions export enum PamSessionStatus { Starting = "starting", Active = "active", @@ -13,6 +19,7 @@ export enum PamSessionStatus { Terminated = "terminated" } +// Accounts export enum PamAccountOrderBy { Name = "name" } diff --git a/frontend/src/hooks/api/pam/mutations.tsx b/frontend/src/hooks/api/pam/mutations.tsx index e6ee62e02..c5d6ff05b 100644 --- a/frontend/src/hooks/api/pam/mutations.tsx +++ b/frontend/src/hooks/api/pam/mutations.tsx @@ -31,7 +31,7 @@ export const useCreatePamResource = () => { return data.resource; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listResources(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listResources({ projectId }) }); } }); }; @@ -48,7 +48,7 @@ export const useUpdatePamResource = () => { return data.resource; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listResources(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listResources({ projectId }) }); } }); }; @@ -64,7 +64,7 @@ export const useDeletePamResource = () => { return data.resource; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listResources(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listResources({ projectId }) }); } }); }; diff --git a/frontend/src/hooks/api/pam/queries.tsx b/frontend/src/hooks/api/pam/queries.tsx index 08861d1e2..7fcfb2569 100644 --- a/frontend/src/hooks/api/pam/queries.tsx +++ b/frontend/src/hooks/api/pam/queries.tsx @@ -4,7 +4,14 @@ import { apiRequest } from "@app/config/request"; import { TPamResourceOption } from "./types/resource-options"; import { PamResourceType } from "./enums"; -import { TListPamAccountsDTO, TPamAccount, TPamFolder, TPamResource, TPamSession } from "./types"; +import { + TListPamAccountsDTO, + TListPamResourcesDTO, + TPamAccount, + TPamFolder, + TPamResource, + TPamSession +} from "./types"; export const pamKeys = { all: ["pam"] as const, @@ -12,7 +19,12 @@ export const pamKeys = { account: () => [...pamKeys.all, "account"] as const, session: () => [...pamKeys.all, "session"] as const, listResourceOptions: () => [...pamKeys.resource(), "options"] as const, - listResources: (projectId: string) => [...pamKeys.resource(), "list", projectId], + listResources: ({ projectId, ...params }: TListPamResourcesDTO) => [ + ...pamKeys.resource(), + "list", + projectId, + params + ], getResource: (resourceType: string, resourceId: string) => [ ...pamKeys.resource(), "get", @@ -54,27 +66,31 @@ export const useListPamResourceOptions = ( }); }; +type TListPamResourcesResponse = { + resources: TPamResource[]; + totalCount: number; +}; + export const useListPamResources = ( - projectId: string, + params: TListPamResourcesDTO, options?: Omit< UseQueryOptions< - TPamResource[], + TListPamResourcesResponse, unknown, - TPamResource[], + TListPamResourcesResponse, ReturnType >, "queryKey" | "queryFn" > ) => { return useQuery({ - queryKey: pamKeys.listResources(projectId), + queryKey: pamKeys.listResources(params), queryFn: async () => { - const { data } = await apiRequest.get<{ resources: TPamResource[] }>( - "/api/v1/pam/resources", - { params: { projectId } } - ); + const { data } = await apiRequest.get("/api/v1/pam/resources", { + params + }); - return data.resources; + return data; }, ...options }); diff --git a/frontend/src/hooks/api/pam/types/index.ts b/frontend/src/hooks/api/pam/types/index.ts index 0b91cb9b0..b7e2c70b0 100644 --- a/frontend/src/hooks/api/pam/types/index.ts +++ b/frontend/src/hooks/api/pam/types/index.ts @@ -1,5 +1,11 @@ import { OrderByDirection } from "../../generic/types"; -import { PamAccountOrderBy, PamAccountView, PamResourceType, PamSessionStatus } from "../enums"; +import { + PamAccountOrderBy, + PamAccountView, + PamResourceOrderBy, + PamResourceType, + PamSessionStatus +} from "../enums"; import { TMySQLAccount, TMySQLResource } from "./mysql-resource"; import { TPostgresAccount, TPostgresResource } from "./postgres-resource"; @@ -46,6 +52,15 @@ export type TPamSession = { }; // Resource DTOs +export type TListPamResourcesDTO = { + projectId: string; + offset?: number; + limit?: number; + orderBy?: PamResourceOrderBy; + orderDirection?: OrderByDirection; + search?: string; +}; + export type TCreatePamResourceDTO = Pick< TPamResource, "name" | "connectionDetails" | "resourceType" | "gatewayId" | "projectId" diff --git a/frontend/src/pages/pam/PamAccountsPage/components/ResourceSelect.tsx b/frontend/src/pages/pam/PamAccountsPage/components/ResourceSelect.tsx index 6c6bcfba6..879167195 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/ResourceSelect.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/ResourceSelect.tsx @@ -1,11 +1,12 @@ +import { useState } from "react"; import { Controller, FormProvider, useForm } from "react-hook-form"; import { SingleValue } from "react-select"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; -import { Button, FilterableSelect, FormControl, ModalClose, Spinner } from "@app/components/v2"; +import { Button, FilterableSelect, FormControl, ModalClose } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; -import { usePopUp } from "@app/hooks"; +import { useDebounce, usePopUp } from "@app/hooks"; import { PamResourceType, useListPamResources } from "@app/hooks/api/pam"; import { PamAddResourceModal } from "../../PamResourcesPage/components/PamAddResourceModal"; @@ -28,7 +29,17 @@ type FormData = z.infer; export const ResourceSelect = ({ onSubmit, projectId }: Props) => { const { permission } = useProjectPermission(); - const { isPending, data: resources } = useListPamResources(projectId); + + const [search, setSearch] = useState(""); + const [debouncedSearch] = useDebounce(search, 350); + + const { isPending, data } = useListPamResources({ + projectId, + limit: 100, + search: debouncedSearch + }); + + const resources = data?.resources || []; const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addResource"] as const); @@ -43,15 +54,6 @@ export const ResourceSelect = ({ onSubmit, projectId }: Props) => { ProjectPermissionSub.PamResources ); - if (isPending) { - return ( -
- -

Loading options...

-
- ); - } - return ( <> @@ -65,6 +67,12 @@ export const ResourceSelect = ({ onSubmit, projectId }: Props) => { { + if (actionMeta.action === "input-change") { + setSearch(val); + } + }} onChange={(newValue) => { if ((newValue as SingleValue<{ id: string }>)?.id === "_create") { handlePopUpOpen("addResource"); diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesSection.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesSection.tsx index 41be0a652..dd29c1b21 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesSection.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesSection.tsx @@ -1,17 +1,9 @@ -import { ContentLoader } from "@app/components/v2"; import { useProject } from "@app/context"; -import { useListPamResources } from "@app/hooks/api/pam"; import { PamResourcesTable } from "./PamResourcesTable"; export const PamResourcesSection = () => { const { currentProject } = useProject(); - const { data: resources = [], isPending } = useListPamResources(currentProject.id, { - refetchInterval: 30000 - }); - - if (isPending) return ; - - return ; + return ; }; diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesTable.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesTable.tsx index 2f46a8aca..cb2e35a04 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesTable.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesTable.tsx @@ -27,6 +27,7 @@ import { Pagination, Table, TableContainer, + TableSkeleton, TBody, Th, THead, @@ -39,29 +40,34 @@ import { OrgGatewayPermissionActions, OrgPermissionSubjects } from "@app/context/OrgPermissionContext/types"; -import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; +import { usePagination, usePopUp } from "@app/hooks"; import { OrderByDirection } from "@app/hooks/api/generic/types"; -import { PAM_RESOURCE_TYPE_MAP, PamResourceType, TPamResource } from "@app/hooks/api/pam"; +import { + PAM_RESOURCE_TYPE_MAP, + PamResourceOrderBy, + PamResourceType, + useListPamResources +} from "@app/hooks/api/pam"; import { PamAddResourceModal } from "./PamAddResourceModal"; import { PamDeleteResourceModal } from "./PamDeleteResourceModal"; import { PamResourceRow } from "./PamResourceRow"; import { PamUpdateResourceModal } from "./PamUpdateResourceModal"; -enum OrderBy { - Name = "name" -} - type Filters = { resourceType: PamResourceType[]; }; type Props = { projectId: string; - resources: TPamResource[]; }; -export const PamResourcesTable = ({ projectId, resources }: Props) => { +export const PamResourcesTable = ({ projectId }: Props) => { const navigate = useNavigate({ from: ROUTE_PATHS.Pam.ResourcesPage.path }); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ @@ -80,6 +86,7 @@ export const PamResourcesTable = ({ projectId, resources }: Props) => { const { search, + debouncedSearch, setSearch, setPage, page, @@ -91,51 +98,48 @@ export const PamResourcesTable = ({ projectId, resources }: Props) => { orderBy, setOrderDirection, setOrderBy - } = usePagination(OrderBy.Name, { initPerPage: 20, initSearch }); + } = usePagination(PamResourceOrderBy.Name, { + initPerPage: getUserTablePreference("pamResourcesTable", PreferenceKey.PerPage, 20), + initSearch + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("pamResourcesTable", PreferenceKey.PerPage, newPerPage); + }; + + const { data, isLoading } = useListPamResources({ + projectId, + offset, + limit: perPage, + search: debouncedSearch, + orderBy, + orderDirection + }); + + const resources = data?.resources || []; + const totalCount = data?.totalCount || 0; const filteredResources = useMemo( () => - resources - .filter((resource) => { - const { name, resourceType } = resource; + resources.filter((resource) => { + const { name, resourceType } = resource; - if (filters.resourceType.length && !filters.resourceType.includes(resourceType)) { - return false; - } + if (filters.resourceType.length && !filters.resourceType.includes(resourceType)) { + return false; + } - const searchValue = search.trim().toLowerCase(); + const searchValue = search.trim().toLowerCase(); - const { name: resourceTypeName } = PAM_RESOURCE_TYPE_MAP[resourceType]; - - return ( - name.toLowerCase().includes(searchValue) || - resourceTypeName.toLowerCase().includes(searchValue) - ); - }) - .sort((a, b) => { - const [one, two] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a]; - - switch (orderBy) { - case OrderBy.Name: - default: - return one.name.toLowerCase().localeCompare(two.name.toLowerCase()); - } - }), - [resources, orderDirection, search, orderBy, filters] + return ( + name.toLowerCase().includes(searchValue) || + resourceType.toLowerCase().includes(searchValue) + ); + }), + [resources, search, filters] ); - useResetPageHelper({ - totalCount: filteredResources.length, - offset, - setPage - }); - - const currentPageData = useMemo( - () => filteredResources.slice(offset, perPage * page), - [filteredResources, offset, perPage, page] - ); - - const handleSort = (column: OrderBy) => { + const handleSort = (column: PamResourceOrderBy) => { if (column === orderBy) { toggleOrderDirection(); return; @@ -145,9 +149,10 @@ export const PamResourcesTable = ({ projectId, resources }: Props) => { setOrderDirection(OrderByDirection.ASC); }; - const getClassName = (col: OrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30"); + const getClassName = (col: PamResourceOrderBy) => + twMerge("ml-2", orderBy === col ? "" : "opacity-30"); - const getColSortIcon = (col: OrderBy) => + const getColSortIcon = (col: PamResourceOrderBy) => orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown; const isTableFiltered = Boolean(filters.resourceType.length); @@ -263,11 +268,11 @@ export const PamResourcesTable = ({ projectId, resources }: Props) => { Resource handleSort(OrderBy.Name)} + onClick={() => handleSort(PamResourceOrderBy.Name)} > - + @@ -275,27 +280,29 @@ export const PamResourcesTable = ({ projectId, resources }: Props) => { - {currentPageData.map((resource) => ( - handlePopUpOpen("updateResource", e)} - onDelete={(e) => handlePopUpOpen("deleteResource", e)} - search={search.trim().toLowerCase()} - /> - ))} + {isLoading && } + {!isLoading && + filteredResources.map((resource) => ( + handlePopUpOpen("updateResource", e)} + onDelete={(e) => handlePopUpOpen("deleteResource", e)} + search={search.trim().toLowerCase()} + /> + ))} - {Boolean(filteredResources.length) && ( + {Boolean(totalCount) && !isLoading && ( setPage(newPage)} + onChangePerPage={handlePerPageChange} /> )} - {isContentEmpty && ( + {!isLoading && isContentEmpty && ( Date: Fri, 7 Nov 2025 17:00:32 -0500 Subject: [PATCH 03/15] move enums to their own file + lint --- .../v1/pam-account-routers/pam-account-router.ts | 2 +- backend/src/ee/services/pam-account/pam-account-dal.ts | 2 +- .../src/ee/services/pam-account/pam-account-enums.ts | 8 ++++++++ .../src/ee/services/pam-account/pam-account-service.ts | 9 ++------- .../src/ee/services/pam-account/pam-account-types.ts | 10 +--------- backend/src/ee/services/pam-folder/pam-folder-dal.ts | 2 +- 6 files changed, 14 insertions(+), 19 deletions(-) create mode 100644 backend/src/ee/services/pam-account/pam-account-enums.ts diff --git a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts index 626daa898..59756ab3e 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { PamFoldersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { PamAccountOrderBy, PamAccountView } from "@app/ee/services/pam-account/pam-account-types"; +import { PamAccountOrderBy, PamAccountView } from "@app/ee/services/pam-account/pam-account-enums"; import { SanitizedMySQLAccountWithResourceSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas"; import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; diff --git a/backend/src/ee/services/pam-account/pam-account-dal.ts b/backend/src/ee/services/pam-account/pam-account-dal.ts index 562567172..5ee805ce1 100644 --- a/backend/src/ee/services/pam-account/pam-account-dal.ts +++ b/backend/src/ee/services/pam-account/pam-account-dal.ts @@ -6,7 +6,7 @@ import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; import { OrderByDirection } from "@app/lib/types"; -import { PamAccountOrderBy, PamAccountView } from "./pam-account-types"; +import { PamAccountOrderBy, PamAccountView } from "./pam-account-enums"; export type TPamAccountDALFactory = ReturnType; diff --git a/backend/src/ee/services/pam-account/pam-account-enums.ts b/backend/src/ee/services/pam-account/pam-account-enums.ts new file mode 100644 index 000000000..92b95df94 --- /dev/null +++ b/backend/src/ee/services/pam-account/pam-account-enums.ts @@ -0,0 +1,8 @@ +export enum PamAccountOrderBy { + Name = "name" +} + +export enum PamAccountView { + Flat = "flat", + Nested = "nested" +} diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts index bfc6c7e7f..8050f702e 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -31,14 +31,9 @@ import { TPamSessionDALFactory } from "../pam-session/pam-session-dal"; import { PamSessionStatus } from "../pam-session/pam-session-enums"; import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPamAccountDALFactory } from "./pam-account-dal"; +import { PamAccountView } from "./pam-account-enums"; import { decryptAccount, decryptAccountCredentials, encryptAccountCredentials } from "./pam-account-fns"; -import { - PamAccountView, - TAccessAccountDTO, - TCreateAccountDTO, - TListAccountsDTO, - TUpdateAccountDTO -} from "./pam-account-types"; +import { TAccessAccountDTO, TCreateAccountDTO, TListAccountsDTO, TUpdateAccountDTO } from "./pam-account-types"; type TPamAccountServiceFactoryDep = { pamResourceDAL: TPamResourceDALFactory; diff --git a/backend/src/ee/services/pam-account/pam-account-types.ts b/backend/src/ee/services/pam-account/pam-account-types.ts index 775ef1f78..781223f33 100644 --- a/backend/src/ee/services/pam-account/pam-account-types.ts +++ b/backend/src/ee/services/pam-account/pam-account-types.ts @@ -1,6 +1,7 @@ import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { TPamAccount } from "../pam-resource/pam-resource-types"; +import { PamAccountOrderBy, PamAccountView } from "./pam-account-enums"; // DTOs export type TCreateAccountDTO = Pick< @@ -30,12 +31,3 @@ export type TListAccountsDTO = { limit?: number; offset?: number; } & TProjectPermission; - -export enum PamAccountOrderBy { - Name = "name" -} - -export enum PamAccountView { - Flat = "flat", - Nested = "nested" -} diff --git a/backend/src/ee/services/pam-folder/pam-folder-dal.ts b/backend/src/ee/services/pam-folder/pam-folder-dal.ts index 9d0ca6cd2..3566ff5f9 100644 --- a/backend/src/ee/services/pam-folder/pam-folder-dal.ts +++ b/backend/src/ee/services/pam-folder/pam-folder-dal.ts @@ -6,7 +6,7 @@ import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; import { OrderByDirection } from "@app/lib/types"; -import { PamAccountOrderBy } from "../pam-account/pam-account-types"; +import { PamAccountOrderBy } from "../pam-account/pam-account-enums"; export type TPamFolderDALFactory = ReturnType; export const pamFolderDALFactory = (db: TDbClient) => { From c51c47eb0373c14d970db37222a40489aea89eb6 Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 7 Nov 2025 17:09:03 -0500 Subject: [PATCH 04/15] review fixes --- .../ee/routes/v1/pam-resource-routers/pam-resource-router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts index 255e11aa8..f2d0f98ef 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts @@ -92,7 +92,7 @@ export const registerPamResourceRouter = async (server: FastifyZodProvider) => { event: { type: EventType.PAM_RESOURCE_LIST, metadata: { - count: totalCount + count: resources.length } } }); From af2c240839a543eafd004a315e62b719f2649dcc Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 14 Nov 2025 13:05:34 -0500 Subject: [PATCH 05/15] fix some reviews --- backend/src/ee/services/pam-account/pam-account-service.ts | 6 ++++-- frontend/src/hooks/api/pam/queries.tsx | 1 + .../pam/PamAccountsPage/components/PamAccountsTable.tsx | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts index 8050f702e..1c04b0553 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -369,14 +369,16 @@ export const pamAccountServiceFactory = ({ if (canReadFolders && accountView === PamAccountView.Nested) { const { totalCount } = await pamFolderDAL.findByProjectId({ projectId, - parentId: folderId + parentId: folderId, + search: params.search }); totalFolderCount = totalCount; } const { totalCount: totalAccountCount } = await pamAccountDAL.findByProjectIdWithResourceDetails({ projectId, folderId, - accountView + accountView, + search: params.search }); const totalCount = totalFolderCount + totalAccountCount; diff --git a/frontend/src/hooks/api/pam/queries.tsx b/frontend/src/hooks/api/pam/queries.tsx index 7fcfb2569..a9f67897f 100644 --- a/frontend/src/hooks/api/pam/queries.tsx +++ b/frontend/src/hooks/api/pam/queries.tsx @@ -148,6 +148,7 @@ export const useListPamAccounts = ( return data; }, + placeholderData: (prev) => prev, ...options }); }; diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx index df3ef308a..b31aaa330 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx @@ -228,6 +228,7 @@ export const PamAccountsTable = ({ projectId }: Props) => { { + setPage(1); setAccountView(e); navigate({ search: (prev) => ({ From 353fe417d074843d8153a921a16d3442604949dd Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 14 Nov 2025 20:31:48 -0500 Subject: [PATCH 06/15] reset filters on view toggle --- .../pages/pam/PamAccountsPage/components/PamAccountsTable.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx index b31aaa330..5bb3d27e5 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx @@ -229,6 +229,7 @@ export const PamAccountsTable = ({ projectId }: Props) => { value={accountView} onChange={(e) => { setPage(1); + setFilters({ resource: [] }); setAccountView(e); navigate({ search: (prev) => ({ From a37d27ece343d395559c2c3a09b97e701036005e Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 14 Nov 2025 20:33:50 -0500 Subject: [PATCH 07/15] escape special characters for ILIKE search --- backend/src/ee/services/pam-account/pam-account-dal.ts | 9 ++++++--- backend/src/ee/services/pam-folder/pam-folder-dal.ts | 4 +++- backend/src/ee/services/pam-resource/pam-resource-dal.ts | 7 +++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/backend/src/ee/services/pam-account/pam-account-dal.ts b/backend/src/ee/services/pam-account/pam-account-dal.ts index 5ee805ce1..b9f52b204 100644 --- a/backend/src/ee/services/pam-account/pam-account-dal.ts +++ b/backend/src/ee/services/pam-account/pam-account-dal.ts @@ -50,11 +50,14 @@ export const pamAccountDALFactory = (db: TDbClient) => { } if (search) { + // escape special characters (`%`, `_`) and the escape character itself (`\`) + const escapedSearch = search.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_"); + const pattern = `%${escapedSearch}%`; void query.where((q) => { void q - .whereILike(`${TableName.PamAccount}.name`, `%${search}%`) - .orWhereILike(`${TableName.PamResource}.name`, `%${search}%`) - .orWhereILike(`${TableName.PamAccount}.description`, `%${search}%`); + .whereRaw(`??.?? ILIKE ? ESCAPE '\\'`, [TableName.PamAccount, "name", pattern]) + .orWhereRaw(`??.?? ILIKE ? ESCAPE '\\'`, [TableName.PamResource, "name", pattern]) + .orWhereRaw(`??.?? ILIKE ? ESCAPE '\\'`, [TableName.PamAccount, "description", pattern]); }); } diff --git a/backend/src/ee/services/pam-folder/pam-folder-dal.ts b/backend/src/ee/services/pam-folder/pam-folder-dal.ts index 3566ff5f9..0b8aa8f60 100644 --- a/backend/src/ee/services/pam-folder/pam-folder-dal.ts +++ b/backend/src/ee/services/pam-folder/pam-folder-dal.ts @@ -43,7 +43,9 @@ export const pamFolderDALFactory = (db: TDbClient) => { } if (search) { - void query.whereILike(`${TableName.PamFolder}.name`, `%${search}%`); + // escape special characters (`%`, `_`) and the escape character itself (`\`) + const escapedSearch = search.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_"); + void query.whereRaw(`??.?? ILIKE ? ESCAPE '\\'`, [TableName.PamFolder, "name", `%${escapedSearch}%`]); } const countQuery = query.clone().count("*", { as: "count" }).first(); diff --git a/backend/src/ee/services/pam-resource/pam-resource-dal.ts b/backend/src/ee/services/pam-resource/pam-resource-dal.ts index 3b7e17499..7205542ed 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-dal.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-dal.ts @@ -47,10 +47,13 @@ export const pamResourceDALFactory = (db: TDbClient) => { const query = dbInstance(TableName.PamResource).where(`${TableName.PamResource}.projectId`, projectId); if (search) { + // escape special characters (`%`, `_`) and the escape character itself (`\`) + const escapedSearch = search.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_"); + const pattern = `%${escapedSearch}%`; void query.where((q) => { void q - .whereILike(`${TableName.PamResource}.name`, `%${search}%`) - .orWhereILike(`${TableName.PamResource}.resourceType`, `%${search}%`); + .whereRaw(`??.?? ILIKE ? ESCAPE '\\'`, [TableName.PamResource, "name", pattern]) + .orWhereRaw(`??.?? ILIKE ? ESCAPE '\\'`, [TableName.PamResource, "resourceType", pattern]); }); } From 19610f4fd6a72b3a62376d88d0142f1c5dd6ed9e Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 14 Nov 2025 21:26:10 -0500 Subject: [PATCH 08/15] move account filter to backend & optimize totalCount logic --- .../pam-account-routers/pam-account-router.ts | 17 ++++-- .../services/pam-account/pam-account-dal.ts | 8 ++- .../pam-account/pam-account-service.ts | 30 +++++++---- .../services/pam-account/pam-account-types.ts | 1 + frontend/src/hooks/api/pam/types/index.ts | 5 ++ .../components/PamAccountsTable.tsx | 53 +++++++++---------- 6 files changed, 71 insertions(+), 43 deletions(-) diff --git a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts index 59756ab3e..07e617eab 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts @@ -36,7 +36,16 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { limit: z.coerce.number().min(1).max(100).default(100), orderBy: z.nativeEnum(PamAccountOrderBy).default(PamAccountOrderBy.Name), orderDirection: z.nativeEnum(OrderByDirection).default(OrderByDirection.ASC), - search: z.string().trim().optional() + search: z.string().trim().optional(), + filterResourceIds: z + .string() + .transform((val) => + val + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + ) + .optional() }), response: { 200: z.object({ @@ -50,7 +59,8 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { projectId, accountPath, accountView, limit, offset, search, orderBy, orderDirection } = req.query; + const { projectId, accountPath, accountView, limit, offset, search, orderBy, orderDirection, filterResourceIds } = + req.query; const { accounts, folders, totalCount, folderId, folderPaths } = await server.services.pamAccount.list({ actorId: req.permission.id, @@ -64,7 +74,8 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { offset, search, orderBy, - orderDirection + orderDirection, + filterResourceIds }); await server.services.auditLog.createAuditLog({ diff --git a/backend/src/ee/services/pam-account/pam-account-dal.ts b/backend/src/ee/services/pam-account/pam-account-dal.ts index b9f52b204..5fa242627 100644 --- a/backend/src/ee/services/pam-account/pam-account-dal.ts +++ b/backend/src/ee/services/pam-account/pam-account-dal.ts @@ -22,7 +22,8 @@ export const pamAccountDALFactory = (db: TDbClient) => { limit, offset = 0, orderBy = PamAccountOrderBy.Name, - orderDirection = OrderByDirection.ASC + orderDirection = OrderByDirection.ASC, + filterResourceIds }: { projectId: string; folderId?: string | null; @@ -32,6 +33,7 @@ export const pamAccountDALFactory = (db: TDbClient) => { offset?: number; orderBy?: PamAccountOrderBy; orderDirection?: OrderByDirection; + filterResourceIds?: string[]; }, tx?: Knex ) => { @@ -61,6 +63,10 @@ export const pamAccountDALFactory = (db: TDbClient) => { }); } + if (filterResourceIds && filterResourceIds.length) { + void query.whereIn(`${TableName.PamAccount}.resourceId`, filterResourceIds); + } + const countQuery = query.clone().count("*", { as: "count" }).first(); void query.select(selectAllTableCols(TableName.PamAccount)).select( diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts index 1c04b0553..93be19b24 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -374,14 +374,6 @@ export const pamAccountServiceFactory = ({ }); totalFolderCount = totalCount; } - const { totalCount: totalAccountCount } = await pamAccountDAL.findByProjectIdWithResourceDetails({ - projectId, - folderId, - accountView, - search: params.search - }); - - const totalCount = totalFolderCount + totalAccountCount; let folders: TPamFolders[] = []; if (canReadFolders && accountView === PamAccountView.Nested && offset < totalFolderCount) { @@ -402,10 +394,12 @@ export const pamAccountServiceFactory = ({ let accountsWithResourceDetails: Awaited< ReturnType >["accounts"] = []; + let totalAccountCount = 0; + const accountsToFetch = limit - folders.length; if (accountsToFetch > 0) { const accountOffset = Math.max(0, offset - totalFolderCount); - const { accounts: accountsResp } = await pamAccountDAL.findByProjectIdWithResourceDetails({ + const { accounts, totalCount } = await pamAccountDAL.findByProjectIdWithResourceDetails({ projectId, folderId, accountView, @@ -413,11 +407,25 @@ export const pamAccountServiceFactory = ({ limit: accountsToFetch, search: params.search, orderBy: params.orderBy, - orderDirection: params.orderDirection + orderDirection: params.orderDirection, + filterResourceIds: params.filterResourceIds }); - accountsWithResourceDetails = accountsResp; + accountsWithResourceDetails = accounts; + totalAccountCount = totalCount; + } else { + // if no accounts are to be fetched for the current page, we still need the total count for pagination + const { totalCount } = await pamAccountDAL.findByProjectIdWithResourceDetails({ + projectId, + folderId, + accountView, + search: params.search, + filterResourceIds: params.filterResourceIds + }); + totalAccountCount = totalCount; } + const totalCount = totalFolderCount + totalAccountCount; + const decryptedAndPermittedAccounts: Array< TPamAccounts & { resource: Pick & { rotationCredentialsConfigured: boolean }; diff --git a/backend/src/ee/services/pam-account/pam-account-types.ts b/backend/src/ee/services/pam-account/pam-account-types.ts index 781223f33..b8498036e 100644 --- a/backend/src/ee/services/pam-account/pam-account-types.ts +++ b/backend/src/ee/services/pam-account/pam-account-types.ts @@ -30,4 +30,5 @@ export type TListAccountsDTO = { orderDirection?: OrderByDirection; limit?: number; offset?: number; + filterResourceIds?: string[]; } & TProjectPermission; diff --git a/frontend/src/hooks/api/pam/types/index.ts b/frontend/src/hooks/api/pam/types/index.ts index b7e2c70b0..ef6309aa8 100644 --- a/frontend/src/hooks/api/pam/types/index.ts +++ b/frontend/src/hooks/api/pam/types/index.ts @@ -79,6 +79,10 @@ export type TDeletePamResourceDTO = { }; // Account DTOs +export type PamAccountFilter = { + resourceIds: string[]; +}; + export type TListPamAccountsDTO = { projectId: string; accountPath?: string | null; @@ -88,6 +92,7 @@ export type TListPamAccountsDTO = { orderBy?: PamAccountOrderBy; orderDirection?: OrderByDirection; search?: string; + filterResourceIds?: string; }; export type TCreatePamAccountDTO = Pick< diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx index 5bb3d27e5..5f9b412eb 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx @@ -50,12 +50,12 @@ import { usePagination, usePopUp } from "@app/hooks"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { PAM_RESOURCE_TYPE_MAP, + PamAccountFilter, PamAccountOrderBy, PamAccountView, - TPamAccount, TPamFolder } from "@app/hooks/api/pam"; -import { useListPamAccounts } from "@app/hooks/api/pam/queries"; +import { useListPamAccounts, useListPamResources } from "@app/hooks/api/pam/queries"; import { AccountViewToggle } from "./AccountViewToggle"; import { FolderBreadCrumbs } from "./FolderBreadCrumbs"; @@ -69,10 +69,6 @@ import { PamFolderRow } from "./PamFolderRow"; import { PamUpdateAccountModal } from "./PamUpdateAccountModal"; import { PamUpdateFolderModal } from "./PamUpdateFolderModal"; -type Filters = { - resource: string[]; -}; - type Props = { projectId: string; }; @@ -103,8 +99,8 @@ export const PamAccountsTable = ({ projectId }: Props) => { initAccountView ?? PamAccountView.Flat ); - const [filters, setFilters] = useState({ - resource: [] + const [filter, setFilter] = useState({ + resourceIds: [] }); const { @@ -139,7 +135,8 @@ export const PamAccountsTable = ({ projectId }: Props) => { limit: perPage, search: debouncedSearch, orderBy, - orderDirection + orderDirection, + filterResourceIds: filter.resourceIds.length ? filter.resourceIds.join(",") : undefined }); const accounts = data?.accounts || []; @@ -166,7 +163,7 @@ export const PamAccountsTable = ({ projectId }: Props) => { resource: { name: resourceName, id: resourceId } } = account; - if (filters.resource.length && !filters.resource.includes(resourceId)) { + if (filter.resourceIds.length && !filter.resourceIds.includes(resourceId)) { return false; } @@ -178,7 +175,7 @@ export const PamAccountsTable = ({ projectId }: Props) => { (description || "").toLowerCase().includes(searchValue) ); }), - [accounts, search, filters] + [accounts, search, filter] ); const handleSort = (column: PamAccountOrderBy) => { @@ -197,7 +194,7 @@ export const PamAccountsTable = ({ projectId }: Props) => { const getColSortIcon = (col: PamAccountOrderBy) => orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown; - const isTableFiltered = Boolean(filters.resource.length); + const isTableFiltered = Boolean(filter.resourceIds.length); const handleFolderClick = (folder: TPamFolder) => { if (accountView === PamAccountView.Flat) { @@ -210,13 +207,13 @@ export const PamAccountsTable = ({ projectId }: Props) => { const isContentEmpty = !filteredAccounts.length && !foldersToRender.length; const isSearchEmpty = isContentEmpty && (Boolean(search) || isTableFiltered); - const uniqueResources = useMemo(() => { - const resourceMap = new Map(); - accounts.forEach((account) => { - resourceMap.set(account.resource.id, account.resource); - }); - return Array.from(resourceMap.values()); - }, [accounts]); + const { data: resourcesData } = useListPamResources({ + projectId, + // temporarily returning a large number until we rework table filtering + limit: 100 + }); + + const resources = resourcesData?.resources || []; return (
@@ -229,7 +226,7 @@ export const PamAccountsTable = ({ projectId }: Props) => { value={accountView} onChange={(e) => { setPage(1); - setFilters({ resource: [] }); + setFilter({ resourceIds: [] }); setAccountView(e); navigate({ search: (prev) => ({ @@ -273,25 +270,25 @@ export const PamAccountsTable = ({ projectId }: Props) => { Resource - {uniqueResources.length ? ( - uniqueResources.map((resource) => { + {resources.length ? ( + resources.map((resource) => { const { name, image } = PAM_RESOURCE_TYPE_MAP[resource.resourceType]; return ( { e.preventDefault(); - const newResources = filters.resource.includes(resource.id) - ? filters.resource.filter((a) => a !== resource.id) - : [...filters.resource, resource.id]; - setFilters((prev) => ({ + const newResources = filter.resourceIds.includes(resource.id) + ? filter.resourceIds.filter((a) => a !== resource.id) + : [...filter.resourceIds, resource.id]; + setFilter((prev) => ({ ...prev, - resource: newResources + resourceIds: newResources })); }} key={resource.id} icon={ - filters.resource.includes(resource.id) && ( + filter.resourceIds.includes(resource.id) && ( ) } From a99e60575cf4b4cd5ed8b70e1e91279446c4a4d5 Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 14 Nov 2025 21:26:19 -0500 Subject: [PATCH 09/15] remove unecessary dbg print --- .../pages/pam/PamAccountsPage/components/PamAddFolderModal.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAddFolderModal.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAddFolderModal.tsx index 266a724cd..d81bd743c 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAddFolderModal.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAddFolderModal.tsx @@ -14,8 +14,6 @@ type Props = { export const PamAddFolderModal = ({ isOpen, onOpenChange, projectId, currentFolderId }: Props) => { const createPamFolder = useCreatePamFolder(); - console.log({ currentFolderId }); - const onSubmit = async (formData: Pick) => { await createPamFolder.mutateAsync({ ...formData, From 9ad69a487f7fd70ae754d10fb23f3b0f8608c784 Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 14 Nov 2025 21:37:47 -0500 Subject: [PATCH 10/15] move resource type filter to backend --- .../pam-resource-router.ts | 16 +++- .../services/pam-resource/pam-resource-dal.ts | 8 +- frontend/src/hooks/api/pam/queries.tsx | 1 + frontend/src/hooks/api/pam/types/index.ts | 1 + .../components/PamResourcesTable.tsx | 86 +++++++++---------- 5 files changed, 63 insertions(+), 49 deletions(-) diff --git a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts index f2d0f98ef..11f2cf774 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts @@ -59,7 +59,16 @@ export const registerPamResourceRouter = async (server: FastifyZodProvider) => { limit: z.coerce.number().min(1).max(100).default(100), orderBy: z.nativeEnum(PamResourceOrderBy).default(PamResourceOrderBy.Name), orderDirection: z.nativeEnum(OrderByDirection).default(OrderByDirection.ASC), - search: z.string().trim().optional() + search: z.string().trim().optional(), + filterResourceTypes: z + .string() + .transform((val) => + val + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + ) + .optional() }), response: { 200: z.object({ @@ -70,7 +79,7 @@ export const registerPamResourceRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { projectId, limit, offset, search, orderBy, orderDirection } = req.query; + const { projectId, limit, offset, search, orderBy, orderDirection, filterResourceTypes } = req.query; const { resources, totalCount } = await server.services.pamResource.list({ actorId: req.permission.id, @@ -82,7 +91,8 @@ export const registerPamResourceRouter = async (server: FastifyZodProvider) => { offset, search, orderBy, - orderDirection + orderDirection, + filterResourceTypes }); await server.services.auditLog.createAuditLog({ diff --git a/backend/src/ee/services/pam-resource/pam-resource-dal.ts b/backend/src/ee/services/pam-resource/pam-resource-dal.ts index 7205542ed..9e5cbc985 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-dal.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-dal.ts @@ -31,7 +31,8 @@ export const pamResourceDALFactory = (db: TDbClient) => { limit, offset = 0, orderBy = PamResourceOrderBy.Name, - orderDirection = OrderByDirection.ASC + orderDirection = OrderByDirection.ASC, + filterResourceTypes }: { projectId: string; search?: string; @@ -39,6 +40,7 @@ export const pamResourceDALFactory = (db: TDbClient) => { offset?: number; orderBy?: PamResourceOrderBy; orderDirection?: OrderByDirection; + filterResourceTypes?: string[]; }, tx?: Knex ) => { @@ -57,6 +59,10 @@ export const pamResourceDALFactory = (db: TDbClient) => { }); } + if (filterResourceTypes && filterResourceTypes.length) { + void query.whereIn(`${TableName.PamResource}.resourceType`, filterResourceTypes); + } + const countQuery = query.clone().count("*", { as: "count" }).first(); void query.select(selectAllTableCols(TableName.PamResource)); diff --git a/frontend/src/hooks/api/pam/queries.tsx b/frontend/src/hooks/api/pam/queries.tsx index a9f67897f..8f8b681e6 100644 --- a/frontend/src/hooks/api/pam/queries.tsx +++ b/frontend/src/hooks/api/pam/queries.tsx @@ -92,6 +92,7 @@ export const useListPamResources = ( return data; }, + placeholderData: (prev) => prev, ...options }); }; diff --git a/frontend/src/hooks/api/pam/types/index.ts b/frontend/src/hooks/api/pam/types/index.ts index ef6309aa8..d8ada6cb7 100644 --- a/frontend/src/hooks/api/pam/types/index.ts +++ b/frontend/src/hooks/api/pam/types/index.ts @@ -59,6 +59,7 @@ export type TListPamResourcesDTO = { orderBy?: PamResourceOrderBy; orderDirection?: OrderByDirection; search?: string; + filterResourceTypes?: string; }; export type TCreatePamResourceDTO = Pick< diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesTable.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesTable.tsx index cb2e35a04..4e550a81f 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesTable.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesTable.tsx @@ -59,8 +59,8 @@ import { PamDeleteResourceModal } from "./PamDeleteResourceModal"; import { PamResourceRow } from "./PamResourceRow"; import { PamUpdateResourceModal } from "./PamUpdateResourceModal"; -type Filters = { - resourceType: PamResourceType[]; +type PamResourceFilter = { + resourceTypes: PamResourceType[]; }; type Props = { @@ -80,8 +80,8 @@ export const PamResourcesTable = ({ projectId }: Props) => { from: ROUTE_PATHS.Pam.ResourcesPage.id }); - const [filters, setFilters] = useState({ - resourceType: [] + const [filter, setFilter] = useState({ + resourceTypes: [] }); const { @@ -114,7 +114,8 @@ export const PamResourcesTable = ({ projectId }: Props) => { limit: perPage, search: debouncedSearch, orderBy, - orderDirection + orderDirection, + filterResourceTypes: filter.resourceTypes.length ? filter.resourceTypes.join(",") : undefined }); const resources = data?.resources || []; @@ -125,7 +126,7 @@ export const PamResourcesTable = ({ projectId }: Props) => { resources.filter((resource) => { const { name, resourceType } = resource; - if (filters.resourceType.length && !filters.resourceType.includes(resourceType)) { + if (filter.resourceTypes.length && !filter.resourceTypes.includes(resourceType)) { return false; } @@ -136,7 +137,7 @@ export const PamResourcesTable = ({ projectId }: Props) => { resourceType.toLowerCase().includes(searchValue) ); }), - [resources, search, filters] + [resources, search, filter] ); const handleSort = (column: PamResourceOrderBy) => { @@ -155,7 +156,7 @@ export const PamResourcesTable = ({ projectId }: Props) => { const getColSortIcon = (col: PamResourceOrderBy) => orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown; - const isTableFiltered = Boolean(filters.resourceType.length); + const isTableFiltered = Boolean(filter.resourceTypes.length); const isContentEmpty = !filteredResources.length; const isSearchEmpty = isContentEmpty && (Boolean(search) || isTableFiltered); @@ -192,43 +193,38 @@ export const PamResourcesTable = ({ projectId }: Props) => { Resource Type - {resources.length ? ( - [...new Set(resources.map(({ resourceType }) => resourceType))].map((type) => { - const { name, image } = PAM_RESOURCE_TYPE_MAP[type]; - - return ( - { - e.preventDefault(); - setFilters((prev) => ({ - ...prev, - resourceType: prev.resourceType.includes(type) - ? prev.resourceType.filter((a) => a !== type) - : [...prev.resourceType, type] - })); - }} - key={type} - icon={ - filters.resourceType.includes(type) && ( - - ) - } - iconPos="right" - > -
- {`${name} - {name} -
-
- ); - }) - ) : ( - No Resources - )} + {Object.entries(PAM_RESOURCE_TYPE_MAP).map(([type, { name, image }]) => { + const resourceType = type as PamResourceType; + return ( + { + e.preventDefault(); + setFilter((prev) => ({ + ...prev, + resourceTypes: prev.resourceTypes.includes(resourceType) + ? prev.resourceTypes.filter((a) => a !== resourceType) + : [...prev.resourceTypes, resourceType] + })); + }} + key={resourceType} + icon={ + filter.resourceTypes.includes(resourceType) && ( + + ) + } + iconPos="right" + > +
+ {`${name} + {name} +
+
+ ); + })}
Date: Fri, 14 Nov 2025 21:38:49 -0500 Subject: [PATCH 11/15] move PamAccountFilter to table file --- frontend/src/hooks/api/pam/types/index.ts | 4 ---- .../pam/PamAccountsPage/components/PamAccountsTable.tsx | 5 ++++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/hooks/api/pam/types/index.ts b/frontend/src/hooks/api/pam/types/index.ts index d8ada6cb7..28d569966 100644 --- a/frontend/src/hooks/api/pam/types/index.ts +++ b/frontend/src/hooks/api/pam/types/index.ts @@ -80,10 +80,6 @@ export type TDeletePamResourceDTO = { }; // Account DTOs -export type PamAccountFilter = { - resourceIds: string[]; -}; - export type TListPamAccountsDTO = { projectId: string; accountPath?: string | null; diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx index 5f9b412eb..63a347961 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx @@ -50,7 +50,6 @@ import { usePagination, usePopUp } from "@app/hooks"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { PAM_RESOURCE_TYPE_MAP, - PamAccountFilter, PamAccountOrderBy, PamAccountView, TPamFolder @@ -69,6 +68,10 @@ import { PamFolderRow } from "./PamFolderRow"; import { PamUpdateAccountModal } from "./PamUpdateAccountModal"; import { PamUpdateFolderModal } from "./PamUpdateFolderModal"; +type PamAccountFilter = { + resourceIds: string[]; +}; + type Props = { projectId: string; }; From 6ba48df32bf0ddfb55434de80e0f9a8367d6f92c Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 14 Nov 2025 21:51:49 -0500 Subject: [PATCH 12/15] lint & issue fix --- .../pam-resource/pam-resource-types.ts | 1 + .../src/ee/services/pki-acme/pki-acme-fns.ts | 4 ++- .../ee/services/pki-acme/pki-acme-service.ts | 36 +++++++++---------- .../src/server/routes/v1/bdd-nock-router.ts | 4 +-- backend/src/server/routes/v1/index.ts | 2 +- .../acme/acme-certificate-authority-fns.ts | 4 +-- .../certificate-profile-service.test.ts | 6 ++-- .../certificate-profile-service.ts | 8 ++--- .../CertificateProfilesTab.tsx | 6 ++-- .../CreateProfileModal.tsx | 4 +-- 10 files changed, 39 insertions(+), 36 deletions(-) diff --git a/backend/src/ee/services/pam-resource/pam-resource-types.ts b/backend/src/ee/services/pam-resource/pam-resource-types.ts index 8d1f8052b..033c12bb3 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-types.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-types.ts @@ -40,6 +40,7 @@ export type TListResourcesDTO = { orderDirection?: OrderByDirection; limit?: number; offset?: number; + filterResourceTypes?: string[]; } & TProjectPermission; // Resource factory diff --git a/backend/src/ee/services/pki-acme/pki-acme-fns.ts b/backend/src/ee/services/pki-acme/pki-acme-fns.ts index cc7ddb9b1..a5206d036 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-fns.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-fns.ts @@ -1,6 +1,8 @@ -import { getConfig } from "@app/lib/config/env"; import RE2 from "re2"; import { z } from "zod"; + +import { getConfig } from "@app/lib/config/env"; + import { AcmeAccountDoesNotExistError } from "./pki-acme-errors"; export const buildUrl = (profileId: string, path: string): string => { diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index dadb809d0..43da08b1c 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -12,29 +12,14 @@ import { z, ZodError } from "zod"; import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; import { TPkiAcmeAuths } from "@app/db/schemas/pki-acme-auths"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; +import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { isPrivateIp } from "@app/lib/ip/ipRange"; import { logger } from "@app/lib/logger"; -import { ActorType } from "@app/services/auth/auth-type"; -import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; -import { - EnrollmentType, - TCertificateProfileWithConfigs -} from "@app/services/certificate-profile/certificate-profile-types"; -import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service"; -import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; -import { TKmsServiceFactory } from "@app/services/kms/kms-service"; -import { TProjectDALFactory } from "@app/services/project/project-dal"; -import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; - -import { getConfig } from "@app/lib/config/env"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; -import { orderCertificate } from "@app/services/certificate-authority/acme/acme-certificate-authority-fns"; -import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; -import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; -import { TExternalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal"; -import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils"; +import { ActorType } from "@app/services/auth/auth-type"; +import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; import { @@ -42,6 +27,21 @@ import { CertKeyUsage, CertSubjectAlternativeNameType } from "@app/services/certificate/certificate-types"; +import { orderCertificate } from "@app/services/certificate-authority/acme/acme-certificate-authority-fns"; +import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; +import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; +import { TExternalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal"; +import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils"; +import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; +import { + EnrollmentType, + TCertificateProfileWithConfigs +} from "@app/services/certificate-profile/certificate-profile-types"; +import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; + import { TLicenseServiceFactory } from "../license/license-service"; import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; diff --git a/backend/src/server/routes/v1/bdd-nock-router.ts b/backend/src/server/routes/v1/bdd-nock-router.ts index ad4777772..597d31d9a 100644 --- a/backend/src/server/routes/v1/bdd-nock-router.ts +++ b/backend/src/server/routes/v1/bdd-nock-router.ts @@ -1,11 +1,11 @@ +import nock, { Definition } from "nock"; import { z } from "zod"; import { getConfig } from "@app/lib/config/env"; import { ForbiddenRequestError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { logger } from "@app/lib/logger"; -import nock, { Definition } from "nock"; export const registerBddNockRouter = async (server: FastifyZodProvider) => { const checkIfBddNockApiEnabled = () => { diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 4d8b87e60..7fb8b1d0b 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -1,3 +1,4 @@ +import { getConfig } from "@app/lib/config/env"; import { APP_CONNECTION_REGISTER_ROUTER_MAP, registerAppConnectionRouter @@ -6,7 +7,6 @@ import { registerCmekRouter } from "@app/server/routes/v1/cmek-router"; import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router"; import { registerSecretSyncRouter, SECRET_SYNC_REGISTER_ROUTER_MAP } from "@app/server/routes/v1/secret-sync-routers"; -import { getConfig } from "@app/lib/config/env"; import { registerAdminRouter } from "./admin-router"; import { registerAuthRoutes } from "./auth-router"; import { registerBddNockRouter } from "./bdd-nock-router"; diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts index 52761e6a0..ff95083c6 100644 --- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts @@ -1,7 +1,9 @@ import * as x509 from "@peculiar/x509"; import acme, { CsrBuffer } from "acme-client"; +import { Knex } from "knex"; import { TableName } from "@app/db/schemas"; +import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, CryptographyError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; @@ -29,8 +31,6 @@ import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-ut import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; -import { getConfig } from "@app/lib/config/env"; -import { Knex } from "knex"; import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal"; import { CaStatus, CaType } from "../certificate-authority-enums"; import { keyAlgorithmToAlgCfg } from "../certificate-authority-fns"; diff --git a/backend/src/services/certificate-profile/certificate-profile-service.test.ts b/backend/src/services/certificate-profile/certificate-profile-service.test.ts index 8865ffb9b..1e31d5788 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.test.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.test.ts @@ -5,16 +5,16 @@ import { ForbiddenError } from "@casl/ability"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import type { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; -import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { ActorType, AuthMethod } from "../auth/auth-type"; +import type { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"; +import type { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; import type { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal"; import type { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; import type { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal"; -import type { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"; -import type { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; import { TAcmeEnrollmentConfigDALFactory } from "../enrollment-config/acme-enrollment-config-dal"; import type { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal"; import type { TEstEnrollmentConfigDALFactory } from "../enrollment-config/est-enrollment-config-dal"; diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index fe58f958f..87063c6da 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import { ActionProjectType } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionCertificateActions, @@ -14,14 +15,13 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; -import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; -import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal"; -import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; -import { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal"; import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"; import { getCertificateCredentials, isCertChainValid } from "../certificate/certificate-fns"; import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; +import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal"; +import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; +import { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal"; import { TAcmeEnrollmentConfigDALFactory } from "../enrollment-config/acme-enrollment-config-dal"; import { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal"; import { TAcmeConfigData, TApiConfigData, TEstConfigData } from "../enrollment-config/enrollment-config-types"; diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx index 3218187c4..39bb3c4e9 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx @@ -1,7 +1,8 @@ +import { useState } from "react"; import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useState } from "react"; +import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; import { Button, DeleteActionModal } from "@app/components/v2"; import { useProjectPermission } from "@app/context"; @@ -9,13 +10,12 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types"; +import { usePopUp } from "@app/hooks"; import { TCertificateProfileWithDetails, useDeleteCertificateProfile } from "@app/hooks/api/certificateProfiles"; -import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; -import { usePopUp } from "@app/hooks"; import { CreateProfileModal } from "./CreateProfileModal"; import { ProfileList } from "./ProfileList"; import { RevealAcmeEabSecretModal } from "./RevealAcmeEabSecretModal"; diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx index 712455ce9..516b64288 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx @@ -1,8 +1,8 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect } from "react"; -import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; From c18961c058c8d5c668c67c591bcd064d7b2ed1d5 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 17 Nov 2025 10:29:57 -0500 Subject: [PATCH 13/15] added useResetPageHelper --- .../pam/PamAccountsPage/components/PamAccountsTable.tsx | 8 +++++++- .../pam/PamResourcesPage/components/PamResourcesTable.tsx | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx index 63a347961..d45f89a90 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx @@ -46,7 +46,7 @@ import { PreferenceKey, setUserTablePreference } from "@app/helpers/userTablePreferences"; -import { usePagination, usePopUp } from "@app/hooks"; +import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { PAM_RESOURCE_TYPE_MAP, @@ -148,6 +148,12 @@ export const PamAccountsTable = ({ projectId }: Props) => { const folderPaths = data?.folderPaths || {}; const currentFolderId = data?.folderId ?? null; + useResetPageHelper({ + totalCount, + offset, + setPage + }); + const foldersToRender = useMemo(() => { if (accountView === PamAccountView.Flat) { return []; diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesTable.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesTable.tsx index 4e550a81f..14f08ccad 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesTable.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourcesTable.tsx @@ -45,7 +45,7 @@ import { PreferenceKey, setUserTablePreference } from "@app/helpers/userTablePreferences"; -import { usePagination, usePopUp } from "@app/hooks"; +import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { PAM_RESOURCE_TYPE_MAP, @@ -121,6 +121,12 @@ export const PamResourcesTable = ({ projectId }: Props) => { const resources = data?.resources || []; const totalCount = data?.totalCount || 0; + useResetPageHelper({ + totalCount, + offset, + setPage + }); + const filteredResources = useMemo( () => resources.filter((resource) => { From 7ce65422fece1ff3df9b0f66b9ce7e9677e0fbe6 Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 18 Nov 2025 10:33:37 -0500 Subject: [PATCH 14/15] fix lint --- backend/src/server/routes/v1/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 4d2b1d4b6..68099e50e 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -1,4 +1,3 @@ -import { getConfig } from "@app/lib/config/env"; import { APP_CONNECTION_REGISTER_ROUTER_MAP, registerAppConnectionRouter From 0615939bfbf082ad0d78a38a37c2de4cbcac8e3f Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 18 Nov 2025 16:52:18 -0500 Subject: [PATCH 15/15] lint --- .../ee/routes/v1/pam-resource-routers/pam-resource-router.ts | 2 +- .../src/pages/pam/PamSessionsPage/components/PamSessionRow.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts index 0ef050f7f..3536e7a99 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts @@ -10,11 +10,11 @@ import { PostgresResourceListItemSchema, SanitizedPostgresResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; -import { OrderByDirection } from "@app/lib/types"; import { SanitizedSSHResourceSchema, SSHResourceListItemSchema } from "@app/ee/services/pam-resource/ssh/ssh-resource-schemas"; +import { OrderByDirection } from "@app/lib/types"; import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; diff --git a/frontend/src/pages/pam/PamSessionsPage/components/PamSessionRow.tsx b/frontend/src/pages/pam/PamSessionsPage/components/PamSessionRow.tsx index a616689eb..6c2b6f616 100644 --- a/frontend/src/pages/pam/PamSessionsPage/components/PamSessionRow.tsx +++ b/frontend/src/pages/pam/PamSessionsPage/components/PamSessionRow.tsx @@ -25,7 +25,7 @@ import { } from "@app/components/v2"; import { HighlightText } from "@app/components/v2/HighlightText"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; -import { PAM_RESOURCE_TYPE_MAP, TTerminalEvent, TPamSession } from "@app/hooks/api/pam"; +import { PAM_RESOURCE_TYPE_MAP, TPamSession, TTerminalEvent } from "@app/hooks/api/pam"; import { formatLogContent } from "../../PamSessionsByIDPage/components/PamSessionLogsSection.utils"; import { aggregateTerminalEvents } from "../../PamSessionsByIDPage/components/terminal-utils";