[Security Solution][Entity Analytics] More detailed privilege control for Asset Criticality assignment (#176333)

## Summary

This PR fixes a bug where users with `read` access and without `write`
access would not be able to see the Criticality assigned to an entity.
The criticality component would only check whether a user has _all_ the
required privileges, which didn't account for read-only case.

By adding more granular and detailed information to the privileges API
call, we can now show the currently assigned criticality but hide the
`Change/Create` button if the user does not have `write` access.


00845ac7-11f4-429c-990b-c068dfb19f6b



### How to test

1. Login in as a `superuser`
2. Assign criticality to some entity via the expandable flyout.
3. Create a role with read-only/read-write or no access to the
criticality index

<img width="1352" alt="Screenshot 2024-01-31 at 09 58 04"
src="22477433-63e8-4ccf-a077-275725a600aa">

4. Create a user(s) with the new role(s)
5. Login with that user and open the expandable flyout
6. Check that the criticality is only shown for users with `read`
privilege and that the update button only shows for users with `write`
privileges







### Checklist

Delete any items that are not applicable to this PR.
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
This commit is contained in:
Tiago Vila Verde 2024-02-09 12:33:20 +01:00 committed by GitHub
parent 2c0fd46961
commit 5ff2f987a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 117 additions and 37 deletions

View file

@ -19,6 +19,8 @@ import { z } from 'zod';
export type EntityAnalyticsPrivileges = z.infer<typeof EntityAnalyticsPrivileges>;
export const EntityAnalyticsPrivileges = z.object({
has_all_required: z.boolean(),
has_read_permissions: z.boolean().optional(),
has_write_permissions: z.boolean().optional(),
privileges: z.object({
elasticsearch: z.object({
cluster: z

View file

@ -3,7 +3,7 @@ info:
title: Entity Analytics Common Schema
description: Common schema for Entity Analytics
version: 1.0.0
paths: { }
paths: {}
components:
schemas:
EntityAnalyticsPrivileges:
@ -11,6 +11,10 @@ components:
properties:
has_all_required:
type: boolean
has_read_permissions:
type: boolean
has_write_permissions:
type: boolean
privileges:
type: object
properties:
@ -20,19 +24,19 @@ components:
cluster:
type: object
properties:
manage_index_templates:
type: boolean
manage_transform:
type: boolean
manage_index_templates:
type: boolean
manage_transform:
type: boolean
index:
type: object
additionalProperties:
type: object
properties:
read:
type: boolean
write:
type: boolean
type: object
properties:
read:
type: boolean
write:
type: boolean
required:
- elasticsearch
required:
@ -176,8 +180,6 @@ components:
items:
$ref: '#/components/schemas/RiskScoreInput'
RiskScoreWeight:
description: "Configuration used to tune risk scoring. Weights can be used to change the score contribution of risk inputs for hosts and users at both a global level and also for Risk Input categories (e.g. 'category_1')."
type: object

View file

@ -44,7 +44,7 @@ const AssetCriticalityComponent: React.FC<Props> = ({ entity }) => {
const criticality = useAssetCriticalityData(entity, modal);
const { euiTheme } = useEuiTheme();
if (criticality.privileges.isLoading || !criticality.privileges.data?.has_all_required) {
if (criticality.privileges.isLoading || !criticality.privileges.data?.has_read_permissions) {
return null;
}
@ -87,27 +87,29 @@ const AssetCriticalityComponent: React.FC<Props> = ({ entity }) => {
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem css={{ flexGrow: 'unset' }}>
<EuiButtonEmpty
data-test-subj="asset-criticality-change-btn"
iconType="arrowStart"
iconSide="left"
flush="right"
onClick={() => modal.toggle(true)}
>
{criticality.status === 'update' ? (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.assetCriticality.changeButton"
defaultMessage="Change"
/>
) : (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.assetCriticality.createButton"
defaultMessage="Create"
/>
)}
</EuiButtonEmpty>
</EuiFlexItem>
{criticality.privileges.data?.has_write_permissions && (
<EuiFlexItem css={{ flexGrow: 'unset' }}>
<EuiButtonEmpty
data-test-subj="asset-criticality-change-btn"
iconType="arrowStart"
iconSide="left"
flush="right"
onClick={() => modal.toggle(true)}
>
{criticality.status === 'update' ? (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.assetCriticality.changeButton"
defaultMessage="Change"
/>
) : (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.assetCriticality.createButton"
defaultMessage="Create"
/>
)}
</EuiButtonEmpty>
</EuiFlexItem>
)}
</EuiFlexGroup>
)}
</EuiAccordion>

View file

@ -40,7 +40,7 @@ export const useAssetCriticalityData = (entity: Entity, modal: ModalState): Stat
queryKey: QUERY_KEYS.doc,
queryFn: () => fetchAssetCriticality({ idField: `${entity.type}.name`, idValue: entity.name }),
retry: (failureCount, error) => error.body.statusCode === 404 && failureCount > 0,
enabled: privileges.data?.has_all_required,
enabled: !!privileges.data?.has_read_permissions,
});
const mutation = useMutation({

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import { _formatPrivileges } from './check_and_format_privileges';
import { ASSET_CRITICALITY_INDEX_PATTERN } from '../../../../common/entity_analytics/asset_criticality/constants';
import { _formatPrivileges, hasReadWritePermissions } from './check_and_format_privileges';
describe('_formatPrivileges', () => {
it('should correctly format elasticsearch index privileges', () => {
@ -146,4 +147,60 @@ describe('_formatPrivileges', () => {
},
});
});
it('should correctly extract read and write permissions from elasticsearch cluster privileges', () => {
const privileges = {
elasticsearch: {
cluster: [
{
privilege: 'read',
authorized: true,
},
{
privilege: 'write',
authorized: false,
},
],
index: {},
},
kibana: [],
};
const result = hasReadWritePermissions(privileges.elasticsearch);
expect(result).toEqual({
has_read_permissions: true,
has_write_permissions: false,
});
});
it('should correctly extract read and write permissions from elasticsearch index privileges', () => {
const privileges = {
elasticsearch: {
cluster: [],
index: {
[ASSET_CRITICALITY_INDEX_PATTERN]: [
{
privilege: 'read',
authorized: true,
},
{
privilege: 'write',
authorized: false,
},
],
},
},
kibana: [],
};
const result = hasReadWritePermissions(
privileges.elasticsearch,
ASSET_CRITICALITY_INDEX_PATTERN
);
expect(result).toEqual({
has_read_permissions: true,
has_write_permissions: false,
});
});
});

View file

@ -11,6 +11,7 @@ import type {
CheckPrivilegesResponse,
SecurityPluginStart,
} from '@kbn/security-plugin/server';
import { ASSET_CRITICALITY_INDEX_PATTERN } from '../../../../common/entity_analytics/asset_criticality/constants';
import type { EntityAnalyticsPrivileges } from '../../../../common/api/entity_analytics/common';
const groupPrivilegesByName = <PrivilegeName extends string>(
privileges: Array<{
@ -69,5 +70,21 @@ export async function checkAndFormatPrivileges({
return {
privileges: _formatPrivileges(privileges),
has_all_required: hasAllRequested,
...hasReadWritePermissions(privileges.elasticsearch, ASSET_CRITICALITY_INDEX_PATTERN),
};
}
export const hasReadWritePermissions = (
{ index, cluster }: CheckPrivilegesResponse['privileges']['elasticsearch'],
indexKey = ''
) => {
const has =
(type: string) =>
({ privilege, authorized }: { privilege: string; authorized: boolean }) =>
privilege === type && authorized;
return {
has_read_permissions: index[indexKey]?.some(has('read')) || cluster.some(has('read')),
has_write_permissions: index[indexKey]?.some(has('write')) || cluster.some(has('write')),
};
};