Skip to content

Commit

Permalink
[RAM] Enable read-only users to access rules (elastic#167003)
Browse files Browse the repository at this point in the history
Closes elastic#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 <[email protected]>
  • Loading branch information
2 people authored and dej611 committed Oct 17, 2023
1 parent 80c575f commit faa71cc
Show file tree
Hide file tree
Showing 10 changed files with 74 additions and 16 deletions.
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "永久",
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "无限期",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -73,6 +76,7 @@ export const useLoadRuleTypesQuery = (props: UseLoadRuleTypesQueryProps) => {
},
hasAnyAuthorizedRuleType,
authorizedRuleTypes,
authorizedToReadAnyRules,
authorizedToCreateAnyRules,
isSuccess,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { isEmpty } from 'lodash';
import { RulesListFilters } from '../../types';

interface UseUiProps {
authorizedToReadAnyRules: boolean;
authorizedToCreateAnyRules: boolean;
filters: RulesListFilters;
hasDefaultRuleTypesFiltersOn: boolean;
Expand Down Expand Up @@ -37,6 +38,7 @@ const getFilterApplied = ({ hasEmptyTypesFilter, filters }: GetFilterAppliedProp
};

export const useRulesListUiState = ({
authorizedToReadAnyRules,
authorizedToCreateAnyRules,
filters,
hasDefaultRuleTypesFiltersOn,
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -60,6 +64,7 @@ export const RuleDefinition: React.FunctionComponent<RuleDefinitionProps> = ({
values: { numberOfConditions },
});
};
const canReadActions = hasShowActionsCapability(capabilities);
const canExecuteActions = hasExecuteActionsCapability(capabilities);
const canSaveRule =
rule &&
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ export const RulesList = ({
ruleTypesState,
hasAnyAuthorizedRuleType,
authorizedRuleTypes,
authorizedToReadAnyRules,
authorizedToCreateAnyRules,
isSuccess: isLoadRuleTypesSuccess,
} = useLoadRuleTypesQuery({ filteredRuleTypes });
Expand Down Expand Up @@ -285,6 +286,7 @@ export const RulesList = ({
});

const { showSpinner, showRulesList, showNoAuthPrompt, showCreateFirstRulePrompt } = useUiState({
authorizedToReadAnyRules,
authorizedToCreateAnyRules,
filters,
hasDefaultRuleTypesFiltersOn,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,20 +297,56 @@ 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'],
})
);
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();
});
});
Expand Down

0 comments on commit faa71cc

Please sign in to comment.