Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is outside the scope of this ticket but it's weird to me that we're using OpenAPI to type the privileges object, when we already have TS types for it on kibana side, right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but from what I know, we can't reuse TS types inside OpenAPi schemas. 😞

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