Skip to content

Commit 6ab5523

Browse files
authored
[SecuritySolution] Add index privileges check to applyDataViewIndices (#214803)
## Summary Add a new privileges check before executing `applyDataViewIndices`. This change impacts the API call `applyDataViewIndices` and the job. `applyDataViewIndices` updates the transforms. Executing without privileges generates a silence error because the transform can't run. I also added some extra unit tests for `applyDataViewIndices`. Required privileges ['read', 'view_index_metadata'] for all security solution dataview + asset_criticality and risk_score indices. ### How to test it 1. **API call with unprivileged user scenario** * Enable the entity store with a superuser * Create an unprivileged user * Call `POST kbn:api/entity_store/engines/apply_dataview_indices` * It should return an error * Add the required privileges * It executes successfully 2. **Task execution with an unprivileged user scenario** * Create a user and add privileges only for the required Entity Store indices * Login with the new user * Enable the entity store * Add a new index to the security data view (the new user shouldn't have access to the new index) * Wait for 30min for the job to run, or update the [source code](https://github.com/elastic/kibana/blob/8d0feb580f13cb1571beaf84a6d5763197211106/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/tasks/data_view_refresh/data_view_refresh_task.ts#L150) to make it run more often * The job execution should fail with an error message containing the new index name. ### Checklist Reviewers should verify this PR satisfies this list as well. - [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
1 parent 9238626 commit 6ab5523

File tree

6 files changed

+221
-21
lines changed

6 files changed

+221
-21
lines changed

x-pack/solutions/security/plugins/security_solution/common/entity_analytics/entity_store/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,8 @@ export const ENTITY_STORE_REQUIRED_ES_CLUSTER_PRIVILEGES = [
2121
'manage_enrich',
2222
];
2323

24+
// Privileges required for the transform to run
25+
export const ENTITY_STORE_SOURCE_REQUIRED_ES_INDEX_PRIVILEGES = ['read', 'view_index_metadata'];
26+
2427
// The index pattern for the entity store has to support '.entities.v1.latest.noop' index
2528
export const ENTITY_STORE_INDEX_PATTERN = '.entities.v1.latest.*';

x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts

Lines changed: 141 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import { EntityType } from '../../../../common/search_strategy';
2222
import type { InitEntityEngineResponse } from '../../../../common/api/entity_analytics';
2323
import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
2424
import { defaultOptions } from './constants';
25+
import type { SecurityPluginStart } from '@kbn/security-plugin/server';
26+
import type { KibanaRequest } from '@kbn/core/server';
27+
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
28+
import { createStubDataView } from '@kbn/data-views-plugin/common/mocks';
2529

2630
const definition: EntityDefinition = convertToEntityManagerDefinition(
2731
{
@@ -46,22 +50,73 @@ const definition: EntityDefinition = convertToEntityManagerDefinition(
4650
{ namespace: 'test', filter: '' }
4751
);
4852

53+
const stubSecurityDataView = createStubDataView({
54+
spec: {
55+
id: 'security',
56+
title: 'security',
57+
},
58+
});
59+
60+
const dataviewService = {
61+
...dataViewPluginMocks.createStartContract(),
62+
get: () => Promise.resolve(stubSecurityDataView),
63+
clearInstanceCache: () => Promise.resolve(),
64+
};
65+
66+
const mockGetEntityDefinition = jest.fn().mockResolvedValue([]);
67+
const mockUpdateEntityDefinition = jest.fn().mockResolvedValue(undefined);
68+
jest.mock('@kbn/entityManager-plugin/server/lib/entity_client', () => {
69+
return {
70+
EntityClient: jest.fn().mockImplementation(() => ({
71+
updateEntityDefinition: mockUpdateEntityDefinition,
72+
getEntityDefinitions: mockGetEntityDefinition,
73+
})),
74+
};
75+
});
76+
77+
const mockListDescriptor = jest.fn().mockResolvedValue({ engines: [] });
78+
const mockUpdateStatus = jest.fn().mockResolvedValue({});
79+
jest.mock('./saved_object/engine_descriptor', () => {
80+
return {
81+
EngineDescriptorClient: jest.fn().mockImplementation(() => ({
82+
list: mockListDescriptor,
83+
updateStatus: mockUpdateStatus,
84+
})),
85+
};
86+
});
87+
88+
const mockCheckPrivileges = jest.fn().mockReturnValue({
89+
hasAllRequested: true,
90+
privileges: {
91+
elasticsearch: { cluster: [], index: [] },
92+
kibana: [],
93+
},
94+
});
95+
4996
describe('EntityStoreDataClient', () => {
50-
const mockSavedObjectClient = savedObjectsClientMock.create();
5197
const clusterClientMock = elasticsearchServiceMock.createScopedClusterClient();
5298
const esClientMock = clusterClientMock.asCurrentUser;
5399
const loggerMock = loggingSystemMock.createLogger();
54100
const dataClient = new EntityStoreDataClient({
55101
clusterClient: clusterClientMock,
56102
logger: loggerMock,
57103
namespace: 'default',
58-
soClient: mockSavedObjectClient,
104+
soClient: savedObjectsClientMock.create(),
59105
kibanaVersion: '9.0.0',
60-
dataViewsService: {} as DataViewsService,
61-
appClient: {} as AppClient,
106+
dataViewsService: dataviewService as unknown as DataViewsService,
107+
appClient: {
108+
getSourcererDataViewId: jest.fn().mockReturnValue('security-solution'),
109+
getAlertsIndex: jest.fn().mockReturnValue('alerts'),
110+
} as unknown as AppClient,
62111
config: {} as EntityStoreConfig,
63112
experimentalFeatures: mockGlobalState.app.enableExperimental,
64113
taskManager: {} as TaskManagerStartContract,
114+
security: {
115+
authz: {
116+
checkPrivilegesDynamicallyWithRequest: () => mockCheckPrivileges,
117+
},
118+
} as unknown as SecurityPluginStart,
119+
request: {} as KibanaRequest,
65120
});
66121

67122
const defaultSearchParams = {
@@ -89,7 +144,7 @@ describe('EntityStoreDataClient', () => {
89144

90145
describe('search entities', () => {
91146
beforeEach(() => {
92-
jest.resetAllMocks();
147+
jest.clearAllMocks();
93148
esClientMock.search.mockResolvedValue(emptySearchResponse);
94149
});
95150

@@ -349,7 +404,7 @@ describe('EntityStoreDataClient', () => {
349404
let spyInit: jest.SpyInstance;
350405

351406
beforeEach(() => {
352-
jest.resetAllMocks();
407+
jest.clearAllMocks();
353408
spyInit = jest
354409
.spyOn(dataClient, 'init')
355410
.mockImplementation(() => Promise.resolve({} as InitEntityEngineResponse));
@@ -364,4 +419,84 @@ describe('EntityStoreDataClient', () => {
364419
expect(spyInit).toHaveBeenCalledWith(EntityType.host, expect.anything(), expect.anything());
365420
});
366421
});
422+
423+
describe('applyDataViewIndices', () => {
424+
beforeEach(() => {
425+
mockUpdateEntityDefinition.mockClear();
426+
jest.clearAllMocks();
427+
});
428+
429+
it('applies data view indices to the entity store', async () => {
430+
mockListDescriptor.mockResolvedValueOnce({ engines: [{}] });
431+
mockGetEntityDefinition.mockResolvedValueOnce({
432+
definitions: [definition],
433+
});
434+
435+
const response = await dataClient.applyDataViewIndices();
436+
437+
expect(mockUpdateEntityDefinition).toHaveBeenCalled();
438+
expect(response.errors.length).toBe(0);
439+
expect(response.successes.length).toBe(1);
440+
});
441+
442+
it('returns empty successes and errors if no engines found', async () => {
443+
mockListDescriptor.mockResolvedValueOnce({ engines: [] });
444+
445+
const response = await dataClient.applyDataViewIndices();
446+
447+
expect(response.successes.length).toBe(0);
448+
expect(response.errors.length).toBe(0);
449+
});
450+
451+
it('throws an error if the user does not have required privileges', async () => {
452+
mockCheckPrivileges.mockReturnValueOnce({
453+
hasAllRequested: false,
454+
privileges: {
455+
elasticsearch: { cluster: [], index: [] },
456+
kibana: [],
457+
},
458+
});
459+
460+
mockListDescriptor.mockResolvedValueOnce({ engines: [{}] });
461+
462+
await expect(dataClient.applyDataViewIndices()).rejects.toThrow(
463+
/The current user does not have the required indices privileges.*/
464+
);
465+
});
466+
467+
it('skips update if index patterns are the same', async () => {
468+
mockListDescriptor.mockResolvedValueOnce({ engines: [{}] });
469+
mockGetEntityDefinition.mockResolvedValueOnce({
470+
definitions: [
471+
{
472+
indexPatterns: [
473+
stubSecurityDataView.getIndexPattern(),
474+
'.asset-criticality.asset-criticality-default',
475+
'risk-score.risk-score-latest-default',
476+
],
477+
},
478+
],
479+
});
480+
481+
const response = await dataClient.applyDataViewIndices();
482+
483+
expect(mockUpdateEntityDefinition).not.toHaveBeenCalled();
484+
expect(response.successes.length).toBe(1);
485+
expect(response.errors.length).toBe(0);
486+
});
487+
488+
it('handles errors during update', async () => {
489+
const testErrorMessages = 'Update failed';
490+
mockUpdateEntityDefinition.mockRejectedValueOnce(new Error(testErrorMessages));
491+
mockListDescriptor.mockResolvedValueOnce({ engines: [{}] });
492+
mockGetEntityDefinition.mockResolvedValueOnce({
493+
definitions: [definition],
494+
});
495+
496+
const response = await dataClient.applyDataViewIndices();
497+
498+
expect(response.errors.length).toBeGreaterThan(0);
499+
expect(response.errors[0].message).toBe(testErrorMessages);
500+
});
501+
});
367502
});

x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import type {
1313
IScopedClusterClient,
1414
AuditEvent,
1515
AnalyticsServiceSetup,
16+
KibanaRequest,
1617
} from '@kbn/core/server';
18+
import type { SecurityPluginStart } from '@kbn/security-plugin/server';
1719
import { EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client';
1820
import type { HealthStatus, SortOrder } from '@elastic/elasticsearch/lib/api/types';
1921
import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
@@ -23,6 +25,7 @@ import moment from 'moment';
2325
import type { EntityDefinitionWithState } from '@kbn/entityManager-plugin/server/lib/entities/types';
2426
import type { EntityDefinition } from '@kbn/entities-schema';
2527
import type { estypes } from '@elastic/elasticsearch';
28+
import { getAllMissingPrivileges } from '../../../../common/entity_analytics/privileges';
2629
import { merge } from '../../../../common/utils/objects/merge';
2730
import { getEnabledStoreEntityTypes } from '../../../../common/entity_analytics/entity_store/utils';
2831
import { EntityType } from '../../../../common/entity_analytics/types';
@@ -96,7 +99,7 @@ import {
9699
import { CRITICALITY_VALUES } from '../asset_criticality/constants';
97100
import { createEngineDescription } from './installation/engine_description';
98101
import { convertToEntityManagerDefinition } from './entity_definitions/entity_manager_conversion';
99-
102+
import { getEntityStoreSourceIndicesPrivileges } from './utils/get_entity_store_privileges';
100103
import type { ApiKeyManager } from './auth/api_key';
101104

102105
// Workaround. TransformState type is wrong. The health type should be: TransformHealth from '@kbn/transform-plugin/common/types/transform_stats'
@@ -126,6 +129,8 @@ interface EntityStoreClientOpts {
126129
experimentalFeatures: ExperimentalFeatures;
127130
telemetry?: AnalyticsServiceSetup;
128131
apiKeyManager?: ApiKeyManager;
132+
security: SecurityPluginStart;
133+
request: KibanaRequest;
129134
}
130135

131136
interface SearchEntitiesParams {
@@ -788,6 +793,41 @@ export class EntityStoreDataClient {
788793

789794
const { engines } = await this.engineClient.list();
790795

796+
if (engines.length === 0) {
797+
logger.debug(
798+
`In namespace ${this.options.namespace}: No entity engines found, skipping data view index application`
799+
);
800+
return {
801+
successes: [],
802+
errors: [],
803+
};
804+
}
805+
806+
const indexPatterns = await buildIndexPatterns(
807+
this.options.namespace,
808+
this.options.appClient,
809+
this.options.dataViewsService
810+
);
811+
812+
const privileges = await getEntityStoreSourceIndicesPrivileges(
813+
this.options.request,
814+
this.options.security,
815+
indexPatterns
816+
);
817+
818+
if (!privileges.has_all_required) {
819+
const missingPrivilegesMsg = getAllMissingPrivileges(privileges).elasticsearch.index.map(
820+
({ indexName, privileges: missingPrivileges }) =>
821+
`Missing [${missingPrivileges.join(', ')}] privileges for index '${indexName}'.`
822+
);
823+
824+
throw new Error(
825+
`The current user does not have the required indices privileges.\n${missingPrivilegesMsg.join(
826+
'\n'
827+
)}`
828+
);
829+
}
830+
791831
const updateDefinitionPromises: Array<Promise<EngineDataviewUpdateResult>> = engines.map(
792832
async (engine) => {
793833
const originalStatus = engine.status;
@@ -803,12 +843,6 @@ export class EntityStoreDataClient {
803843
);
804844
}
805845

806-
const indexPatterns = await buildIndexPatterns(
807-
this.options.namespace,
808-
this.options.appClient,
809-
this.options.dataViewsService
810-
);
811-
812846
// Skip update if index patterns are the same
813847
if (isEqual(definition.indexPatterns, indexPatterns)) {
814848
logger.debug(

x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/tasks/data_view_refresh/data_view_refresh_task.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ export const registerEntityStoreDataViewRefreshTask = ({
8989

9090
const dataViewsService = await dataViews.dataViewsServiceFactory(soClient, internalUserClient);
9191

92-
const appClient = appClientFactory.create(await apiKeyManager.getRequestFromApiKey(apiKey));
92+
const request = await apiKeyManager.getRequestFromApiKey(apiKey);
93+
94+
const appClient = appClientFactory.create(request);
9395

9496
const entityStoreClient: EntityStoreDataClient = new EntityStoreDataClient({
9597
namespace,
@@ -104,6 +106,8 @@ export const registerEntityStoreDataViewRefreshTask = ({
104106
kibanaVersion,
105107
dataViewsService,
106108
config: entityStoreConfig,
109+
security,
110+
request,
107111
});
108112

109113
await entityStoreClient.applyDataViewIndices();

x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/utils/get_entity_store_privileges.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { RISK_SCORE_INDEX_PATTERN } from '../../../../../common/constants';
1212
import {
1313
ENTITY_STORE_INDEX_PATTERN,
1414
ENTITY_STORE_REQUIRED_ES_CLUSTER_PRIVILEGES,
15+
ENTITY_STORE_SOURCE_REQUIRED_ES_INDEX_PRIVILEGES,
1516
} from '../../../../../common/entity_analytics/entity_store/constants';
1617
import { checkAndFormatPrivileges } from '../../utils/check_and_format_privileges';
1718
import { entityEngineDescriptorTypeName } from '../saved_object';
@@ -22,13 +23,7 @@ export const getEntityStorePrivileges = (
2223
securitySolutionIndices: string[]
2324
) => {
2425
// The entity store needs access to all security solution indices
25-
const indicesPrivileges = securitySolutionIndices.reduce<Record<string, string[]>>(
26-
(acc, index) => {
27-
acc[index] = ['read', 'view_index_metadata'];
28-
return acc;
29-
},
30-
{}
31-
);
26+
const indicesPrivileges = getEntityStoreSourceRequiredIndicesPrivileges(securitySolutionIndices);
3227

3328
// The entity store has to create the following indices
3429
indicesPrivileges[ENTITY_STORE_INDEX_PATTERN] = ['read', 'manage'];
@@ -49,3 +44,30 @@ export const getEntityStorePrivileges = (
4944
},
5045
});
5146
};
47+
48+
// Get the index privileges required for running the transform
49+
export const getEntityStoreSourceIndicesPrivileges = (
50+
request: KibanaRequest,
51+
security: SecurityPluginStart,
52+
indexPatterns: string[]
53+
) => {
54+
const requiredIndicesPrivileges = getEntityStoreSourceRequiredIndicesPrivileges(indexPatterns);
55+
56+
return checkAndFormatPrivileges({
57+
request,
58+
security,
59+
privilegesToCheck: {
60+
elasticsearch: {
61+
cluster: [],
62+
index: requiredIndicesPrivileges,
63+
},
64+
},
65+
});
66+
};
67+
68+
const getEntityStoreSourceRequiredIndicesPrivileges = (securitySolutionIndices: string[]) => {
69+
return securitySolutionIndices.reduce<Record<string, string[]>>((acc, index) => {
70+
acc[index] = ENTITY_STORE_SOURCE_REQUIRED_ES_INDEX_PRIVILEGES;
71+
return acc;
72+
}, {});
73+
};

x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,8 @@ export class RequestContextFactory implements IRequestContextFactory {
266266
request,
267267
namespace: getSpaceId(),
268268
}),
269+
security: startPlugins.security,
270+
request,
269271
});
270272
}),
271273
getAssetInventoryClient: memoize(() => {

0 commit comments

Comments
 (0)