diff --git a/x-pack/packages/security-solution/features/src/security/kibana_sub_features.ts b/x-pack/packages/security-solution/features/src/security/kibana_sub_features.ts index d2efada7b3eac..00d7748d7cf98 100644 --- a/x-pack/packages/security-solution/features/src/security/kibana_sub_features.ts +++ b/x-pack/packages/security-solution/features/src/security/kibana_sub_features.ts @@ -17,7 +17,7 @@ import { SecuritySubFeatureId } from '../product_features_keys'; import { APP_ID } from '../constants'; import type { SecurityFeatureParams } from './types'; -const endpointListSubFeature: SubFeatureConfig = { +const endpointListSubFeature = (): SubFeatureConfig => ({ requireAllSpaces: true, privilegesTooltip: i18n.translate( 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList.privilegesTooltip', @@ -67,8 +67,9 @@ const endpointListSubFeature: SubFeatureConfig = { ], }, ], -}; -const trustedApplicationsSubFeature: SubFeatureConfig = { +}); + +const trustedApplicationsSubFeature = (): SubFeatureConfig => ({ requireAllSpaces: true, privilegesTooltip: i18n.translate( 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications.privilegesTooltip', @@ -124,8 +125,8 @@ const trustedApplicationsSubFeature: SubFeatureConfig = { ], }, ], -}; -const hostIsolationExceptionsBasicSubFeature: SubFeatureConfig = { +}); +const hostIsolationExceptionsBasicSubFeature = (): SubFeatureConfig => ({ requireAllSpaces: true, privilegesTooltip: i18n.translate( 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions.privilegesTooltip', @@ -181,8 +182,8 @@ const hostIsolationExceptionsBasicSubFeature: SubFeatureConfig = { ], }, ], -}; -const blocklistSubFeature: SubFeatureConfig = { +}); +const blocklistSubFeature = (): SubFeatureConfig => ({ requireAllSpaces: true, privilegesTooltip: i18n.translate( 'securitySolutionPackages.features.featureRegistry.subFeatures.blockList.privilegesTooltip', @@ -235,8 +236,8 @@ const blocklistSubFeature: SubFeatureConfig = { ], }, ], -}; -const eventFiltersSubFeature: SubFeatureConfig = { +}); +const eventFiltersSubFeature = (): SubFeatureConfig => ({ requireAllSpaces: true, privilegesTooltip: i18n.translate( 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters.privilegesTooltip', @@ -292,8 +293,8 @@ const eventFiltersSubFeature: SubFeatureConfig = { ], }, ], -}; -const policyManagementSubFeature: SubFeatureConfig = { +}); +const policyManagementSubFeature = (): SubFeatureConfig => ({ requireAllSpaces: true, privilegesTooltip: i18n.translate( 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement.privilegesTooltip', @@ -343,9 +344,9 @@ const policyManagementSubFeature: SubFeatureConfig = { ], }, ], -}; +}); -const responseActionsHistorySubFeature: SubFeatureConfig = { +const responseActionsHistorySubFeature = (): SubFeatureConfig => ({ requireAllSpaces: true, privilegesTooltip: i18n.translate( 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory.privilegesTooltip', @@ -394,8 +395,8 @@ const responseActionsHistorySubFeature: SubFeatureConfig = { ], }, ], -}; -const hostIsolationSubFeature: SubFeatureConfig = { +}); +const hostIsolationSubFeature = (): SubFeatureConfig => ({ requireAllSpaces: true, privilegesTooltip: i18n.translate( 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation.privilegesTooltip', @@ -431,9 +432,9 @@ const hostIsolationSubFeature: SubFeatureConfig = { ], }, ], -}; +}); -const processOperationsSubFeature: SubFeatureConfig = { +const processOperationsSubFeature = (): SubFeatureConfig => ({ requireAllSpaces: true, privilegesTooltip: i18n.translate( 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations.privilegesTooltip', @@ -471,8 +472,8 @@ const processOperationsSubFeature: SubFeatureConfig = { ], }, ], -}; -const fileOperationsSubFeature: SubFeatureConfig = { +}); +const fileOperationsSubFeature = (): SubFeatureConfig => ({ requireAllSpaces: true, privilegesTooltip: i18n.translate( 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations.privilegesTooltip', @@ -510,11 +511,11 @@ const fileOperationsSubFeature: SubFeatureConfig = { ], }, ], -}; +}); // execute operations are not available in 8.7, // but will be available in 8.8 -const executeActionSubFeature: SubFeatureConfig = { +const executeActionSubFeature = (): SubFeatureConfig => ({ requireAllSpaces: true, privilegesTooltip: i18n.translate( 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations.privilegesTooltip', @@ -552,10 +553,10 @@ const executeActionSubFeature: SubFeatureConfig = { ], }, ], -}; +}); // 8.15 feature -const scanActionSubFeature: SubFeatureConfig = { +const scanActionSubFeature = (): SubFeatureConfig => ({ requireAllSpaces: true, privilegesTooltip: i18n.translate( 'securitySolutionPackages.features.featureRegistry.subFeatures.scanOperations.privilegesTooltip', @@ -593,9 +594,9 @@ const scanActionSubFeature: SubFeatureConfig = { ], }, ], -}; +}); -const endpointExceptionsSubFeature: SubFeatureConfig = { +const endpointExceptionsSubFeature = (): SubFeatureConfig => ({ requireAllSpaces: true, privilegesTooltip: i18n.translate( 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions.privilegesTooltip', @@ -642,7 +643,7 @@ const endpointExceptionsSubFeature: SubFeatureConfig = { ], }, ], -}; +}); /** * Sub-features that will always be available for Security @@ -660,20 +661,47 @@ export const getSecurityBaseKibanaSubFeatureIds = ( export const getSecuritySubFeaturesMap = ({ experimentalFeatures, }: SecurityFeatureParams): Map => { + const enableSpaceAwarenessIfNeeded = (subFeature: SubFeatureConfig): SubFeatureConfig => { + if (experimentalFeatures.endpointManagementSpaceAwarenessEnabled) { + subFeature.requireAllSpaces = false; + subFeature.privilegesTooltip = undefined; + } + + return subFeature; + }; + const securitySubFeaturesList: Array<[SecuritySubFeatureId, SubFeatureConfig]> = [ - [SecuritySubFeatureId.endpointList, endpointListSubFeature], - [SecuritySubFeatureId.endpointExceptions, endpointExceptionsSubFeature], - [SecuritySubFeatureId.trustedApplications, trustedApplicationsSubFeature], - [SecuritySubFeatureId.hostIsolationExceptionsBasic, hostIsolationExceptionsBasicSubFeature], - [SecuritySubFeatureId.blocklist, blocklistSubFeature], - [SecuritySubFeatureId.eventFilters, eventFiltersSubFeature], - [SecuritySubFeatureId.policyManagement, policyManagementSubFeature], - [SecuritySubFeatureId.responseActionsHistory, responseActionsHistorySubFeature], - [SecuritySubFeatureId.hostIsolation, hostIsolationSubFeature], - [SecuritySubFeatureId.processOperations, processOperationsSubFeature], - [SecuritySubFeatureId.fileOperations, fileOperationsSubFeature], - [SecuritySubFeatureId.executeAction, executeActionSubFeature], - [SecuritySubFeatureId.scanAction, scanActionSubFeature], + [SecuritySubFeatureId.endpointList, enableSpaceAwarenessIfNeeded(endpointListSubFeature())], + [ + SecuritySubFeatureId.endpointExceptions, + enableSpaceAwarenessIfNeeded(endpointExceptionsSubFeature()), + ], + [ + SecuritySubFeatureId.trustedApplications, + enableSpaceAwarenessIfNeeded(trustedApplicationsSubFeature()), + ], + [ + SecuritySubFeatureId.hostIsolationExceptionsBasic, + enableSpaceAwarenessIfNeeded(hostIsolationExceptionsBasicSubFeature()), + ], + [SecuritySubFeatureId.blocklist, enableSpaceAwarenessIfNeeded(blocklistSubFeature())], + [SecuritySubFeatureId.eventFilters, enableSpaceAwarenessIfNeeded(eventFiltersSubFeature())], + [ + SecuritySubFeatureId.policyManagement, + enableSpaceAwarenessIfNeeded(policyManagementSubFeature()), + ], + [ + SecuritySubFeatureId.responseActionsHistory, + enableSpaceAwarenessIfNeeded(responseActionsHistorySubFeature()), + ], + [SecuritySubFeatureId.hostIsolation, enableSpaceAwarenessIfNeeded(hostIsolationSubFeature())], + [ + SecuritySubFeatureId.processOperations, + enableSpaceAwarenessIfNeeded(processOperationsSubFeature()), + ], + [SecuritySubFeatureId.fileOperations, enableSpaceAwarenessIfNeeded(fileOperationsSubFeature())], + [SecuritySubFeatureId.executeAction, enableSpaceAwarenessIfNeeded(executeActionSubFeature())], + [SecuritySubFeatureId.scanAction, enableSpaceAwarenessIfNeeded(scanActionSubFeature())], ]; // Use the following code to add feature based on feature flag diff --git a/x-pack/packages/security-solution/features/src/security/types.ts b/x-pack/packages/security-solution/features/src/security/types.ts index b4ccb7cb52d4a..d069657070fc6 100644 --- a/x-pack/packages/security-solution/features/src/security/types.ts +++ b/x-pack/packages/security-solution/features/src/security/types.ts @@ -9,6 +9,13 @@ import type { ProductFeatureSecurityKey, SecuritySubFeatureId } from '../product import type { ProductFeatureKibanaConfig } from '../types'; export interface SecurityFeatureParams { + /** + * Experimental features. + * Unfortunately these can't be properly Typed due to it requiring an + * import directly from the Security Solution plugin. The list of `keys` in this + * object are defined here: + * @see https://github.com/elastic/kibana/blob/main/x-pack/plugins/security_solution/common/experimental_features.ts#L14 + */ experimentalFeatures: Record; savedObjects: string[]; } diff --git a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.tsx b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.tsx index 4b0d520dee661..daa1ddd704f74 100644 --- a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.tsx +++ b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.tsx @@ -148,6 +148,7 @@ export class FeatureTable extends Component { arrowDisplay={canExpandCategory ? 'left' : 'none'} forceState={canExpandCategory ? undefined : 'closed'} buttonContent={buttonContent} + buttonProps={{ 'data-test-subj': `featureCategory_${category.id}_accordionToggle` }} extraAction={canExpandCategory ? extraAction : undefined} >
@@ -162,7 +163,10 @@ export class FeatureTable extends Component { )} {featuresInCategory.map((feature) => ( - + {this.renderPrivilegeControlsForFeature(feature)} ))} @@ -229,6 +233,9 @@ export class FeatureTable extends Component { data-test-subj="featurePrivilegeControls" buttonContent={buttonContent} buttonClassName="euiAccordionWithDescription" + buttonProps={{ + 'data-test-subj': `featurePrivilegeControls_${feature.category.id}_${feature.id}_accordionToggle`, + }} extraAction={extraAction} forceState={hasSubFeaturePrivileges ? undefined : 'closed'} arrowDisplay={hasSubFeaturePrivileges ? 'left' : 'none'} diff --git a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.tsx b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.tsx index 8e00327fd334b..436119884f51d 100644 --- a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.tsx +++ b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.tsx @@ -71,8 +71,14 @@ export const FeatureTableExpandedRow = ({ }; return ( - - + +
onChange(feature.id, updatedPrivileges)} selectedFeaturePrivileges={selectedFeaturePrivileges} diff --git a/x-pack/packages/security/ui_components/src/kibana_privilege_table/sub_feature_form.tsx b/x-pack/packages/security/ui_components/src/kibana_privilege_table/sub_feature_form.tsx index 9155d8ae52835..21bf95955328b 100644 --- a/x-pack/packages/security/ui_components/src/kibana_privilege_table/sub_feature_form.tsx +++ b/x-pack/packages/security/ui_components/src/kibana_privilege_table/sub_feature_form.tsx @@ -13,7 +13,7 @@ import { EuiIconTip, EuiText, } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import type { @@ -33,10 +33,23 @@ interface Props { privilegeIndex: number; onChange: (selectedPrivileges: string[]) => void; disabled?: boolean; + categoryId?: string; } export const SubFeatureForm = (props: Props) => { const groupsWithPrivileges = props.subFeature.getPrivilegeGroups(); + const subFeatureNameTestId = useMemo(() => { + // Removes anything that is not a Number, Letter or space and replaces it with _ + return props.subFeature.name.toLowerCase().replace(/[^\w\d]/g, '_'); + }, [props.subFeature.name]); + const getTestId = useCallback( + (suffix: string = '') => { + return `${props.categoryId ? `${props.categoryId}_` : ''}${ + props.featureId + }_${subFeatureNameTestId}${suffix ? `_${suffix}` : ''}`; + }, + [props.categoryId, props.featureId, subFeatureNameTestId] + ); const getTooltip = () => { if (!props.subFeature.privilegesTooltip) { @@ -55,6 +68,7 @@ export const SubFeatureForm = (props: Props) => { type="iInCircle" color="subdued" content={tooltipContent} + anchorProps={{ 'data-test-subj': getTestId('nameTooltip') }} /> ); }; @@ -63,11 +77,11 @@ export const SubFeatureForm = (props: Props) => { return null; } return ( - + - + {props.subFeature.name} {getTooltip()} @@ -85,7 +99,9 @@ export const SubFeatureForm = (props: Props) => { )} - {groupsWithPrivileges.map(renderPrivilegeGroup)} + + {groupsWithPrivileges.map(renderPrivilegeGroup)} + ); diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index cae5263df7c4d..cc1c44e8ad00f 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -88,6 +88,16 @@ export const allowedExperimentalValues = Object.freeze({ */ responseActionsCrowdstrikeManualHostIsolationEnabled: true, + /** + * Space awareness for Elastic Defend management. + * Feature depends on Fleet's corresponding features also being enabled: + * - `subfeaturePrivileges` + * - `useSpaceAwareness` + * and Fleet must set it runtime mode to spaces by calling the following API: + * - `POST /internal/fleet/enable_space_awareness` + */ + endpointManagementSpaceAwarenessEnabled: false, + /** * Enables new notes */ diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_list/endpoints_mocked_data.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_list/endpoints_mocked_data.cy.ts index 8232281c4a6a6..2235cd3df6832 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_list/endpoints_mocked_data.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_list/endpoints_mocked_data.cy.ts @@ -52,8 +52,8 @@ describe('Endpoints page', { tags: ['@ess', '@serverless', '@brokenInServerless' expect(body.sortDirection).to.equal('desc'); }); - // no sorting indicator is present on the screen - cy.get('.euiTableSortIcon').should('not.exist'); + // no column shows sorting to be on + cy.get('.euiTableHeaderButton-isSorted').should('not.exist'); }); it('User can sort by any field', () => { @@ -71,12 +71,12 @@ describe('Endpoints page', { tags: ['@ess', '@serverless', '@brokenInServerless' cy.getByTestSubj(`tableHeaderCell_${field}_${i}`).as('header').click(); validateSortingInResponse(field, 'asc'); cy.get('@header').should('have.attr', 'aria-sort', 'ascending'); - cy.get('.euiTableSortIcon').should('exist'); + cy.get('.euiTableHeaderButton-isSorted').should('exist'); cy.get('@header').click(); validateSortingInResponse(field, 'desc'); cy.get('@header').should('have.attr', 'aria-sort', 'descending'); - cy.get('.euiTableSortIcon').should('exist'); + cy.get('.euiTableHeaderButton-isSorted').should('exist'); } }); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_role_rbac.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac.cy.ts similarity index 95% rename from x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_role_rbac.cy.ts rename to x-pack/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac.cy.ts index f0dc0c1a5891d..64779bb2ba27e 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_role_rbac.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac.cy.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { closeAllToasts } from '../tasks/toasts'; -import { login, ROLE } from '../tasks/login'; -import { loadPage } from '../tasks/common'; +import { closeAllToasts } from '../../tasks/toasts'; +import { login, ROLE } from '../../tasks/login'; +import { loadPage } from '../../tasks/common'; describe('When defining a kibana role for Endpoint security access', { tags: '@ess' }, () => { const getAllSubFeatureRows = (): Cypress.Chainable> => { diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac_with_space_awareness.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac_with_space_awareness.cy.ts new file mode 100644 index 0000000000000..424b3fc954c57 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac_with_space_awareness.cy.ts @@ -0,0 +1,126 @@ +/* + * 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 { login, ROLE } from '../../tasks/login'; +import { createSpace, deleteSpace } from '../../tasks/spaces'; +import { + clickEndpointSubFeaturePrivilegesCustomization, + clickFlyoutAddKibanaPrivilegeButton, + clickRoleSaveButton, + clickViewPrivilegeSummaryButton, + ENDPOINT_SUB_FEATURE_PRIVILEGE_IDS, + expandEndpointSecurityFeaturePrivileges, + expandSecuritySolutionCategoryKibanaPrivileges, + navigateToRolePage, + openKibanaFeaturePrivilegesFlyout, + setEndpointSubFeaturePrivilege, + setKibanaPrivilegeSpace, + setRoleName, + setSecuritySolutionEndpointGroupPrivilege, +} from '../../screens/stack_management/role_page'; + +describe( + 'When defining a kibana role for Endpoint security access with space awareness enabled', + { + // TODO:PR Remove `'@skipInServerlessMKI` once PR merges to `main` + tags: ['@ess', '@serverless', '@serverlessMKI', '@skipInServerlessMKI'], + env: { + ftrConfig: { + productTypes: [ + { product_line: 'security', product_tier: 'complete' }, + { product_line: 'endpoint', product_tier: 'complete' }, + ], + kbnServerArgs: [ + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'endpointManagementSpaceAwarenessEnabled', + ])}`, + ], + }, + }, + }, + () => { + let spaceId: string = ''; + const roleName = `test_${Math.random().toString().substring(2, 6)}`; + + before(() => { + login(ROLE.system_indices_superuser); + createSpace(`foo_${Math.random().toString().substring(2, 6)}`).then((response) => { + spaceId = response.body.id; + }); + }); + + after(() => { + if (spaceId) { + deleteSpace(spaceId); + spaceId = ''; + } + }); + + beforeEach(() => { + login(ROLE.system_indices_superuser); + navigateToRolePage(); + setRoleName(roleName); + openKibanaFeaturePrivilegesFlyout(); + expandSecuritySolutionCategoryKibanaPrivileges(); + expandEndpointSecurityFeaturePrivileges(); + }); + + it('should allow configuration per-space', () => { + setKibanaPrivilegeSpace(spaceId); + setSecuritySolutionEndpointGroupPrivilege('all'); + clickEndpointSubFeaturePrivilegesCustomization(); + setEndpointSubFeaturePrivilege('endpoint_list', 'all'); + setEndpointSubFeaturePrivilege('host_isolation', 'all'); + clickFlyoutAddKibanaPrivilegeButton(); + clickRoleSaveButton(); + + navigateToRolePage(roleName); + clickViewPrivilegeSummaryButton(); + cy.getByTestSubj('expandPrivilegeSummaryRow').click({ multiple: true }); + + cy.getByTestSubj('privilegeSummaryFeatureCategory_securitySolution') + .findByTestSubj(`space-avatar-${spaceId}`) + .should('exist'); + + cy.get('#row_siem_expansion') + .findByTestSubj('subFeatureEntry') + .then(($element) => { + const features: string[] = []; + + $element.each((_, $subFeature) => { + features.push($subFeature.textContent ?? ''); + }); + + return features; + }) + // Using `include.members` here because in serverless, an additional privilege shows + // up in this list - `Endpoint exceptions`. + .should('include.members', [ + 'Endpoint ListAll', + 'Trusted ApplicationsNone', + 'Host Isolation ExceptionsNone', + 'BlocklistNone', + 'Event FiltersNone', + 'Elastic Defend Policy ManagementNone', + 'Response Actions HistoryNone', + 'Host IsolationAll', + 'Process OperationsNone', + 'File OperationsNone', + 'Execute OperationsNone', + 'Scan OperationsNone', + ]); + }); + + it('should not display the privilege tooltip', () => { + ENDPOINT_SUB_FEATURE_PRIVILEGE_IDS.forEach((subFeaturePrivilegeId) => { + cy.getByTestSubj(`securitySolution_siem_${subFeaturePrivilegeId}_nameTooltip`).should( + 'not.exist' + ); + }); + }); + } +); diff --git a/x-pack/plugins/security_solution/public/management/cypress/screens/stack_management/role_page.ts b/x-pack/plugins/security_solution/public/management/cypress/screens/stack_management/role_page.ts new file mode 100644 index 0000000000000..fb9b798b93d6e --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/screens/stack_management/role_page.ts @@ -0,0 +1,172 @@ +/* + * 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 { loadPage } from '../../tasks/common'; + +/** + * Navigates to the Stack Management security role page. If `roleName` argument is passed, + * then the edit page for that role is displayed. Otherwise the "create new" role form page + * will be loaded. + * + * @param roleName + */ +export const navigateToRolePage = (roleName?: string): void => { + return loadPage(`/app/management/security/roles/edit${roleName ? `/${roleName}` : ''}`); +}; + +/** + * Ensure the role page is currently loaded + */ +export const ensureOnRolePage = (): Cypress.Chainable> => { + return cy.getByTestSubj('roleNameFormRow').should('exist'); +}; + +/** + * Opens the kibana privileges flyout from the role page + */ +export const openKibanaFeaturePrivilegesFlyout = (): Cypress.Chainable => { + ensureOnRolePage(); + return cy.getByTestSubj('addSpacePrivilegeButton').click(); +}; + +/** + * Returns the Flyout DIV + */ +export const getKibanaFeaturePrivilegesFlyout = (): Cypress.Chainable => { + return cy.getByTestSubj('createSpacePrivilegeButton').closest('.euiFlyout'); +}; + +/** + * Expands the "Security" (security solution) accordion on the Kibana privilesge flyout + */ +export const expandSecuritySolutionCategoryKibanaPrivileges = (): Cypress.Chainable => { + return cy.getByTestSubj('featureCategory_securitySolution_accordionToggle').click(); +}; + +/** + * Returns the area of the ui (`div`) that holds the security Solution set of kibana privileges. + * This is the top-level accordion group in the flyout that is titled "Security" with the security + * icon next to it. + */ +export const getSecuritySolutionCategoryKibanaPrivileges = (): Cypress.Chainable< + JQuery +> => { + return getKibanaFeaturePrivilegesFlyout().findByTestSubj( + 'featureCategory_securitySolution' + ); +}; + +/** + * Expand the "security" grouping found inside of the top-level "Security" (security solution) + * kibana feature privileges grouping. This is the area where Endpoint related RBAC is managed + */ +export const expandEndpointSecurityFeaturePrivileges = (): Cypress.Chainable => { + return cy.getByTestSubj('featurePrivilegeControls_securitySolution_siem_accordionToggle').click(); +}; + +export const getEndpointSecurityFeaturePrivileges = () => { + return cy.getByTestSubj('featureCategory_securitySolution_siem'); +}; + +/** + * Set a space on the Kibana Privileges flyout + * @param spaceId + */ +export const setKibanaPrivilegeSpace = (spaceId: string) => { + getKibanaFeaturePrivilegesFlyout() + .findByTestSubj('spaceSelectorComboBox') + .findByTestSubj('comboBoxToggleListButton') + .click(); + + cy.getByTestSubj('comboBoxOptionsList spaceSelectorComboBox-optionsList') + .find(`button#spaceOption_${spaceId}`) + .click(); +}; + +/** + * Sets the privilege for the `security` grouping (inside of the security solution top-level flyout category) + * @param privilege + */ +export const setSecuritySolutionEndpointGroupPrivilege = ( + privilege: 'all' | 'read' | 'none' +): Cypress.Chainable> => { + return getSecuritySolutionCategoryKibanaPrivileges().findByTestSubj(`siem_${privilege}`).click(); +}; + +/** + * Clicks the toggle that allows for user to customize the Endpoint related features (RBAC) + */ +export const clickEndpointSubFeaturePrivilegesCustomization = (): Cypress.Chainable< + JQuery +> => { + return getEndpointSecurityFeaturePrivileges() + .findByTestSubj('customizeSubFeaturePrivileges') + .click(); +}; + +export const ENDPOINT_SUB_FEATURE_PRIVILEGE_IDS = Object.freeze([ + 'endpoint_list', + 'trusted_applications', + 'host_isolation_exceptions', + 'blocklist', + 'event_filters', + 'elastic_defend_policy_management', + 'response_actions_history', + 'host_isolation', + 'process_operations', + 'file_operations', + 'execute_operations', + 'scan_operations', +] as const); + +type EndpointSubFeaturePrivilegeId = (typeof ENDPOINT_SUB_FEATURE_PRIVILEGE_IDS)[number]; + +/* @private */ +const privilegeMapToTitle = Object.freeze({ + all: 'All', + read: 'Read', + none: 'None', +}); + +export const setEndpointSubFeaturePrivilege = ( + feature: EndpointSubFeaturePrivilegeId, + privilege: 'all' | 'read' | 'none' +): Cypress.Chainable> => { + return getEndpointSecurityFeaturePrivileges() + .findByTestSubj(`securitySolution_siem_${feature}_privilegeGroup`) + .find(`button[title="${privilegeMapToTitle[privilege]}"]`) + .click(); +}; + +/** + * Clicks the "Create/Update space privilege" button on the kibana privileges flyout + */ +export const clickFlyoutAddKibanaPrivilegeButton = (): Cypress.Chainable> => { + return getKibanaFeaturePrivilegesFlyout().findByTestSubj('createSpacePrivilegeButton').click(); +}; + +/** + * Clicks the Save/Update role button + */ +export const clickRoleSaveButton = (): Cypress.Chainable> => { + ensureOnRolePage(); + return cy.getByTestSubj('roleFormSaveButton').click(); +}; + +/** + * Sets the name of the role on the form + * @param roleName + */ +export const setRoleName = (roleName: string): void => { + ensureOnRolePage(); + cy.getByTestSubj('roleFormNameInput').type(roleName); +}; + +export const clickViewPrivilegeSummaryButton = (): Cypress.Chainable> => { + ensureOnRolePage(); + return cy.getByTestSubj('viewPrivilegeSummaryButton').click(); +}; diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/common.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/common.ts index da6a197d9ed99..64fd3279d18cb 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/common.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/common.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { closeKibanaBrowserSecurityToastIfNecessary } from './toasts'; + export const API_AUTH = Object.freeze({ user: Cypress.env('KIBANA_USERNAME') ?? Cypress.env('ELASTICSEARCH_USERNAME'), pass: Cypress.env('KIBANA_PASSWORD') ?? Cypress.env('ELASTICSEARCH_PASSWORD'), @@ -24,6 +26,7 @@ export const waitForPageToBeLoaded = () => { export const loadPage = (url: string, options: Partial = {}) => { cy.visit(url, options); waitForPageToBeLoaded(); + closeKibanaBrowserSecurityToastIfNecessary(); }; export const request = ({ diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/spaces.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/spaces.ts new file mode 100644 index 0000000000000..fbfe4e5c9d238 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/spaces.ts @@ -0,0 +1,27 @@ +/* + * 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 type { Space } from '@kbn/spaces-plugin/common'; +import { request } from './common'; + +export const createSpace = (spaceId: string): Cypress.Chainable> => { + return request({ + method: 'POST', + url: '/api/spaces/space', + body: { + name: spaceId, + id: spaceId, + }, + }); +}; + +export const deleteSpace = (spaceId: string): Cypress.Chainable> => { + return request({ + method: 'DELETE', + url: `/api/spaces/space/${spaceId}`, + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/toasts.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/toasts.ts index af759a297931e..580141806b403 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/toasts.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/toasts.ts @@ -7,11 +7,12 @@ import { subj as testSubjSelector } from '@kbn/test-subj-selector'; import pRetry from 'p-retry'; +const toastCloseButtonSelector = testSubjSelector('toastCloseButton'); + /** * Closes all currently displayed Toasts (if any) */ export const closeAllToasts = (): Cypress.Chainable> => { - const toastCloseButtonSelector = testSubjSelector('toastCloseButton'); const noToastsFoundError = new Error('none found'); return cy.get('body').then(($body) => { @@ -46,3 +47,48 @@ export const expectAndCloseSuccessToast = () => { cy.getByTestSubj('toastCloseButton').click(); cy.contains('Success').should('not.exist'); }; + +/** + * Checks to see if the Toast normally shown by the kibana stack, indicating + * _"Your browser does not meet the security requirements for Kibana."_ and if so, it closes it + */ +export const closeKibanaBrowserSecurityToastIfNecessary = (): Cypress.Chainable< + JQuery +> => { + const toastMessage = 'Your browser does not meet the security requirements for Kibana'; + const subjectSelector = `${testSubjSelector( + 'euiToastHeader__title' + )}:contains('${toastMessage}')`; + const notFound = new Error('notFound'); + + return cy.get('body').then(($body) => { + return pRetry( + async () => { + const toasts = $body.find(subjectSelector).closest('.euiToast'); + + if (toasts.length) { + cy.log( + `Found ${toasts.length} toast(s) with "${toastMessage}" ----> Will close it/them.` + ); + return toasts; + } else { + throw notFound; + } + }, + { retries: 3, forever: false } + ) + .then(($toasts) => { + $toasts.find(toastCloseButtonSelector).each((_, $toastCLoseButton) => { + $toastCLoseButton.click(); + }); + return $body; + }) + .catch((error) => { + if (error !== notFound) { + throw error; + } + + return $body; + }); + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json b/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json index fcd96e1edd36b..5d124d1035259 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json +++ b/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json @@ -33,5 +33,6 @@ "@kbn/tooling-log", "@kbn/security-solution-serverless", "@kbn/dev-utils", + "@kbn/spaces-plugin", ] } diff --git a/x-pack/test/defend_workflows_cypress/serverless_config.ts b/x-pack/test/defend_workflows_cypress/serverless_config.ts index 0434c13cd2392..38c9c5040e8d3 100644 --- a/x-pack/test/defend_workflows_cypress/serverless_config.ts +++ b/x-pack/test/defend_workflows_cypress/serverless_config.ts @@ -28,8 +28,14 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { esTestCluster: { ...config.esTestCluster, - serverArgs: [...config.esTestCluster.serverArgs, 'http.host=0.0.0.0'], + serverArgs: [ + ...config.esTestCluster.serverArgs, + 'http.host=0.0.0.0', + // Enable custom roles + 'xpack.security.authc.native_roles.enabled=true', + ], }, + esServerlessOptions: { ...(config.esServerlessOptions ?? {}), resources: Object.values(ES_RESOURCES), @@ -52,6 +58,12 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { 'servers.elasticsearch.port' )}`, + // Enable spaces UI capabilities + '--xpack.spaces.maxSpaces=100', + + // Enable UI to create custom roles in kibana + `--xpack.security.roleManagementEnabled=true`, + // Enable Fleet server standalone so that no checks are done to see if fleet-server has // registered with Kibana and we are able to access the Agents page of Fleet '--xpack.fleet.internal.fleetServerStandalone=true',