From 323562cbe77e796554a490fe6766ff0f201e711f Mon Sep 17 00:00:00 2001 From: Elijah Brown Date: Mon, 6 Jan 2025 10:41:09 -0700 Subject: [PATCH] BED-4868 Wrong Reader Count for AZKeyVaults (#1017) * Added the 'countLabel' field and added a field to the data returned by the endpoint, which EntityInfoDataTable can look at for help getting the correct count for top level Vault Readers. * Add logic to correctly count the AZKeyVaults. * Added a test for BED-4868. * Check in whitespace and header fixes from just prepare-for-codereview --- .../EntityInfo/EntityInfoDataTable.test.tsx | 111 ++++++++++++++++-- .../EntityInfo/EntityInfoDataTable.tsx | 18 ++- .../bh-shared-ui/src/utils/content.ts | 22 +++- 3 files changed, 131 insertions(+), 20 deletions(-) diff --git a/cmd/ui/src/views/Explore/EntityInfo/EntityInfoDataTable.test.tsx b/cmd/ui/src/views/Explore/EntityInfo/EntityInfoDataTable.test.tsx index 302a51a490..4a63c75ae9 100644 --- a/cmd/ui/src/views/Explore/EntityInfo/EntityInfoDataTable.test.tsx +++ b/cmd/ui/src/views/Explore/EntityInfo/EntityInfoDataTable.test.tsx @@ -18,12 +18,13 @@ import { render, screen } from 'src/test-utils'; import userEvent from '@testing-library/user-event'; import { rest, RequestHandler } from 'msw'; import { setupServer } from 'msw/node'; -import { ActiveDirectoryNodeKind, allSections } from 'bh-shared-ui'; +import { ActiveDirectoryNodeKind, allSections, AzureNodeKind } from 'bh-shared-ui'; import EntityInfoDataTable from './EntityInfoDataTable'; import { EntityInfoPanelContextProvider } from './EntityInfoPanelContextProvider'; const objectId = 'fake-object-id'; -const sections = allSections[ActiveDirectoryNodeKind.GPO]!(objectId); +const adGpoSections = allSections[ActiveDirectoryNodeKind.GPO]!(objectId); +const azKeyVaultSections = allSections[AzureNodeKind.KeyVault]!(objectId); const queryCount = { controllers: { @@ -58,25 +59,70 @@ const queryCount = { }, } as const; +const keyVaultTest = { + KeyReaders: { + count: 0, + limit: 128, + skip: 0, + data: [], + }, + CertificateReaders: { + count: 8, + limit: 128, + skip: 0, + data: [], + }, + SecretReaders: { + count: 3003, + limit: 128, + skip: 0, + data: [], + }, + AllReaders: { + count: 1998, + limit: 128, + skip: 0, + data: [], + }, +} as const; + const handlers: Array = [ rest.get(`api/v2/gpos/${objectId}/:asset`, (req, res, ctx) => { const asset = req.params.asset as keyof typeof queryCount; return res(ctx.json(queryCount[asset])); }), + + rest.get(`api/v2/azure/key-vaults*`, (req, res, ctx) => { + if (req.url.searchParams.get('related_entity_type') === 'all-readers') { + return res(ctx.json(keyVaultTest.AllReaders)); + } + + if (req.url.searchParams.get('related_entity_type') === 'key-readers') { + return res(ctx.json(keyVaultTest.KeyReaders)); + } + + if (req.url.searchParams.get('related_entity_type') === 'secret-readers') { + return res(ctx.json(keyVaultTest.SecretReaders)); + } + + if (req.url.searchParams.get('related_entity_type') === 'certificate-readers') { + return res(ctx.json(keyVaultTest.CertificateReaders)); + } + }), ]; const server = setupServer(...handlers); -describe('EntityInfoDataTable', () => { - describe('Node count', () => { - beforeAll(() => server.listen()); - afterEach(() => server.resetHandlers()); - afterAll(() => server.close()); +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); +describe('EntityInfoDataTable', () => { + describe('Node count for nested table that counts all sections', () => { it('sums nested section node counts', async () => { render( - + ); const sum = await screen.findByText('5,064'); @@ -93,7 +139,7 @@ describe('EntityInfoDataTable', () => { render( - + ); @@ -113,7 +159,7 @@ describe('EntityInfoDataTable', () => { const user = userEvent.setup(); render( - + ); @@ -127,4 +173,49 @@ describe('EntityInfoDataTable', () => { expect(zero.textContent).toBe('0'); }); }); + + describe('Node count for Vault Readers nested table', () => { + it('Verify Vault Reader count is the count returned by All Readers', async () => { + const user = userEvent.setup(); + + render( + + + + ); + + // wait for the total count to be available - this holds off all following tests until the counts have been returned + const sum = await screen.findByText('1,998'); + expect(sum).not.toBeNull(); + + // verify the vault reader count is as expected + const vaultReadersHeader = await screen.findByText('Vault Readers'); + const vaultReadersCount = vaultReadersHeader.nextElementSibling; + expect(vaultReadersCount).toHaveTextContent('1,998'); + + // expand the Vault Readers accordian + const button = await screen.findByRole('button'); + await user.click(button); + + // verify the key readers count is as expected + const keyReadersHeader = await screen.findByText('Key Readers'); + const keyReadersCount = keyReadersHeader.nextElementSibling; + expect(keyReadersCount).toHaveTextContent('0'); + + // verify the certificate readers count is as expected + const certReadersHeader = await screen.findByText('Certificate Readers'); + const certReadersCount = certReadersHeader.nextElementSibling; + expect(certReadersCount).toHaveTextContent('8'); + + // verify the secret readers count is as expected + const secretReadersHeader = await screen.findByText('Secret Readers'); + const secretReadersCount = secretReadersHeader.nextElementSibling; + expect(secretReadersCount).toHaveTextContent('3,003'); + + // verify the all readers count is as expected + const allReadersHeader = await screen.findByText('All Readers'); + const allReadersCount = allReadersHeader.nextElementSibling; + expect(allReadersCount).toHaveTextContent('1,998'); + }); + }); }); diff --git a/cmd/ui/src/views/Explore/EntityInfo/EntityInfoDataTable.tsx b/cmd/ui/src/views/Explore/EntityInfo/EntityInfoDataTable.tsx index bb83111aa1..2f127c3c16 100644 --- a/cmd/ui/src/views/Explore/EntityInfo/EntityInfoDataTable.tsx +++ b/cmd/ui/src/views/Explore/EntityInfo/EntityInfoDataTable.tsx @@ -28,7 +28,7 @@ import { addSnackbar } from 'src/ducks/global/actions'; import { transformFlatGraphResponse } from 'src/utils'; import EntityInfoCollapsibleSection from './EntityInfoCollapsibleSection'; -const EntityInfoDataTable: React.FC = ({ id, label, endpoint, sections }) => { +const EntityInfoDataTable: React.FC = ({ id, label, endpoint, countLabel, sections }) => { const dispatch = useDispatch(); const countQuery = useQuery( @@ -83,12 +83,18 @@ const EntityInfoDataTable: React.FC = ({ id, label, en let count: number | undefined; if (Array.isArray(countQuery.data)) { - count = countQuery.data.reduce((acc, val) => { - const count = val.count ?? 0; - return acc + count; - }, 0); + if (countLabel !== undefined) { + countQuery.data.forEach((sectionData: any) => { + if (sectionData.countLabel === countLabel) count = sectionData.count; + }); + } else { + count = countQuery.data.reduce((acc, val) => { + const count = val?.count ?? 0; + return acc + count; + }, 0); + } } else if (countQuery.data) { - count = countQuery.data.count ?? 0; + count = countQuery.data?.count ?? 0; } return ( diff --git a/packages/javascript/bh-shared-ui/src/utils/content.ts b/packages/javascript/bh-shared-ui/src/utils/content.ts index e17c21937a..621bf0aecc 100644 --- a/packages/javascript/bh-shared-ui/src/utils/content.ts +++ b/packages/javascript/bh-shared-ui/src/utils/content.ts @@ -29,6 +29,7 @@ export interface EntityInfoDataTableProps { id: string; label: string; endpoint?: ({ counts, skip, limit, type }: EntitySectionEndpointParams) => Promise; + countLabel?: string; sections?: EntityInfoDataTableProps[]; } @@ -286,6 +287,7 @@ export const allSections: Partial EntityInfo { id, label: 'Vault Readers', + countLabel: 'All Readers', sections: [ { id, @@ -295,7 +297,10 @@ export const allSections: Partial EntityInfo .getAZEntityInfoV2('key-vaults', id, 'key-readers', counts, skip, limit, type, { signal: controller.signal, }) - .then((res) => res.data), + .then((res) => { + if (type !== 'graph') res.data.countLabel = 'Key Readers'; + return res.data; + }), }, { id, @@ -305,7 +310,10 @@ export const allSections: Partial EntityInfo .getAZEntityInfoV2('key-vaults', id, 'certificate-readers', counts, skip, limit, type, { signal: controller.signal, }) - .then((res) => res.data), + .then((res) => { + if (type !== 'graph') res.data.countLabel = 'Certificate Readers'; + return res.data; + }), }, { id, @@ -315,7 +323,10 @@ export const allSections: Partial EntityInfo .getAZEntityInfoV2('key-vaults', id, 'secret-readers', counts, skip, limit, type, { signal: controller.signal, }) - .then((res) => res.data), + .then((res) => { + if (type !== 'graph') res.data.countLabel = 'Secret Readers'; + return res.data; + }), }, { id, @@ -325,7 +336,10 @@ export const allSections: Partial EntityInfo .getAZEntityInfoV2('key-vaults', id, 'all-readers', counts, skip, limit, type, { signal: controller.signal, }) - .then((res) => res.data), + .then((res) => { + if (type !== 'graph') res.data.countLabel = 'All Readers'; + return res.data; + }), }, ], },