From faa71ccd6b512a1af8efd9014cf9dc247e57c768 Mon Sep 17 00:00:00 2001 From: Umberto Pepato Date: Fri, 6 Oct 2023 18:01:18 +0200 Subject: [PATCH] [RAM] Enable read-only users to access rules (#167003) Closes #166613 ## Summary Enables users with read privilege on `Stack rules` to see the rules table and the rule detail page without editing capabilities. Before: ![image](https://github.com/elastic/kibana/assets/18363145/a377819f-b140-41a4-aad9-9f5a5d779d63) After: ![image](https://github.com/elastic/kibana/assets/18363145/0c1bbf08-0f7b-4778-ae3e-062b0bd49c8f) In case of empty rules lists, the prompt "Create your first rule" was shown, even to users without create permissions: ![image](https://github.com/elastic/kibana/assets/18363145/3beaadb5-7b54-473e-9daa-e96c79e5a9f4) To avoid confusion, read-only users now see the empty table instead: ![image](https://github.com/elastic/kibana/assets/18363145/f35d212a-bc3e-4ee6-a3af-605966efced9) In the rule detail page, users without access to `Actions and Connectors` now see a missing privileges message under `Actions` in the details panel instead of `No actions` and a `Forbidden` error toast. ![image](https://github.com/elastic/kibana/assets/18363145/7d3edeae-3cd0-44c5-be2b-57ba4b54f69e) Finally, the original missing authorization prompt now shows "read" instead of "create": ![image](https://github.com/elastic/kibana/assets/18363145/f88b492d-3b20-41ac-a7c7-3cdd7f971ee5) ## To test - Create an Elasticsearch query rule - Create a Role with read privilege granted in `Stack rules` (under Kibana > Management) and assign it to a user - Create a test user with the created role - Log in as the test user - Navigate to Stack Management > Rules - Check that the rules table is visible, with create and update actions disabled ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) --------- Co-authored-by: Xavier Mouligneau --- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../prompts/no_permission_prompt.tsx | 4 +- .../hooks/use_load_rule_types_query.ts | 4 ++ .../hooks/use_rules_list_ui_state.ts | 7 +++- .../components/rule_definition.test.tsx | 1 + .../components/rule_definition.tsx | 27 +++++++++--- .../rules_list/components/rules_list.tsx | 2 + .../apps/observability/pages/rules_page.ts | 42 +++++++++++++++++-- 10 files changed, 74 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index c1f3af869cb78..7dcddfb492d0d 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -38306,7 +38306,6 @@ "xpack.triggersActionsUI.sections.rulesList.monthsLabel": "mois", "xpack.triggersActionsUI.sections.rulesList.multipleTitle": "règles", "xpack.triggersActionsUI.sections.rulesList.noPermissionToCreateDescription": "Contactez votre administrateur système.", - "xpack.triggersActionsUI.sections.rulesList.noPermissionToCreateTitle": "Aucune autorisation pour créer des règles", "xpack.triggersActionsUI.sections.rulesList.previousSnooze": "Précédent", "xpack.triggersActionsUI.sections.rulesList.refreshRulesButtonLabel": "Actualiser", "xpack.triggersActionsUI.sections.rulesList.remainingSnoozeIndefinite": "Indéfiniment", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 18492c43f4216..fcdea36b11678 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -38297,7 +38297,6 @@ "xpack.triggersActionsUI.sections.rulesList.monthsLabel": "月", "xpack.triggersActionsUI.sections.rulesList.multipleTitle": "ルール", "xpack.triggersActionsUI.sections.rulesList.noPermissionToCreateDescription": "システム管理者にお問い合わせください。", - "xpack.triggersActionsUI.sections.rulesList.noPermissionToCreateTitle": "ルールを作成する権限がありません", "xpack.triggersActionsUI.sections.rulesList.previousSnooze": "前へ", "xpack.triggersActionsUI.sections.rulesList.refreshRulesButtonLabel": "更新", "xpack.triggersActionsUI.sections.rulesList.remainingSnoozeIndefinite": "永久", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 296b107b99e5c..a8f2091c05fde 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -38291,7 +38291,6 @@ "xpack.triggersActionsUI.sections.rulesList.monthsLabel": "个月", "xpack.triggersActionsUI.sections.rulesList.multipleTitle": "规则", "xpack.triggersActionsUI.sections.rulesList.noPermissionToCreateDescription": "请联系您的系统管理员。", - "xpack.triggersActionsUI.sections.rulesList.noPermissionToCreateTitle": "没有创建规则的权限", "xpack.triggersActionsUI.sections.rulesList.previousSnooze": "上一步", "xpack.triggersActionsUI.sections.rulesList.refreshRulesButtonLabel": "刷新", "xpack.triggersActionsUI.sections.rulesList.remainingSnoozeIndefinite": "无限期", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/no_permission_prompt.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/no_permission_prompt.tsx index 9063617e90f69..49c277adbf08c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/no_permission_prompt.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/no_permission_prompt.tsx @@ -16,8 +16,8 @@ export const NoPermissionPrompt = () => ( title={

} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_types_query.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_types_query.ts index d9b1f6080c18f..4892341e57385 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_types_query.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_types_query.ts @@ -64,6 +64,9 @@ export const useLoadRuleTypesQuery = (props: UseLoadRuleTypesQueryProps) => { const authorizedToCreateAnyRules = authorizedRuleTypes.some( (ruleType) => ruleType.authorizedConsumers[ALERTS_FEATURE_ID]?.all ); + const authorizedToReadAnyRules = + authorizedToCreateAnyRules || + authorizedRuleTypes.some((ruleType) => ruleType.authorizedConsumers[ALERTS_FEATURE_ID]?.read); return { ruleTypesState: { @@ -73,6 +76,7 @@ export const useLoadRuleTypesQuery = (props: UseLoadRuleTypesQueryProps) => { }, hasAnyAuthorizedRuleType, authorizedRuleTypes, + authorizedToReadAnyRules, authorizedToCreateAnyRules, isSuccess, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_rules_list_ui_state.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_rules_list_ui_state.ts index 95ad83ba1f1cb..0f1c86240a248 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_rules_list_ui_state.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_rules_list_ui_state.ts @@ -9,6 +9,7 @@ import { isEmpty } from 'lodash'; import { RulesListFilters } from '../../types'; interface UseUiProps { + authorizedToReadAnyRules: boolean; authorizedToCreateAnyRules: boolean; filters: RulesListFilters; hasDefaultRuleTypesFiltersOn: boolean; @@ -37,6 +38,7 @@ const getFilterApplied = ({ hasEmptyTypesFilter, filters }: GetFilterAppliedProp }; export const useRulesListUiState = ({ + authorizedToReadAnyRules, authorizedToCreateAnyRules, filters, hasDefaultRuleTypesFiltersOn, @@ -56,8 +58,9 @@ export const useRulesListUiState = ({ const isInitialLoading = isInitialLoadingRuleTypes || isInitialLoadingRules; const isLoading = isLoadingRuleTypes || isLoadingRules; - const showNoAuthPrompt = !isInitialLoadingRuleTypes && !authorizedToCreateAnyRules; - const showCreateFirstRulePrompt = !isLoading && !hasData && !isFilterApplied; + const showNoAuthPrompt = !isInitialLoadingRuleTypes && !authorizedToReadAnyRules; + const showCreateFirstRulePrompt = + !isLoading && !hasData && !isFilterApplied && authorizedToCreateAnyRules; const showSpinner = isInitialLoading && (isLoadingRuleTypes || (!showNoAuthPrompt && isLoadingRules)); const showRulesList = !showSpinner && !showCreateFirstRulePrompt && !showNoAuthPrompt; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.test.tsx index 148d0994fc497..842822f98c43f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.test.tsx @@ -23,6 +23,7 @@ jest.mock('./rule_actions', () => ({ jest.mock('../../../lib/capabilities', () => ({ hasAllPrivilege: jest.fn(() => true), hasSaveRulesCapability: jest.fn(() => true), + hasShowActionsCapability: jest.fn(() => true), hasExecuteActionsCapability: jest.fn(() => true), hasManageApiKeysCapability: jest.fn(() => true), })); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.tsx index 6c7f133da753f..721118b1d6f0e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.tsx @@ -21,7 +21,11 @@ import { formatDuration } from '@kbn/alerting-plugin/common'; import { RuleDefinitionProps } from '../../../../types'; import { RuleType, useLoadRuleTypes } from '../../../..'; import { useKibana } from '../../../../common/lib/kibana'; -import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities'; +import { + hasAllPrivilege, + hasExecuteActionsCapability, + hasShowActionsCapability, +} from '../../../lib/capabilities'; import { RuleActions } from './rule_actions'; import { RuleEdit } from '../../rule_form'; @@ -60,6 +64,7 @@ export const RuleDefinition: React.FunctionComponent = ({ values: { numberOfConditions }, }); }; + const canReadActions = hasShowActionsCapability(capabilities); const canExecuteActions = hasExecuteActionsCapability(capabilities); const canSaveRule = rule && @@ -209,11 +214,21 @@ export const RuleDefinition: React.FunctionComponent = ({ })} - + {canReadActions ? ( + + ) : ( + + + {i18n.translate('xpack.triggersActionsUI.ruleDetails.cannotReadActions', { + defaultMessage: 'Connector feature privileges are required to view actions', + })} + + + )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index 83285163aeee3..9949b51554492 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -243,6 +243,7 @@ export const RulesList = ({ ruleTypesState, hasAnyAuthorizedRuleType, authorizedRuleTypes, + authorizedToReadAnyRules, authorizedToCreateAnyRules, isSuccess: isLoadRuleTypesSuccess, } = useLoadRuleTypesQuery({ filteredRuleTypes }); @@ -285,6 +286,7 @@ export const RulesList = ({ }); const { showSpinner, showRulesList, showNoAuthPrompt, showCreateFirstRulePrompt } = useUiState({ + authorizedToReadAnyRules, authorizedToCreateAnyRules, filters, hasDefaultRuleTypesFiltersOn, diff --git a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts index e912a52d36a18..e9b87e203fad8 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts @@ -297,9 +297,42 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { 'Create rule button', async () => await testSubjects.exists('createRuleButton') ); + await retry.waitFor( + 'Create rule button is enabled', + async () => await testSubjects.isEnabled('createRuleButton') + ); }); it(`shows the no permission prompt when the user has no permissions`, async () => { + // We kept this test to make sure that the stack management rule page + // is showing the right prompt corresponding to the right privileges. + // Knowing that o11y alert page won't come up if you do not have any + // kind of privileges to o11y + await observability.users.setTestUserRole({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: [], + feature: { + discover: ['read'], + }, + spaces: ['*'], + }, + ], + }); + await observability.alerts.common.navigateToRulesPage(); + await retry.waitFor( + 'No permissions prompt', + async () => await testSubjects.exists('noPermissionPrompt') + ); + await observability.users.restoreDefaultTestUserRole(); + }); + + it(`shows the rules list in read-only mode when the user only has read permissions`, async () => { await observability.users.setTestUserRole( observability.users.defineBasicObservabilityRole({ logs: ['read'], @@ -307,10 +340,13 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { ); await observability.alerts.common.navigateToRulesPage(); await retry.waitFor( - 'No permissions prompt', - async () => await testSubjects.exists('noPermissionPrompt') + 'Read-only rules list is visible', + async () => await testSubjects.exists('rulesList') + ); + await retry.waitFor( + 'Create rule button is disabled', + async () => !(await testSubjects.isEnabled('createRuleButton')) ); - await observability.users.restoreDefaultTestUserRole(); }); });