Skip to content

Commit d04fdf7

Browse files
chennn1990kibanamachine
authored andcommitted
Entity store/entity maintainers min license (elastic#260170)
## Summary Entity Store entity maintainers now respect Kibana license tier at run time - Each maintainer can declare an optional minimum license - If the active license is not enough, that run is skipped and maintainer state is left unchanged - When no minimum is set, the framework uses the lowest tier so behavior matches a Basic-style default - License is evaluated when the task runs, so tier changes apply without restarting Kibana The internal list-maintainers response includes the configured minimum license so callers can see requirements next to status Duplicate common constants were consolidated so shared plugin constants live in one place for public code ## Testing - Automated tests cover maintainer registration, license skip behavior, and list response shape for minimum license ## How to verify - Run the entity maintainer unit tests - Run your usual repo change-validation checks --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
1 parent 24489a1 commit d04fdf7

17 files changed

Lines changed: 273 additions & 175 deletions

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

Lines changed: 0 additions & 68 deletions
This file was deleted.

x-pack/solutions/security/plugins/entity_store/kibana.jsonc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"spaces",
1414
"taskManager",
1515
"dataViews",
16-
"security"
16+
"security",
17+
"licensing"
1718
],
1819
"optionalPlugins": [
1920
"encryptedSavedObjects"

x-pack/solutions/security/plugins/entity_store/moon.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ dependsOn:
4545
- '@kbn/safer-lodash-set'
4646
- '@kbn/tracing-utils'
4747
- '@kbn/esql-language'
48+
- '@kbn/licensing-plugin'
49+
- '@kbn/licensing-types'
4850
tags:
4951
- plugin
5052
- prod

x-pack/solutions/security/plugins/entity_store/public/bulk_update_entities_api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import type { HttpStart } from '@kbn/core/public';
99
import type { EntityType } from '../common';
10-
import { API_VERSIONS, ENTITY_STORE_ROUTES } from '../common/constants';
10+
import { API_VERSIONS, ENTITY_STORE_ROUTES } from '../common';
1111

1212
export interface BulkUpdateEntitiesParams {
1313
entityType: EntityType;

x-pack/solutions/security/plugins/entity_store/public/hooks/useInstallEntityStoreV2.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
type Services,
1313
} from './useInstallEntityStoreV2';
1414
import { EntityStoreStatus } from '../../common';
15-
import { ENTITY_STORE_ROUTES, FF_ENABLE_ENTITY_STORE_V2 } from '../../common/constants';
15+
import { ENTITY_STORE_ROUTES, FF_ENABLE_ENTITY_STORE_V2 } from '../../common';
1616

1717
interface MockServices {
1818
http: { get: jest.Mock; post: jest.Mock };

x-pack/solutions/security/plugins/entity_store/public/hooks/useInstallEntityStoreV2.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
1010
import type { HttpFetchOptionsWithPath, HttpSetup, IUiSettingsClient } from '@kbn/core/public';
1111
import { useEffect } from 'react';
1212
import { EntityStoreStatus } from '../../common';
13-
import { ENTITY_STORE_ROUTES, FF_ENABLE_ENTITY_STORE_V2 } from '../../common/constants';
13+
import { ENTITY_STORE_ROUTES, FF_ENABLE_ENTITY_STORE_V2 } from '../../common';
1414
import type { StatusRequestQuery } from '../../server/routes/apis/status';
1515

1616
export interface Services {

x-pack/solutions/security/plugins/entity_store/public/search_entities_api.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@
66
*/
77

88
import type { HttpStart } from '@kbn/core/public';
9-
import type { Entity } from '../common';
10-
import { ENTITY_STORE_ROUTES } from '../common/constants';
11-
import type { EntityType } from '../common';
12-
import { API_VERSIONS } from '../common/constants';
9+
import type { Entity, EntityType } from '../common';
10+
import { API_VERSIONS, ENTITY_STORE_ROUTES } from '../common';
1311
export interface SearchEntitiesFromEntityStoreParams {
1412
entityTypes: EntityType[];
1513
filterQuery?: string;

x-pack/solutions/security/plugins/entity_store/server/domain/entity_maintainers/entity_maintainers_client.test.ts

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { loggerMock } from '@kbn/logging-mocks';
1010
import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
1111
import type { KibanaRequest } from '@kbn/core/server';
1212
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
13+
import { DEFAULT_ENTITY_MAINTAINER_MIN_LICENSE } from '../../tasks/entity_maintainers';
1314
import { EntityMaintainerTaskStatus } from '../../tasks/entity_maintainers/types';
1415
import type { EntityMaintainerTaskEntry } from '../../tasks/entity_maintainers/types';
1516

@@ -180,8 +181,18 @@ describe('EntityMaintainersClient', () => {
180181
describe('init', () => {
181182
it('should schedule only maintainers without an existing task', async () => {
182183
entityMaintainersRegistry.getAll.mockReturnValue([
183-
{ id: 'm1', interval: '5m', description: 'M1' },
184-
{ id: 'm2', interval: '1h', description: 'M2' },
184+
{
185+
id: 'm1',
186+
interval: '5m',
187+
description: 'M1',
188+
minLicense: DEFAULT_ENTITY_MAINTAINER_MIN_LICENSE,
189+
},
190+
{
191+
id: 'm2',
192+
interval: '1h',
193+
description: 'M2',
194+
minLicense: DEFAULT_ENTITY_MAINTAINER_MIN_LICENSE,
195+
},
185196
]);
186197
const taskManager = {
187198
get: jest.fn().mockImplementation((taskId: string) => {
@@ -211,7 +222,12 @@ describe('EntityMaintainersClient', () => {
211222

212223
it('should not schedule when all maintainers already have tasks', async () => {
213224
entityMaintainersRegistry.getAll.mockReturnValue([
214-
{ id: 'm1', interval: '5m', description: 'M1' },
225+
{
226+
id: 'm1',
227+
interval: '5m',
228+
description: 'M1',
229+
minLicense: DEFAULT_ENTITY_MAINTAINER_MIN_LICENSE,
230+
},
215231
]);
216232
const taskManager = {
217233
get: jest.fn().mockResolvedValue({
@@ -232,7 +248,12 @@ describe('EntityMaintainersClient', () => {
232248

233249
it('should propagate error when scheduleEntityMaintainerTask rejects', async () => {
234250
entityMaintainersRegistry.getAll.mockReturnValue([
235-
{ id: 'm1', interval: '5m', description: 'M1' },
251+
{
252+
id: 'm1',
253+
interval: '5m',
254+
description: 'M1',
255+
minLicense: DEFAULT_ENTITY_MAINTAINER_MIN_LICENSE,
256+
},
236257
]);
237258
const taskManager = {
238259
get: jest.fn().mockRejectedValue(new Error('Not found')),
@@ -288,8 +309,8 @@ describe('EntityMaintainersClient', () => {
288309
describe('removeAll', () => {
289310
it('should remove all registered tasks', async () => {
290311
const entries: EntityMaintainerTaskEntry[] = [
291-
{ id: 'm1', interval: '5m' },
292-
{ id: 'm2', interval: '1h' },
312+
{ id: 'm1', interval: '5m', minLicense: DEFAULT_ENTITY_MAINTAINER_MIN_LICENSE },
313+
{ id: 'm2', interval: '1h', minLicense: DEFAULT_ENTITY_MAINTAINER_MIN_LICENSE },
293314
];
294315
entityMaintainersRegistry.getAll.mockReturnValue(entries);
295316
const client = createClient();
@@ -308,8 +329,8 @@ describe('EntityMaintainersClient', () => {
308329

309330
it('should propagate error when any remove fails', async () => {
310331
const entries: EntityMaintainerTaskEntry[] = [
311-
{ id: 'm1', interval: '5m' },
312-
{ id: 'm2', interval: '1h' },
332+
{ id: 'm1', interval: '5m', minLicense: DEFAULT_ENTITY_MAINTAINER_MIN_LICENSE },
333+
{ id: 'm2', interval: '1h', minLicense: DEFAULT_ENTITY_MAINTAINER_MIN_LICENSE },
313334
];
314335
entityMaintainersRegistry.getAll.mockReturnValue(entries);
315336
(removeEntityMaintainer as jest.Mock)
@@ -328,6 +349,7 @@ describe('EntityMaintainersClient', () => {
328349
id: 'm1',
329350
interval: '5m',
330351
description: 'Maintainer one',
352+
minLicense: DEFAULT_ENTITY_MAINTAINER_MIN_LICENSE,
331353
},
332354
]);
333355
const taskManagerGet = jest.fn().mockRejectedValue(new Error('Not found'));
@@ -342,14 +364,17 @@ describe('EntityMaintainersClient', () => {
342364
taskStatus: EntityMaintainerTaskStatus.NEVER_STARTED,
343365
interval: '5m',
344366
description: 'Maintainer one',
367+
minLicense: DEFAULT_ENTITY_MAINTAINER_MIN_LICENSE,
345368
taskSnapshot: undefined,
346369
});
347370
expect(getTaskId).toHaveBeenCalledWith('m1', 'default');
348371
expect(taskManagerGet).toHaveBeenCalledWith('m1:default');
349372
});
350373

351374
it('should return entries with taskSnapshot and taskStatus from task state when task exists', async () => {
352-
entityMaintainersRegistry.getAll.mockReturnValue([{ id: 'm1', interval: '5m' }]);
375+
entityMaintainersRegistry.getAll.mockReturnValue([
376+
{ id: 'm1', interval: '5m', minLicense: 'gold' },
377+
]);
353378
const taskManagerGet = jest.fn().mockResolvedValue({
354379
state: {
355380
taskStatus: EntityMaintainerTaskStatus.STARTED,
@@ -370,6 +395,7 @@ describe('EntityMaintainersClient', () => {
370395
id: 'm1',
371396
taskStatus: EntityMaintainerTaskStatus.STARTED,
372397
interval: '5m',
398+
minLicense: 'gold',
373399
taskSnapshot: {
374400
runs: 10,
375401
lastSuccessTimestamp: '2024-01-15T12:00:00.000Z',
@@ -381,7 +407,12 @@ describe('EntityMaintainersClient', () => {
381407

382408
it('should return taskStatus from task state', async () => {
383409
entityMaintainersRegistry.getAll.mockReturnValue([
384-
{ id: 'm1', interval: '5m', description: 'Registry says stopped' },
410+
{
411+
id: 'm1',
412+
interval: '5m',
413+
description: 'Registry says stopped',
414+
minLicense: DEFAULT_ENTITY_MAINTAINER_MIN_LICENSE,
415+
},
385416
]);
386417
const taskManagerGet = jest.fn().mockResolvedValue({
387418
state: {
@@ -409,7 +440,9 @@ describe('EntityMaintainersClient', () => {
409440
});
410441

411442
it('should propagate non-not-found errors from taskManager.get', async () => {
412-
entityMaintainersRegistry.getAll.mockReturnValue([{ id: 'm1', interval: '5m' }]);
443+
entityMaintainersRegistry.getAll.mockReturnValue([
444+
{ id: 'm1', interval: '5m', minLicense: DEFAULT_ENTITY_MAINTAINER_MIN_LICENSE },
445+
]);
413446
const taskManagerGet = jest.fn().mockRejectedValue(new Error('ES connection failed'));
414447
const client = createClient({ taskManager: { get: taskManagerGet } });
415448
mockSavedObjectsErrorHelpers.isNotFoundError.mockReturnValue(false);
@@ -418,7 +451,9 @@ describe('EntityMaintainersClient', () => {
418451
});
419452

420453
it('should use default runs and timestamps when task state metadata is missing', async () => {
421-
entityMaintainersRegistry.getAll.mockReturnValue([{ id: 'm1', interval: '5m' }]);
454+
entityMaintainersRegistry.getAll.mockReturnValue([
455+
{ id: 'm1', interval: '5m', minLicense: DEFAULT_ENTITY_MAINTAINER_MIN_LICENSE },
456+
]);
422457
const taskManagerGet = jest.fn().mockResolvedValue({
423458
state: {
424459
taskStatus: EntityMaintainerTaskStatus.STARTED,

x-pack/solutions/security/plugins/entity_store/server/domain/entity_maintainers/entity_maintainers_client.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import type { Logger } from '@kbn/logging';
99
import { SavedObjectsErrorHelpers, type KibanaRequest } from '@kbn/core/server';
1010
import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
11+
import type { LicenseType } from '@kbn/licensing-types';
1112
import {
1213
getTaskId,
1314
removeEntityMaintainer,
@@ -32,6 +33,7 @@ export interface EntityMaintainerListEntry {
3233
taskStatus: EntityMaintainerTaskStatus;
3334
interval: string;
3435
description?: string;
36+
minLicense: LicenseType;
3537
taskSnapshot?: TaskSnapshot;
3638
}
3739

@@ -162,7 +164,7 @@ export class EntityMaintainersClient {
162164

163165
const results = await Promise.all(
164166
entries.map(async (entry): Promise<EntityMaintainerListEntry> => {
165-
const { id, interval, description } = entry;
167+
const { id, interval, description, minLicense } = entry;
166168
const taskId = getTaskId(id, this.namespace);
167169
let taskSnapshot: TaskSnapshot | undefined;
168170
let taskStatus: EntityMaintainerTaskStatus = EntityMaintainerTaskStatus.NEVER_STARTED;
@@ -195,6 +197,7 @@ export class EntityMaintainersClient {
195197
taskStatus,
196198
interval,
197199
description,
200+
minLicense,
198201
taskSnapshot,
199202
};
200203
})

x-pack/solutions/security/plugins/entity_store/server/routes/apis/entity_maintainers/get_maintainers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import type { IKibanaResponse } from '@kbn/core-http-server';
9+
import type { LicenseType } from '@kbn/licensing-types';
910
import { API_VERSIONS, ENTITY_STORE_ROUTES } from '../../../../common';
1011
import { DEFAULT_ENTITY_STORE_PERMISSIONS } from '../../constants';
1112
import type { EntityStorePluginRouter } from '../../../types';
@@ -21,6 +22,7 @@ interface EntityMaintainerResponseItem {
2122
taskStatus: EntityMaintainerTaskStatus;
2223
interval: string;
2324
description: string | null;
25+
minLicense: LicenseType;
2426
customState: EntityMaintainerState | null;
2527
runs: number;
2628
lastSuccessTimestamp: string | null;
@@ -36,6 +38,7 @@ function toGetMaintainersResponseItem(
3638
taskStatus: entry.taskStatus,
3739
interval: entry.interval,
3840
description: entry.description ?? null,
41+
minLicense: entry.minLicense,
3942
customState: snapshot?.state ?? null,
4043
runs: snapshot?.runs ?? 0,
4144
lastSuccessTimestamp: snapshot?.lastSuccessTimestamp ?? null,

0 commit comments

Comments
 (0)