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) && ( ) }