From 299407da6aec9a1c92eba09304393dd4c6349e21 Mon Sep 17 00:00:00 2001 From: Thom Heymann <190132+thomheymann@users.noreply.github.com> Date: Tue, 29 Aug 2023 08:08:08 +0100 Subject: [PATCH 1/2] Add functional tests for serverless security management UIs (#164886) --- .../functional/page_objects/api_keys_page.ts | 2 +- .../test_suites/common/management.ts | 12 + .../test_suites/common/security/api_keys.ts | 457 ++++++++++++++++++ 3 files changed, 470 insertions(+), 1 deletion(-) create mode 100644 x-pack/test_serverless/functional/test_suites/common/security/api_keys.ts diff --git a/x-pack/test/functional/page_objects/api_keys_page.ts b/x-pack/test/functional/page_objects/api_keys_page.ts index 23f63c87107fa..0875774470018 100644 --- a/x-pack/test/functional/page_objects/api_keys_page.ts +++ b/x-pack/test/functional/page_objects/api_keys_page.ts @@ -66,7 +66,7 @@ export function ApiKeysPageProvider({ getService }: FtrProviderContext) { }, async isApiKeyModalExists() { - return await find.existsByCssSelector('[role="dialog"]'); + return await find.existsByCssSelector('.euiFlyoutHeader'); }, async getNewApiKeyCreation() { diff --git a/x-pack/test_serverless/functional/test_suites/common/management.ts b/x-pack/test_serverless/functional/test_suites/common/management.ts index 504325ff363b1..fe2561854c49a 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management.ts @@ -54,6 +54,18 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { appName: 'Watcher', url: 'insightsAndAlerting/watcher', }, + { + appName: 'Users', + url: 'security/users', + }, + { + appName: 'Roles', + url: 'security/roles', + }, + { + appName: 'Role Mappings', + url: 'security/role_mappings', + }, ]; DISABLED_PLUGINS.forEach(({ appName, url }) => { diff --git a/x-pack/test_serverless/functional/test_suites/common/security/api_keys.ts b/x-pack/test_serverless/functional/test_suites/common/security/api_keys.ts new file mode 100644 index 0000000000000..ab650c4c43a25 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/security/api_keys.ts @@ -0,0 +1,457 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { Client } from '@elastic/elasticsearch'; +import { ToolingLog } from '@kbn/tooling-log'; +import type { estypes } from '@elastic/elasticsearch'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +async function clearAllApiKeys(esClient: Client, logger: ToolingLog) { + const existingKeys = await esClient.security.queryApiKeys(); + if (existingKeys.count > 0) { + await Promise.all( + existingKeys.api_keys.map(async (key) => { + esClient.security.invalidateApiKey({ ids: [key.id] }); + }) + ); + } else { + logger.debug('No API keys to delete.'); + } +} + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const es = getService('es'); + const pageObjects = getPageObjects(['common', 'apiKeys']); + const log = getService('log'); + const security = getService('security'); + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const browser = getService('browser'); + const retry = getService('retry'); + + const testRoles: Record = { + viewer: { + cluster: ['all'], + indices: [ + { + names: ['*'], + privileges: ['all'], + allow_restricted_indices: false, + }, + { + names: ['*'], + privileges: ['monitor', 'read', 'view_index_metadata', 'read_cross_cluster'], + allow_restricted_indices: true, + }, + ], + run_as: ['*'], + }, + }; + + const otherUser: estypes.SecurityPutUserRequest = { + username: 'other_user', + password: 'changeme', + roles: ['superuser'], + }; + + async function ensureApiKeysExist(apiKeysNames: string[]) { + await retry.try(async () => { + for (const apiKeyName of apiKeysNames) { + log.debug(`Checking if API key ("${apiKeyName}") exists.`); + await pageObjects.apiKeys.ensureApiKeyExists(apiKeyName); + log.debug(`API key ("${apiKeyName}") exists.`); + } + }); + } + + describe('Home page', function () { + before(async () => { + await clearAllApiKeys(es, log); + await security.testUser.setRoles(['kibana_admin']); + await es.security.putUser(otherUser); + + await pageObjects.common.navigateToUrl('management', 'security/api_keys', { + shouldUseHashForSubUrl: false, + }); + }); + + after(async () => { + await es.security.deleteUser({ username: otherUser.username }); + await security.testUser.restoreDefaults(); + }); + + // https://www.elastic.co/guide/en/kibana/7.6/api-keys.html#api-keys-security-privileges + it('Hides management link if user is not authorized', async () => { + await testSubjects.missingOrFail('apiKeys'); + }); + + it('Loads the app', async () => { + await security.testUser.setRoles(['test_api_keys']); + log.debug('Checking for Create API key call to action'); + await find.existsByLinkText('Create API key'); + }); + + describe('creates API key', function () { + before(async () => { + await security.testUser.setRoles(['kibana_admin', 'test_api_keys']); + await pageObjects.common.navigateToUrl('management', 'security/api_keys', { + shouldUseHashForSubUrl: false, + }); + + // Delete any API keys created outside of these tests + await pageObjects.apiKeys.bulkDeleteApiKeys(); + }); + + afterEach(async () => { + await pageObjects.apiKeys.deleteAllApiKeyOneByOne(); + }); + + after(async () => { + await clearAllApiKeys(es, log); + }); + + it('when submitting form, close dialog and displays new api key', async () => { + const apiKeyName = 'Happy API Key'; + await pageObjects.apiKeys.clickOnPromptCreateApiKey(); + expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys/create'); + expect(await pageObjects.apiKeys.getFlyoutTitleText()).to.be('Create API key'); + + await pageObjects.apiKeys.setApiKeyName(apiKeyName); + await pageObjects.apiKeys.clickSubmitButtonOnApiKeyFlyout(); + const newApiKeyCreation = await pageObjects.apiKeys.getNewApiKeyCreation(); + + expect(await browser.getCurrentUrl()).to.not.contain( + 'app/management/security/api_keys/flyout' + ); + expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys'); + expect(await pageObjects.apiKeys.isApiKeyModalExists()).to.be(false); + expect(newApiKeyCreation).to.be(`Created API key '${apiKeyName}'`); + }); + + it('with optional expiration, redirects back and displays base64', async () => { + const apiKeyName = 'Happy expiration API key'; + await pageObjects.apiKeys.clickOnPromptCreateApiKey(); + expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys/create'); + + await pageObjects.apiKeys.setApiKeyName(apiKeyName); + await pageObjects.apiKeys.toggleCustomExpiration(); + await pageObjects.apiKeys.setApiKeyCustomExpiration('12'); + await pageObjects.apiKeys.clickSubmitButtonOnApiKeyFlyout(); + const newApiKeyCreation = await pageObjects.apiKeys.getNewApiKeyCreation(); + + expect(await browser.getCurrentUrl()).to.not.contain( + 'app/management/security/api_keys/create' + ); + expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys'); + expect(await pageObjects.apiKeys.isApiKeyModalExists()).to.be(false); + expect(newApiKeyCreation).to.be(`Created API key '${apiKeyName}'`); + }); + }); + + describe('Update API key', function () { + before(async () => { + await security.testUser.setRoles(['kibana_admin', 'test_api_keys']); + await pageObjects.common.navigateToUrl('management', 'security/api_keys', { + shouldUseHashForSubUrl: false, + }); + + // Delete any API keys created outside these tests + await pageObjects.apiKeys.bulkDeleteApiKeys(); + }); + + afterEach(async () => { + await pageObjects.apiKeys.deleteAllApiKeyOneByOne(); + }); + + after(async () => { + await clearAllApiKeys(es, log); + }); + + it('should create a new API key, click the name of the new row, fill out and submit form, and display success message', async () => { + // Create a key to updated + const apiKeyName = 'Happy API Key to Update'; + + await es.security.grantApiKey({ + api_key: { + name: apiKeyName, + expiration: '1d', + }, + grant_type: 'password', + run_as: 'elastic', + username: 'elastic', + password: 'changeme', + }); + + await browser.refresh(); + + log.debug('API key created, moving on to update'); + + // Update newly created API Key + await pageObjects.apiKeys.clickExistingApiKeyToOpenFlyout(apiKeyName); + + expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys'); + + await pageObjects.apiKeys.waitForSubmitButtonOnApiKeyFlyoutEnabled(); + + expect(await pageObjects.apiKeys.getFlyoutTitleText()).to.be('Update API key'); + + // Verify name input box are disabled + const apiKeyNameInput = await pageObjects.apiKeys.getApiKeyName(); + expect(await apiKeyNameInput.isEnabled()).to.be(false); + + // Status should be displayed + const apiKeyStatus = await pageObjects.apiKeys.getFlyoutApiKeyStatus(); + expect(await apiKeyStatus).to.be('Expires in a day'); + + // Verify metadata is editable + const apiKeyMetadataSwitch = await pageObjects.apiKeys.getMetadataSwitch(); + expect(await apiKeyMetadataSwitch.isEnabled()).to.be(true); + + // Verify restrict privileges is editable + const apiKeyRestrictPrivilegesSwitch = + await pageObjects.apiKeys.getRestrictPrivilegesSwitch(); + expect(await apiKeyRestrictPrivilegesSwitch.isEnabled()).to.be(true); + + // Toggle restrict privileges so the code editor shows up + await apiKeyRestrictPrivilegesSwitch.click(); + + // Toggle metadata switch so the code editor shows up + await apiKeyMetadataSwitch.click(); + + // Check default value of metadata and set value + const restrictPrivilegesCodeEditorValue = + await pageObjects.apiKeys.getCodeEditorValueByIndex(0); + expect(restrictPrivilegesCodeEditorValue).to.be('{}'); + + // Check default value of metadata and set value + const metadataCodeEditorValue = await pageObjects.apiKeys.getCodeEditorValueByIndex(1); + expect(metadataCodeEditorValue).to.be('{}'); + + await pageObjects.apiKeys.setCodeEditorValueByIndex(0, JSON.stringify(testRoles)); + + await pageObjects.apiKeys.setCodeEditorValueByIndex(1, '{"name":"metadataTest"}'); + + // Submit values to update API key + await pageObjects.apiKeys.clickSubmitButtonOnApiKeyFlyout(); + + // Get success message + const updatedApiKeyToastText = await pageObjects.apiKeys.getApiKeyUpdateSuccessToast(); + expect(updatedApiKeyToastText).to.be(`Updated API key '${apiKeyName}'`); + + expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys'); + expect(await pageObjects.apiKeys.isApiKeyModalExists()).to.be(false); + }); + }); + + describe('Readonly API key', function () { + before(async () => { + await security.role.create('read_security_role', { + elasticsearch: { + cluster: ['read_security'], + }, + kibana: [ + { + feature: { + infrastructure: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.testUser.setRoles(['kibana_admin', 'test_api_keys']); + await pageObjects.common.navigateToUrl('management', 'security/api_keys', { + shouldUseHashForSubUrl: false, + }); + + // Delete any API keys created outside these tests + await pageObjects.apiKeys.bulkDeleteApiKeys(); + }); + + afterEach(async () => { + await pageObjects.apiKeys.deleteAllApiKeyOneByOne(); + }); + + after(async () => { + await clearAllApiKeys(es, log); + }); + + // Serverless tests run as elastic (superuser) so unable to test `read_security` permissions + it.skip('should see readonly form elements', async () => { + // Create a key to updated + const apiKeyName = 'Happy API Key to View'; + + await es.security.grantApiKey({ + api_key: { + name: apiKeyName, + expiration: '1d', + metadata: { name: 'metadatatest' }, + role_descriptors: { ...testRoles }, + }, + grant_type: 'password', + run_as: 'elastic', + username: 'elastic', + password: 'changeme', + }); + + await browser.refresh(); + + log.debug('API key created, moving on to view'); + + // Set testUsers roles to have the `read_security` cluster privilege + await security.testUser.setRoles(['read_security_role']); + + // View newly created API Key + await pageObjects.apiKeys.clickExistingApiKeyToOpenFlyout(apiKeyName); + expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys'); + expect(await pageObjects.apiKeys.getFlyoutTitleText()).to.be('API key details'); + + // Verify name input box are disabled + const apiKeyNameInput = await pageObjects.apiKeys.getApiKeyName(); + expect(await apiKeyNameInput.isEnabled()).to.be(false); + + // Status should be displayed + const apiKeyStatus = await pageObjects.apiKeys.getFlyoutApiKeyStatus(); + expect(await apiKeyStatus).to.be('Expires in a day'); + + const apiKeyMetadataSwitch = await pageObjects.apiKeys.getMetadataSwitch(); + const apiKeyRestrictPrivilegesSwitch = + await pageObjects.apiKeys.getRestrictPrivilegesSwitch(); + + // Verify metadata and restrict privileges switches are now disabled + expect(await apiKeyMetadataSwitch.isEnabled()).to.be(false); + expect(await apiKeyRestrictPrivilegesSwitch.isEnabled()).to.be(false); + + // Close flyout with cancel + await pageObjects.apiKeys.clickCancelButtonOnApiKeyFlyout(); + + // Undo `read_security_role` + await security.testUser.setRoles(['kibana_admin', 'test_api_keys']); + }); + + it('should show the `API key details` flyout if the expiration date is passed', async () => { + const apiKeyName = 'expired-key'; + + await es.security.grantApiKey({ + api_key: { + name: apiKeyName, + expiration: '1ms', + }, + grant_type: 'password', + run_as: 'elastic', + username: 'elastic', + password: 'changeme', + }); + + await browser.refresh(); + + log.debug('API key created, moving on to view'); + + await pageObjects.apiKeys.clickExistingApiKeyToOpenFlyout(apiKeyName); + + expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys'); + expect(await pageObjects.apiKeys.getFlyoutTitleText()).to.be('API key details'); + + // Verify name input box are disabled + const apiKeyNameInput = await pageObjects.apiKeys.getApiKeyName(); + expect(await apiKeyNameInput.isEnabled()).to.be(false); + + // Status should be displayed + const apiKeyStatus = await pageObjects.apiKeys.getFlyoutApiKeyStatus(); + expect(await apiKeyStatus).to.be('Expired'); + + const apiKeyMetadataSwitch = await pageObjects.apiKeys.getMetadataSwitch(); + const apiKeyRestrictPrivilegesSwitch = + await pageObjects.apiKeys.getRestrictPrivilegesSwitch(); + + // Verify metadata and restrict privileges switches are now disabled + expect(await apiKeyMetadataSwitch.isEnabled()).to.be(false); + expect(await apiKeyRestrictPrivilegesSwitch.isEnabled()).to.be(false); + + await pageObjects.apiKeys.clickCancelButtonOnApiKeyFlyout(); + }); + + it('should show the `API key details flyout` if the API key does not belong to the user', async () => { + const apiKeyName = 'other-key'; + + await es.security.grantApiKey({ + api_key: { + name: apiKeyName, + }, + grant_type: 'password', + run_as: otherUser.username, + username: 'elastic', + password: 'changeme', + }); + + await browser.refresh(); + + log.debug('API key created, moving on to view'); + + await pageObjects.apiKeys.clickExistingApiKeyToOpenFlyout(apiKeyName); + expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys'); + expect(await pageObjects.apiKeys.getFlyoutTitleText()).to.be('API key details'); + + // Verify name input box are disabled + const apiKeyNameInput = await pageObjects.apiKeys.getApiKeyName(); + expect(await apiKeyNameInput.isEnabled()).to.be(false); + + // Status should be displayed + const apiKeyStatus = await pageObjects.apiKeys.getFlyoutApiKeyStatus(); + expect(await apiKeyStatus).to.be('Active'); + + const apiKeyMetadataSwitch = await pageObjects.apiKeys.getMetadataSwitch(); + const apiKeyRestrictPrivilegesSwitch = + await pageObjects.apiKeys.getRestrictPrivilegesSwitch(); + + // Verify metadata and restrict privileges switches are now disabled + expect(await apiKeyMetadataSwitch.isEnabled()).to.be(false); + expect(await apiKeyRestrictPrivilegesSwitch.isEnabled()).to.be(false); + + await pageObjects.apiKeys.clickCancelButtonOnApiKeyFlyout(); + }); + }); + + describe('deletes API key(s)', function () { + before(async () => { + await security.testUser.setRoles(['kibana_admin', 'test_api_keys']); + await pageObjects.common.navigateToUrl('management', 'security/api_keys', { + shouldUseHashForSubUrl: false, + }); + }); + + beforeEach(async () => { + await pageObjects.apiKeys.clickOnPromptCreateApiKey(); + await pageObjects.apiKeys.setApiKeyName('api key 1'); + await pageObjects.apiKeys.clickSubmitButtonOnApiKeyFlyout(); + await ensureApiKeysExist(['api key 1']); + }); + + it('one by one', async () => { + await pageObjects.apiKeys.deleteAllApiKeyOneByOne(); + expect(await pageObjects.apiKeys.getApiKeysFirstPromptTitle()).to.be( + 'Create your first API key' + ); + }); + + it('by bulk', async () => { + await pageObjects.apiKeys.clickOnTableCreateApiKey(); + await pageObjects.apiKeys.setApiKeyName('api key 2'); + await pageObjects.apiKeys.clickSubmitButtonOnApiKeyFlyout(); + + // Make sure all API keys we want to delete are created and rendered. + await ensureApiKeysExist(['api key 1', 'api key 2']); + + await pageObjects.apiKeys.bulkDeleteApiKeys(); + expect(await pageObjects.apiKeys.getApiKeysFirstPromptTitle()).to.be( + 'Create your first API key' + ); + }); + }); + }); +}; From 34c7a03016138cc1bae8296e1bb25bf4e73a8f4d Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Tue, 29 Aug 2023 09:54:40 +0200 Subject: [PATCH 2/2] [Security Solution] expandable flyout - add no data message in entities details and entities overview components (#164955) --- .../left/components/entities_details.test.tsx | 15 ++-- .../left/components/entities_details.tsx | 35 ++++++---- .../public/flyout/left/components/test_ids.ts | 1 + .../flyout/left/components/translations.ts | 7 ++ .../components/entities_overview.test.tsx | 70 ++++++++++++++----- .../right/components/entities_overview.tsx | 36 +++++----- .../flyout/right/components/test_ids.ts | 1 + .../flyout/right/components/translations.ts | 7 ++ 8 files changed, 121 insertions(+), 51 deletions(-) diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/entities_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/entities_details.test.tsx index d02f84207ecde..eb56069bb7646 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/entities_details.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/entities_details.test.tsx @@ -11,7 +11,12 @@ import '@testing-library/jest-dom'; import { LeftPanelContext } from '../context'; import { TestProviders } from '../../../common/mock'; import { EntitiesDetails } from './entities_details'; -import { ENTITIES_DETAILS_TEST_ID, HOST_DETAILS_TEST_ID, USER_DETAILS_TEST_ID } from './test_ids'; +import { + ENTITIES_DETAILS_NO_DATA_TEST_ID, + ENTITIES_DETAILS_TEST_ID, + HOST_DETAILS_TEST_ID, + USER_DETAILS_TEST_ID, +} from './test_ids'; import { mockContextValue } from '../mocks/mock_context'; import { EXPANDABLE_PANEL_CONTENT_TEST_ID } from '../../shared/components/test_ids'; @@ -49,8 +54,8 @@ describe('', () => { expect(getByTestId(HOST_TEST_ID)).toBeInTheDocument(); }); - it('does not render user and host details if user name and host name are not available', () => { - const { queryByTestId } = render( + it('should render no data message if user name and host name are not available', () => { + const { getByTestId, queryByTestId } = render( ', () => { ); + expect(getByTestId(ENTITIES_DETAILS_NO_DATA_TEST_ID)).toBeInTheDocument(); expect(queryByTestId(USER_TEST_ID)).not.toBeInTheDocument(); expect(queryByTestId(HOST_TEST_ID)).not.toBeInTheDocument(); }); it('does not render user and host details if @timestamp is not available', () => { - const { queryByTestId } = render( + const { getByTestId, queryByTestId } = render( ', () => { ); + expect(getByTestId(ENTITIES_DETAILS_NO_DATA_TEST_ID)).toBeInTheDocument(); expect(queryByTestId(USER_TEST_ID)).not.toBeInTheDocument(); expect(queryByTestId(HOST_TEST_ID)).not.toBeInTheDocument(); }); diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/entities_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/entities_details.tsx index 29e748dd70eae..ff3678a06e428 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/entities_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/entities_details.tsx @@ -7,11 +7,12 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { ENTITIES_NO_DATA_MESSAGE } from './translations'; import { useLeftPanelContext } from '../context'; import { getField } from '../../shared/utils'; import { UserDetails } from './user_details'; import { HostDetails } from './host_details'; -import { ENTITIES_DETAILS_TEST_ID } from './test_ids'; +import { ENTITIES_DETAILS_NO_DATA_TEST_ID, ENTITIES_DETAILS_TEST_ID } from './test_ids'; export const ENTITIES_TAB_ID = 'entities-details'; @@ -24,19 +25,29 @@ export const EntitiesDetails: React.FC = () => { const userName = getField(getFieldsData('user.name')); const timestamp = getField(getFieldsData('@timestamp')); + const showDetails = timestamp && (hostName || userName); + const showUserDetails = userName && timestamp; + const showHostDetails = hostName && timestamp; + return ( - - {userName && timestamp && ( - - - - )} - {hostName && timestamp && ( - - - + <> + {showDetails ? ( + + {showUserDetails && ( + + + + )} + {showHostDetails && ( + + + + )} + + ) : ( +
{ENTITIES_NO_DATA_MESSAGE}
)} -
+ ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts index 6c736770544bd..03b768607c20c 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts @@ -42,6 +42,7 @@ export const PREVALENCE_DETAILS_TABLE_NO_DATA_TEST_ID = /* Entities */ export const ENTITIES_DETAILS_TEST_ID = `${PREFIX}EntitiesDetails` as const; +export const ENTITIES_DETAILS_NO_DATA_TEST_ID = `${ENTITIES_DETAILS_TEST_ID}NoData` as const; export const USER_DETAILS_TEST_ID = `${PREFIX}UsersDetails` as const; export const USER_DETAILS_INFO_TEST_ID = 'user-overview'; export const USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID = diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts index dbd0cd21e129c..9e7cf56db7c05 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts @@ -7,6 +7,13 @@ import { i18n } from '@kbn/i18n'; +export const ENTITIES_NO_DATA_MESSAGE = i18n.translate( + 'xpack.securitySolution.flyout.entitiesNoDataMessage', + { + defaultMessage: 'No user or host data available', + } +); + export const ANALYZER_ERROR_MESSAGE = i18n.translate( 'xpack.securitySolution.flyout.analyzerErrorMessage', { diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.test.tsx index d26a93262fa31..528b839bb218c 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.test.tsx @@ -11,6 +11,7 @@ import { RightPanelContext } from '../context'; import { ENTITIES_HOST_OVERVIEW_TEST_ID, ENTITIES_USER_OVERVIEW_TEST_ID, + INSIGHTS_ENTITIES_NO_DATA_TEST_ID, INSIGHTS_ENTITIES_TEST_ID, } from './test_ids'; import { EntitiesOverview } from './entities_overview'; @@ -28,16 +29,18 @@ const TITLE_LINK_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(INSIGHTS_E const TITLE_ICON_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(INSIGHTS_ENTITIES_TEST_ID); const TITLE_TEXT_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(INSIGHTS_ENTITIES_TEST_ID); +const mockContextValue = { + eventId: 'event id', + indexName: 'index', + scopeId: 'scopeId', + getFieldsData: mockGetFieldsData, +} as unknown as RightPanelContext; + describe('', () => { it('should render wrapper component', () => { - const contextValue = { - eventId: 'event id', - getFieldsData: mockGetFieldsData, - } as unknown as RightPanelContext; - const { getByTestId, queryByTestId } = render( - + @@ -51,14 +54,9 @@ describe('', () => { }); it('should render user and host', () => { - const contextValue = { - eventId: 'event id', - getFieldsData: mockGetFieldsData, - } as unknown as RightPanelContext; - const { getByTestId } = render( - + @@ -69,7 +67,7 @@ describe('', () => { it('should only render user when host name is null', () => { const contextValue = { - eventId: 'event id', + ...mockContextValue, getFieldsData: (field: string) => (field === 'user.name' ? 'user1' : null), } as unknown as RightPanelContext; @@ -87,7 +85,7 @@ describe('', () => { it('should only render host when user name is null', () => { const contextValue = { - eventId: 'event id', + ...mockContextValue, getFieldsData: (field: string) => (field === 'host.name' ? 'host1' : null), } as unknown as RightPanelContext; @@ -103,13 +101,13 @@ describe('', () => { expect(queryByTestId(ENTITIES_USER_OVERVIEW_TEST_ID)).not.toBeInTheDocument(); }); - it('should not render if both host name and user name are null/blank', () => { + it('should render no data message if both host name and user name are null/blank', () => { const contextValue = { - eventId: 'event id', + ...mockContextValue, getFieldsData: (field: string) => {}, } as unknown as RightPanelContext; - const { container } = render( + const { queryByTestId } = render( @@ -117,13 +115,47 @@ describe('', () => { ); - expect(container).toBeEmptyDOMElement(); + expect(queryByTestId(INSIGHTS_ENTITIES_NO_DATA_TEST_ID)).toBeInTheDocument(); }); it('should not render if eventId is null', () => { const contextValue = { + ...mockContextValue, eventId: null, - getFieldsData: (field: string) => {}, + } as unknown as RightPanelContext; + + const { container } = render( + + + + + + ); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should not render if indexName is null', () => { + const contextValue = { + ...mockContextValue, + indexName: null, + } as unknown as RightPanelContext; + + const { container } = render( + + + + + + ); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should not render if scopeId is null', () => { + const contextValue = { + ...mockContextValue, + scopeId: null, } as unknown as RightPanelContext; const { container } = render( diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.tsx index d74fd844ea4a9..efab7fa9f6d03 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.tsx @@ -8,10 +8,10 @@ import React, { useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { INSIGHTS_ENTITIES_NO_DATA_TEST_ID, INSIGHTS_ENTITIES_TEST_ID } from './test_ids'; import { ExpandablePanel } from '../../shared/components/expandable_panel'; import { useRightPanelContext } from '../context'; -import { INSIGHTS_ENTITIES_TEST_ID } from './test_ids'; -import { ENTITIES_TITLE } from './translations'; +import { ENTITIES_NO_DATA_MESSAGE, ENTITIES_TITLE } from './translations'; import { getField } from '../../shared/utils'; import { HostEntityOverview } from './host_entity_overview'; import { UserEntityOverview } from './user_entity_overview'; @@ -42,7 +42,7 @@ export const EntitiesOverview: React.FC = () => { }); }, [eventId, openLeftPanel, indexName, scopeId]); - if (!eventId || (!userName && !hostName)) { + if (!eventId || !indexName || !scopeId) { return null; } @@ -56,19 +56,23 @@ export const EntitiesOverview: React.FC = () => { }} data-test-subj={INSIGHTS_ENTITIES_TEST_ID} > - - {userName && ( - - - - )} - - {hostName && ( - - - - )} - + {userName || hostName ? ( + + {userName && ( + + + + )} + + {hostName && ( + + + + )} + + ) : ( +
{ENTITIES_NO_DATA_MESSAGE}
+ )} ); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts index c4eb67f5ddba9..9c810f7bfc696 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts @@ -83,6 +83,7 @@ export const SUMMARY_ROW_VALUE_TEST_ID = (dataTestSubj: string) => `${dataTestSu /* Insights Entities */ export const INSIGHTS_ENTITIES_TEST_ID = 'securitySolutionDocumentDetailsFlyoutInsightsEntities'; +export const INSIGHTS_ENTITIES_NO_DATA_TEST_ID = `${INSIGHTS_ENTITIES_TEST_ID}NoData` as const; export const ENTITIES_USER_OVERVIEW_TEST_ID = 'securitySolutionDocumentDetailsFlyoutEntitiesUserOverview'; export const ENTITIES_USER_OVERVIEW_LINK_TEST_ID = `${ENTITIES_USER_OVERVIEW_TEST_ID}Link`; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts index 3a04a68ac0ebf..4d1390701ad10 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts @@ -130,6 +130,13 @@ export const ENTITIES_TITLE = i18n.translate( { defaultMessage: 'Entities' } ); +export const ENTITIES_NO_DATA_MESSAGE = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.entitiesNoDataMessage', + { + defaultMessage: 'No user or host data available', + } +); + export const THREAT_INTELLIGENCE_TITLE = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.threatIntelligenceTitle', { defaultMessage: 'Threat Intelligence' }