[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](a377819f-b140-41a4-aad9-9f5a5d779d63)

After:


![image](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](3beaadb5-7b54-473e-9daa-e96c79e5a9f4)

To avoid confusion, read-only users now see the empty table instead:


![image](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](7d3edeae-3cd0-44c5-be2b-57ba4b54f69e)

Finally, the original missing authorization prompt now shows "read"
instead of "create":


![image](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 <xavier.mouligneau@elastic.co>
This commit is contained in:
Umberto Pepato 2023-10-06 18:01:18 +02:00 committed by GitHub
parent ea0a1a073e
commit e49628acab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 74 additions and 16 deletions

View file

@ -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",

View file

@ -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": "永久",

View file

@ -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": "无限期",

View file

@ -16,8 +16,8 @@ export const NoPermissionPrompt = () => (
title={
<h1>
<FormattedMessage
id="xpack.triggersActionsUI.sections.rulesList.noPermissionToCreateTitle"
defaultMessage="No permissions to create rules"
id="xpack.triggersActionsUI.sections.rulesList.noPermissionToReadTitle"
defaultMessage="No permissions to read rules"
/>
</h1>
}

View file

@ -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,
};

View file

@ -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;

View file

@ -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),
}));

View file

@ -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<RuleDefinitionProps> = ({
values: { numberOfConditions },
});
};
const canReadActions = hasShowActionsCapability(capabilities);
const canExecuteActions = hasExecuteActionsCapability(capabilities);
const canSaveRule =
rule &&
@ -209,11 +214,21 @@ export const RuleDefinition: React.FunctionComponent<RuleDefinitionProps> = ({
})}
</ItemTitleRuleSummary>
<EuiFlexItem grow={3}>
<RuleActions
ruleActions={rule.actions}
actionTypeRegistry={actionTypeRegistry}
legacyNotifyWhen={rule.notifyWhen}
/>
{canReadActions ? (
<RuleActions
ruleActions={rule.actions}
actionTypeRegistry={actionTypeRegistry}
legacyNotifyWhen={rule.notifyWhen}
/>
) : (
<EuiFlexItem>
<EuiText size="s">
{i18n.translate('xpack.triggersActionsUI.ruleDetails.cannotReadActions', {
defaultMessage: 'Connector feature privileges are required to view actions',
})}
</EuiText>
</EuiFlexItem>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>

View file

@ -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,

View file

@ -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();
});
});