mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
2c0fd46961
commit
5ff2f987a5
6 changed files with 117 additions and 37 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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')),
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue