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 @@ -53,7 +53,9 @@ describe('useInstallEntityStoreV2', () => {
const mockServices = createMockServices();
mockServices.uiSettings.get.mockReturnValue(true);
mockServices.spaces.getActiveSpace.mockResolvedValue({ id: 'custom-space' });
mockServices.http.get.mockResolvedValueOnce({ status: EntityStoreStatus.enum.not_installed });
mockServices.http.get
.mockResolvedValueOnce({ status: EntityStoreStatus.enum.not_installed })
.mockResolvedValueOnce({ status: EntityStoreStatus.enum.not_installed });

renderHook(() => useInstallEntityStoreV2(asServices(mockServices)));

Expand All @@ -63,16 +65,20 @@ describe('useInstallEntityStoreV2', () => {
});
});

expect(mockServices.http.get).toHaveBeenCalledTimes(1);
expect(mockServices.http.get).toHaveBeenNthCalledWith(1, {
path: ENTITY_STORE_ROUTES.public.STATUS,
query: { include_components: false },
});
expect(mockServices.http.get).toHaveBeenCalledTimes(2);
expect(mockServices.http.post).not.toHaveBeenCalled();
});

it('should proceed when not in default space and v1 is installed', async () => {
it('should install when not in default space, v1 is installed, and v2 is not installed', async () => {
const mockServices = createMockServices();
mockServices.uiSettings.get.mockReturnValue(true);
mockServices.spaces.getActiveSpace.mockResolvedValue({ id: 'custom-space' });
mockServices.http.get
.mockResolvedValueOnce({ status: EntityStoreStatus.enum.running })
.mockResolvedValueOnce({ status: EntityStoreStatus.enum.not_installed })
.mockResolvedValueOnce({ status: EntityStoreStatus.enum.running });
mockServices.http.post.mockResolvedValueOnce({});

Expand All @@ -83,12 +89,37 @@ describe('useInstallEntityStoreV2', () => {
});

expect(mockServices.http.get).toHaveBeenNthCalledWith(1, {
path: '/api/entity_store/status',
});
expect(mockServices.http.get).toHaveBeenNthCalledWith(2, {
path: ENTITY_STORE_ROUTES.public.STATUS,
query: { include_components: false },
});
expect(mockServices.http.get).toHaveBeenNthCalledWith(2, {
path: '/api/entity_store/status',
});
expect(mockServices.http.post).toHaveBeenCalledWith({
path: ENTITY_STORE_ROUTES.public.INSTALL,
body: JSON.stringify({}),
});
});

it('should init entity maintainers when not in default space, v1 is not installed, and v2 is running', async () => {
const mockServices = createMockServices();
mockServices.uiSettings.get.mockReturnValue(true);
mockServices.spaces.getActiveSpace.mockResolvedValue({ id: 'custom-space' });
mockServices.http.get.mockResolvedValueOnce({ status: EntityStoreStatus.enum.running });
mockServices.http.post.mockResolvedValue({});

renderHook(() => useInstallEntityStoreV2(asServices(mockServices)));

await waitFor(() => {
expect(mockServices.http.post).toHaveBeenCalledTimes(1);
});

expect(mockServices.http.get).toHaveBeenCalledTimes(1);
expect(mockServices.http.post).toHaveBeenCalledWith({
path: ENTITY_STORE_ROUTES.internal.ENTITY_MAINTAINERS_INIT,
body: JSON.stringify({}),
query: { apiVersion: '2' },
});
});

it("should not install when entity store status is 'running'", async () => {
Expand Down Expand Up @@ -119,7 +150,7 @@ describe('useInstallEntityStoreV2', () => {
});
});

it('when entity store is not installed and space is default, installs entity store then inits entity maintainers', async () => {
it('when entity store is not installed and space is default, installs entity store (install API inits maintainers)', async () => {
const mockServices = createMockServices();
mockServices.uiSettings.get.mockReturnValue(true);
mockServices.spaces.getActiveSpace.mockResolvedValue({ id: 'default' });
Expand All @@ -129,18 +160,18 @@ describe('useInstallEntityStoreV2', () => {
renderHook(() => useInstallEntityStoreV2(asServices(mockServices)));

await waitFor(() => {
expect(mockServices.http.post).toHaveBeenCalledTimes(2);
expect(mockServices.http.post).toHaveBeenCalledTimes(1);
});

expect(mockServices.http.get).toHaveBeenCalledWith({
path: ENTITY_STORE_ROUTES.public.STATUS,
query: { include_components: false },
});
expect(mockServices.http.post).toHaveBeenNthCalledWith(1, {
expect(mockServices.http.post).toHaveBeenCalledWith({
path: ENTITY_STORE_ROUTES.public.INSTALL,
body: JSON.stringify({}),
});
expect(mockServices.http.post).toHaveBeenNthCalledWith(2, {
expect(mockServices.http.post).not.toHaveBeenCalledWith({
path: ENTITY_STORE_ROUTES.internal.ENTITY_MAINTAINERS_INIT,
body: JSON.stringify({}),
query: { apiVersion: '2' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,21 +60,24 @@ export const useInstallEntityStoreV2 = (services: Services) => {
if (!isEntityStoreV2Enabled) return;

const space = await services.spaces.getActiveSpace();
// Install v2 and remove v1 in default namespace AND every namespace where v1 is currently installed
if (space.id !== 'default' && !(await isEntityStoreV1Installed(services.http))) return;

const statusResponse = await services.http.get<{ status: EntityStoreStatus }>(
getStatusRequest
);
const isEntityStoreV2Installed = isEntityStoreInstalled(statusResponse.status);
// In non-default spaces, only auto-install v2 where v1 existed. If v2 is already there,
// skip the v1 check and still run (e.g. init entity maintainers for this space).
if (space.id !== 'default' && !isEntityStoreV2Installed) {
if (!(await isEntityStoreV1Installed(services.http))) {
return;
}
}
// Entity store already installed → init entity maintainers only.
if (isEntityStoreV2Installed) {
await services.http.post(initEntityMaintainersRequest);
return;
}
// Entity store not installed → install entity store, then init entity maintainers.
// Entity store not installed → install entity store (init entity maintainers is already done by the install API).
await services.http.post(installAllEntitiesRequest);
await services.http.post(initEntityMaintainersRequest);
} catch (e) {
services.logger.error('Failed to initialize Entity Store V2');
services.logger.error(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ export function registerInstall(router: EntityStorePluginRouter) {
},
wrapMiddlewares(async (ctx, req, res): Promise<IKibanaResponse> => {
const entityStoreCtx = await ctx.entityStore;
const { logger, assetManagerClient: assetManager } = entityStoreCtx;
const {
logger,
assetManagerClient: assetManager,
entityMaintainersClient,
} = entityStoreCtx;
const { entityTypes, logExtraction, historySnapshot } = req.body;
logger.debug('Install api called');

Expand All @@ -70,6 +74,7 @@ export function registerInstall(router: EntityStorePluginRouter) {
}

await assetManager.init(req, toInstall, logExtraction, historySnapshot);
await entityMaintainersClient.init(req);

return res.created({ body: { ok: true } });
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,28 @@

import { apiTest } from '@kbn/scout-security';
import { expect } from '@kbn/scout-security/api';
import { PUBLIC_HEADERS, ENTITY_STORE_ROUTES, ENTITY_STORE_TAGS } from '../fixtures/constants';
import { FF_ENABLE_ENTITY_STORE_V2 } from '../../../../common';
import {
PUBLIC_HEADERS,
INTERNAL_HEADERS,
ENTITY_STORE_ROUTES,
ENTITY_STORE_TAGS,
} from '../fixtures/constants';
import { FF_ENABLE_ENTITY_STORE_V2, type GetEntityMaintainersResponse } from '../../../../common';

apiTest.describe('Entity Store install / update API tests', { tag: ENTITY_STORE_TAGS }, () => {
let defaultHeaders: Record<string, string>;
let internalHeaders: Record<string, string>;

apiTest.beforeAll(async ({ samlAuth }) => {
const credentials = await samlAuth.asInteractiveUser('admin');
defaultHeaders = {
...credentials.cookieHeader,
...PUBLIC_HEADERS,
};
internalHeaders = {
...credentials.cookieHeader,
...INTERNAL_HEADERS,
};
});

apiTest(
Expand All @@ -35,6 +45,18 @@ apiTest.describe('Entity Store install / update API tests', { tag: ENTITY_STORE_
});
expect(install.statusCode).toBe(201);

const maintainersResponse = await apiClient.get(
ENTITY_STORE_ROUTES.internal.ENTITY_MAINTAINERS_GET,
{
headers: internalHeaders,
responseType: 'json',
}
);
expect(maintainersResponse.statusCode).toBe(200);
const { maintainers } = maintainersResponse.body as GetEntityMaintainersResponse;
expect(maintainers.length).toBeGreaterThan(0);
expect(maintainers.every((m) => m.taskStatus === 'started')).toBe(true);

const uninstall = await apiClient.post(ENTITY_STORE_ROUTES.public.UNINSTALL, {
headers: defaultHeaders,
responseType: 'json',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { ENTITY_STORE_ROUTES, API_VERSIONS } from '@kbn/entity-store/common';
import { ENTITY_RESOLUTION_CSV_UPLOAD_URL } from '@kbn/security-solution-plugin/common/entity_analytics/entity_store/constants';
import type { FtrProviderContext } from '../../../../ftr_provider_context';
import { EntityStoreUtils } from '../../utils';
import { entityMaintainerRouteHelpersFactory } from '../../utils/entity_maintainers';

/** Same value as `MAINTAINER_ID` in entity_store `server/maintainers/automated_resolution`. */
const AUTOMATED_RESOLUTION_MAINTAINER_ID = 'automated-resolution';

const TEST_PREFIX = 'csv-test:';

Expand Down Expand Up @@ -116,11 +120,14 @@ export default ({ getService }: FtrProviderContext) => {

describe('@ess @serverless @skipInServerlessMKI Entity Resolution CSV Upload', () => {
before(async () => {
// Use enableEntityStoreV2 (without maintainer init) to prevent the
// automated resolution maintainer from racing with CSV upload tests.
// Use enableEntityStoreV2 and explicitly stop
// the automated-resolution maintainer so it cannot race with CSV upload tests.
// The maintainer would link entities sharing the same user.email,
// interfering with the test's own resolution assertions.
await entityStoreUtils.enableEntityStoreV2();
await entityMaintainerRouteHelpersFactory(supertest).stopMaintainer(
AUTOMATED_RESOLUTION_MAINTAINER_ID
);
await cleanEntities();
await seedEntities();
await waitForEntities();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export default ({ getService }: FtrProviderContext): void => {
startMaintainer: async () => {
throw new Error('Preview API tests do not use maintainer route startMaintainer');
},
stopMaintainer: async () => {
throw new Error('Preview API tests do not use maintainer route stopMaintainer');
},
},
});
const setEntityStoreV2Setting = async (enabled: boolean) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ interface RetryServiceLike {

type MaintainerRoutesLike = Pick<
ReturnType<typeof entityMaintainerRouteHelpersFactory>,
'getMaintainers' | 'runMaintainer' | 'runMaintainerSync' | 'startMaintainer'
'getMaintainers' | 'runMaintainer' | 'runMaintainerSync' | 'startMaintainer' | 'stopMaintainer'
>;

interface EntityStoreUtilsLike {
Expand Down Expand Up @@ -364,6 +364,8 @@ export const riskScoreMaintainerScenarioFactory = ({
dataViewPattern,
maintainerAutoStart: false,
});
await routes.stopMaintainer('risk-score');

if (runMode === 'sync') {
await routes.runMaintainerSync('risk-score');
return;
Expand Down
Loading