Skip to content

Commit

Permalink
[SecuritySolution] Check user permissions before initialising entity …
Browse files Browse the repository at this point in the history
…engine (elastic#198661)

## Summary

* Create privileges API for the Entity Store
* Create missing privileges callout
* Add missing Entity Store privileges callout to Entity Store 
* Add missing Entity Store privileges callout to Dashboard

![Screenshot 2024-11-04 at 15 57
15](https://github.com/user-attachments/assets/ed013571-4f0d-4605-bd2a-faa5ad3ac3e6)
![Screenshot 2024-11-04 at 16 16
03](https://github.com/user-attachments/assets/4cf6cf7d-a8c1-4c96-8fd1-2bf8be9f785e)



https://github.com/user-attachments/assets/30cdb096-24cd-4a1c-a20b-abbbece865d7

### Update:

I added a "Line clamp" and "Read More" button as requested by Mark:
![Screenshot 2024-11-05 at 13 15
51](https://github.com/user-attachments/assets/42fbec93-e258-49af-8acc-ae18314be442)


### 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)
- [x] [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
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
  • Loading branch information
machadoum authored Nov 6, 2024
1 parent 36f6d6f commit 0e3b83b
Show file tree
Hide file tree
Showing 27 changed files with 565 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,10 @@ export const EntityAnalyticsPrivileges = z.object({
has_write_permissions: z.boolean().optional(),
privileges: z.object({
elasticsearch: z.object({
cluster: z
.object({
manage_index_templates: z.boolean().optional(),
manage_transform: z.boolean().optional(),
})
.optional(),
index: z
.object({})
.catchall(
z.object({
read: z.boolean().optional(),
write: z.boolean().optional(),
})
)
.optional(),
cluster: z.object({}).catchall(z.boolean()).optional(),
index: z.object({}).catchall(z.object({}).catchall(z.boolean())).optional(),
}),
kibana: z.object({}).catchall(z.boolean()).optional(),
}),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,18 @@ components:
properties:
cluster:
type: object
properties:
manage_index_templates:
type: boolean
manage_transform:
type: boolean
additionalProperties:
type: boolean
index:
type: object
additionalProperties:
type: object
properties:
read:
type: boolean
write:
type: boolean
additionalProperties:
type: boolean
kibana:
type: object
additionalProperties:
type: boolean
required:
- elasticsearch
required:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*
* info:
* title: Get Entity Store Privileges Schema
* version: 1
*/

import type { z } from '@kbn/zod';

import { EntityAnalyticsPrivileges } from '../../common/common.gen';

export type EntityStoreGetPrivilegesResponse = z.infer<typeof EntityStoreGetPrivilegesResponse>;
export const EntityStoreGetPrivilegesResponse = EntityAnalyticsPrivileges;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
openapi: 3.0.0
info:
title: Get Entity Store Privileges Schema
version: '1'
paths:
/internal/entity_store/privileges:
get:
x-labels: [ess, serverless]
x-internal: true
x-codegen-enabled: true
operationId: EntityStoreGetPrivileges
summary: Get Entity Store Privileges
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '../../common/common.schema.yaml#/components/schemas/EntityAnalyticsPrivileges'
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ import type {
DeleteEntityEngineRequestParamsInput,
DeleteEntityEngineResponse,
} from './entity_analytics/entity_store/engine/delete.gen';
import type { EntityStoreGetPrivilegesResponse } from './entity_analytics/entity_store/engine/get_privileges.gen';
import type {
GetEntityEngineRequestParamsInput,
GetEntityEngineResponse,
Expand Down Expand Up @@ -1119,6 +1120,18 @@ If a record already exists for the specified entity, that record is overwritten
})
.catch(catchAxiosErrorFormatAndThrow);
}
async entityStoreGetPrivileges() {
this.log.info(`${new Date().toISOString()} Calling API EntityStoreGetPrivileges`);
return this.kbnClient
.request<EntityStoreGetPrivilegesResponse>({
path: '/internal/entity_store/privileges',
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '1',
},
method: 'GET',
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Export detection rules to an `.ndjson` file. The following configuration items are also included in the `.ndjson` file:
- Actions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@
*/

export const ENTITY_STORE_URL = '/api/entity_store' as const;
export const ENTITY_STORE_INTERNAL_PRIVILEGES_URL = `${ENTITY_STORE_URL}/privileges` as const;
export const ENTITIES_URL = `${ENTITY_STORE_URL}/entities` as const;

export const LIST_ENTITIES_URL = `${ENTITIES_URL}/list` as const;

export const ENTITY_STORE_REQUIRED_ES_CLUSTER_PRIVILEGES = [
'manage_index_templates',
'manage_transform',
'manage_ingest_pipelines',
'manage_enrich',
];

// The index pattern for the entity store has to support '.entities.v1.latest.noop' index
export const ENTITY_STORE_INDEX_PATTERN = '.entities.v1.latest.*';
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { getAllMissingPrivileges } from './privileges';
import type { EntityAnalyticsPrivileges } from '../api/entity_analytics';

describe('getAllMissingPrivileges', () => {
it('should return all missing privileges for elasticsearch and kibana', () => {
const privileges: EntityAnalyticsPrivileges = {
privileges: {
elasticsearch: {
index: {
'logs-*': { read: true, view_index_metadata: true },
'auditbeat-*': { read: false, view_index_metadata: false },
},
cluster: {
manage_enrich: false,
manage_ingest_pipelines: true,
},
},
kibana: {
'saved_object:entity-engine-status/all': false,
'saved_object:entity-definition/all': true,
},
},
has_all_required: false,
has_read_permissions: false,
has_write_permissions: false,
};

const result = getAllMissingPrivileges(privileges);

expect(result).toEqual({
elasticsearch: {
index: [{ indexName: 'auditbeat-*', privileges: ['read', 'view_index_metadata'] }],
cluster: ['manage_enrich'],
},
kibana: ['saved_object:entity-engine-status/all'],
});
});

it('should return empty lists if all privileges are true', () => {
const privileges: EntityAnalyticsPrivileges = {
privileges: {
elasticsearch: {
index: {
'logs-*': { read: true, view_index_metadata: true },
},
cluster: {
manage_enrich: true,
},
},
kibana: {
'saved_object:entity-engine-status/all': true,
},
},
has_all_required: true,
has_read_permissions: true,
has_write_permissions: true,
};

const result = getAllMissingPrivileges(privileges);

expect(result).toEqual({
elasticsearch: {
index: [],
cluster: [],
},
kibana: [],
});
});

it('should handle empty privileges object', () => {
const privileges: EntityAnalyticsPrivileges = {
privileges: {
elasticsearch: {
index: {},
cluster: {},
},
kibana: {},
},
has_all_required: false,
has_read_permissions: false,
has_write_permissions: false,
};

const result = getAllMissingPrivileges(privileges);

expect(result).toEqual({
elasticsearch: {
index: [],
cluster: [],
},
kibana: [],
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { EntityAnalyticsPrivileges } from '../api/entity_analytics';

export const getAllMissingPrivileges = (privilege: EntityAnalyticsPrivileges) => {
const esPrivileges = privilege.privileges.elasticsearch;
const kbnPrivileges = privilege.privileges.kibana;

const index = Object.entries(esPrivileges.index ?? {})
.map(([indexName, indexPrivileges]) => ({
indexName,
privileges: filterUnauthorized(indexPrivileges),
}))
.filter(({ privileges }) => privileges.length > 0);

return {
elasticsearch: { index, cluster: filterUnauthorized(esPrivileges.cluster) },
kibana: filterUnauthorized(kbnPrivileges),
};
};

const filterUnauthorized = (obj: Record<string, boolean> | undefined) =>
Object.entries(obj ?? {})
.filter(([_, authorized]) => !authorized)
.map(([privileges, _]) => privileges);
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,42 @@ import { useIsOverflow } from '../../hooks/use_is_overflow';
import * as i18n from './translations';

const LINE_CLAMP = 3;
const LINE_CLAMP_HEIGHT = 5.5;
const LINE_CLAMP_HEIGHT = '5.5em';
const MAX_HEIGHT = '33vh';

const ReadMore = styled(EuiButtonEmpty)`
span.euiButtonContent {
padding: 0;
}
`;

const ExpandedContent = styled.div`
max-height: 33vh;
const ExpandedContent = styled.div<{ maxHeight: string }>`
max-height: ${({ maxHeight }) => maxHeight};
overflow-wrap: break-word;
overflow-x: hidden;
overflow-y: auto;
`;

const StyledLineClamp = styled.div<{ lineClampHeight: number }>`
const StyledLineClamp = styled.div<{ lineClampHeight: string; lineClamp: number }>`
display: -webkit-box;
-webkit-line-clamp: ${LINE_CLAMP};
-webkit-line-clamp: ${({ lineClamp }) => lineClamp};
-webkit-box-orient: vertical;
overflow: hidden;
max-height: ${({ lineClampHeight }) => lineClampHeight}em;
height: ${({ lineClampHeight }) => lineClampHeight}em;
max-height: ${({ lineClampHeight }) => lineClampHeight};
height: ${({ lineClampHeight }) => lineClampHeight};
`;

const LineClampComponent: React.FC<{
children: ReactNode;
lineClampHeight?: number;
}> = ({ children, lineClampHeight = LINE_CLAMP_HEIGHT }) => {
lineClampHeight?: string;
lineClamp?: number;
maxHeight?: string;
}> = ({
children,
lineClampHeight = LINE_CLAMP_HEIGHT,
lineClamp = LINE_CLAMP,
maxHeight = MAX_HEIGHT,
}) => {
const [isExpanded, setIsExpanded] = useState<boolean | null>(null);
const [isOverflow, descriptionRef] = useIsOverflow(children);

Expand All @@ -51,7 +59,7 @@ const LineClampComponent: React.FC<{
if (isExpanded) {
return (
<>
<ExpandedContent data-test-subj="expanded-line-clamp">
<ExpandedContent maxHeight={maxHeight} data-test-subj="expanded-line-clamp">
<p>{children}</p>
</ExpandedContent>
{isOverflow && (
Expand All @@ -70,6 +78,7 @@ const LineClampComponent: React.FC<{
data-test-subj="styled-line-clamp"
ref={descriptionRef}
lineClampHeight={lineClampHeight}
lineClamp={lineClamp}
>
{children}
</StyledLineClamp>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
*/

import { useMemo } from 'react';
import { LIST_ENTITIES_URL } from '../../../common/entity_analytics/entity_store/constants';
import {
ENTITY_STORE_INTERNAL_PRIVILEGES_URL,
LIST_ENTITIES_URL,
} from '../../../common/entity_analytics/entity_store/constants';
import type { UploadAssetCriticalityRecordsResponse } from '../../../common/api/entity_analytics/asset_criticality/upload_asset_criticality_csv.gen';
import type { DisableRiskEngineResponse } from '../../../common/api/entity_analytics/risk_engine/engine_disable_route.gen';
import type { RiskEngineStatusResponse } from '../../../common/api/entity_analytics/risk_engine/engine_status_route.gen';
Expand Down Expand Up @@ -172,6 +175,15 @@ export const useEntityAnalyticsRoutes = () => {
method: 'GET',
});

/**
* Get Entity Store privileges
*/
const fetchEntityStorePrivileges = () =>
http.fetch<EntityAnalyticsPrivileges>(ENTITY_STORE_INTERNAL_PRIVILEGES_URL, {
version: '1',
method: 'GET',
});

/**
* Create asset criticality
*/
Expand Down Expand Up @@ -295,6 +307,7 @@ export const useEntityAnalyticsRoutes = () => {
scheduleNowRiskEngine,
fetchRiskEnginePrivileges,
fetchAssetCriticalityPrivileges,
fetchEntityStorePrivileges,
createAssetCriticality,
deleteAssetCriticality,
fetchAssetCriticality,
Expand Down
Loading

0 comments on commit 0e3b83b

Please sign in to comment.