diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobject.md b/docs/development/core/public/kibana-plugin-core-public.savedobject.md
index 9ced619ad4bfe..c6bc13b98bc06 100644
--- a/docs/development/core/public/kibana-plugin-core-public.savedobject.md
+++ b/docs/development/core/public/kibana-plugin-core-public.savedobject.md
@@ -4,7 +4,6 @@
## SavedObject interface
-
Signature:
```typescript
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobject.md
index ebb105c846aff..0df97b0d4221a 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobject.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobject.md
@@ -4,7 +4,6 @@
## SavedObject interface
-
Signature:
```typescript
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 46667230edc3b..fa5dc745e6931 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -944,6 +944,8 @@ export type RecursiveReadonly = T extends (...args: any[]) => any ? T : T ext
[K in keyof T]: RecursiveReadonly;
}> : T;
+// Warning: (ae-missing-release-tag) "SavedObject" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
// @public (undocumented)
export interface SavedObject {
attributes: T;
diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts
index 5015a9c3db78e..13b4a12893666 100644
--- a/src/core/public/saved_objects/index.ts
+++ b/src/core/public/saved_objects/index.ts
@@ -32,11 +32,6 @@ export {
export { SimpleSavedObject } from './simple_saved_object';
export { SavedObjectsStart, SavedObjectsService } from './saved_objects_service';
export {
- SavedObject,
- SavedObjectAttribute,
- SavedObjectAttributes,
- SavedObjectAttributeSingle,
- SavedObjectReference,
SavedObjectsBaseOptions,
SavedObjectsFindOptions,
SavedObjectsMigrationVersion,
@@ -48,3 +43,11 @@ export {
SavedObjectsImportError,
SavedObjectsImportRetry,
} from '../../server/types';
+
+export {
+ SavedObject,
+ SavedObjectAttribute,
+ SavedObjectAttributes,
+ SavedObjectAttributeSingle,
+ SavedObjectReference,
+} from '../../types';
diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts
index 1d927211b43e5..962965a08f8b2 100644
--- a/src/core/server/saved_objects/types.ts
+++ b/src/core/server/saved_objects/types.ts
@@ -35,66 +35,17 @@ export {
import { LegacyConfig } from '../legacy';
import { SavedObjectUnsanitizedDoc } from './serialization';
import { SavedObjectsMigrationLogger } from './migrations/core/migration_logger';
+import { SavedObject } from '../../types';
+
export {
SavedObjectAttributes,
SavedObjectAttribute,
SavedObjectAttributeSingle,
+ SavedObject,
+ SavedObjectReference,
+ SavedObjectsMigrationVersion,
} from '../../types';
-/**
- * Information about the migrations that have been applied to this SavedObject.
- * When Kibana starts up, KibanaMigrator detects outdated documents and
- * migrates them based on this value. For each migration that has been applied,
- * the plugin's name is used as a key and the latest migration version as the
- * value.
- *
- * @example
- * migrationVersion: {
- * dashboard: '7.1.1',
- * space: '6.6.6',
- * }
- *
- * @public
- */
-export interface SavedObjectsMigrationVersion {
- [pluginName: string]: string;
-}
-
-/**
- * @public
- */
-export interface SavedObject {
- /** The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` */
- id: string;
- /** The type of Saved Object. Each plugin can define it's own custom Saved Object types. */
- type: string;
- /** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */
- version?: string;
- /** Timestamp of the last time this document had been updated. */
- updated_at?: string;
- error?: {
- message: string;
- statusCode: number;
- };
- /** {@inheritdoc SavedObjectAttributes} */
- attributes: T;
- /** {@inheritdoc SavedObjectReference} */
- references: SavedObjectReference[];
- /** {@inheritdoc SavedObjectsMigrationVersion} */
- migrationVersion?: SavedObjectsMigrationVersion;
-}
-
-/**
- * A reference to another saved object.
- *
- * @public
- */
-export interface SavedObjectReference {
- name: string;
- type: string;
- id: string;
-}
-
/**
*
* @public
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 84fe081adaae6..229ffc4d21575 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -1595,6 +1595,8 @@ export interface RouteValidatorOptions {
// @public
export type SafeRouteMethod = 'get' | 'options';
+// Warning: (ae-missing-release-tag) "SavedObject" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
// @public (undocumented)
export interface SavedObject {
attributes: T;
diff --git a/src/core/types/saved_objects.ts b/src/core/types/saved_objects.ts
index 73eb2db11d62f..d3faab6c557cd 100644
--- a/src/core/types/saved_objects.ts
+++ b/src/core/types/saved_objects.ts
@@ -46,3 +46,54 @@ export type SavedObjectAttribute = SavedObjectAttributeSingle | SavedObjectAttri
export interface SavedObjectAttributes {
[key: string]: SavedObjectAttribute;
}
+
+/**
+ * A reference to another saved object.
+ *
+ * @public
+ */
+export interface SavedObjectReference {
+ name: string;
+ type: string;
+ id: string;
+}
+
+/**
+ * Information about the migrations that have been applied to this SavedObject.
+ * When Kibana starts up, KibanaMigrator detects outdated documents and
+ * migrates them based on this value. For each migration that has been applied,
+ * the plugin's name is used as a key and the latest migration version as the
+ * value.
+ *
+ * @example
+ * migrationVersion: {
+ * dashboard: '7.1.1',
+ * space: '6.6.6',
+ * }
+ *
+ * @public
+ */
+export interface SavedObjectsMigrationVersion {
+ [pluginName: string]: string;
+}
+
+export interface SavedObject {
+ /** The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` */
+ id: string;
+ /** The type of Saved Object. Each plugin can define it's own custom Saved Object types. */
+ type: string;
+ /** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */
+ version?: string;
+ /** Timestamp of the last time this document had been updated. */
+ updated_at?: string;
+ error?: {
+ message: string;
+ statusCode: number;
+ };
+ /** {@inheritdoc SavedObjectAttributes} */
+ attributes: T;
+ /** {@inheritdoc SavedObjectReference} */
+ references: SavedObjectReference[];
+ /** {@inheritdoc SavedObjectsMigrationVersion} */
+ migrationVersion?: SavedObjectsMigrationVersion;
+}
diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts
index 3ce46e2955f50..6a363af9e57d4 100644
--- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts
+++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts
@@ -19,6 +19,7 @@ import {
// @ts-ignore
} from '../../common/constants';
import { LayerDescriptor } from '../../common/descriptor_types';
+import { MapSavedObject } from '../../../../../plugins/maps/common/map_saved_object_type';
interface IStats {
[key: string]: {
@@ -32,33 +33,6 @@ interface ILayerTypeCount {
[key: string]: number;
}
-interface IMapSavedObject {
- [key: string]: any;
- fields: IFieldType[];
- title: string;
- id?: string;
- type?: string;
- timeFieldName?: string;
- fieldFormatMap?: Record<
- string,
- {
- id: string;
- params: unknown;
- }
- >;
- attributes?: {
- title?: string;
- description?: string;
- mapStateJSON?: string;
- layerListJSON?: string;
- uiStateJSON?: string;
- bounds?: {
- type?: string;
- coordinates?: [];
- };
- };
-}
-
function getUniqueLayerCounts(layerCountsList: ILayerTypeCount[], mapsCount: number) {
const uniqueLayerTypes = _.uniq(_.flatten(layerCountsList.map(lTypes => Object.keys(lTypes))));
@@ -102,7 +76,7 @@ export function buildMapsTelemetry({
indexPatternSavedObjects,
settings,
}: {
- mapSavedObjects: IMapSavedObject[];
+ mapSavedObjects: MapSavedObject[];
indexPatternSavedObjects: IIndexPattern[];
settings: SavedObjectAttribute;
}): SavedObjectAttributes {
@@ -183,7 +157,7 @@ export async function getMapsTelemetry(
savedObjectsClient: SavedObjectsClientContract,
config: Function
) {
- const mapSavedObjects: IMapSavedObject[] = await getMapSavedObjects(savedObjectsClient);
+ const mapSavedObjects: MapSavedObject[] = await getMapSavedObjects(savedObjectsClient);
const indexPatternSavedObjects: IIndexPattern[] = await getIndexPatternSavedObjects(
savedObjectsClient
);
diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts
index 49868bb7ad5d5..56622617586f7 100644
--- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts
+++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts
@@ -82,16 +82,21 @@ export function registerGenerateFromJobParams(
}
const { exportType } = request.params;
+ let jobParams;
let response;
try {
- const jobParams = rison.decode(jobParamsRison) as object | null;
+ jobParams = rison.decode(jobParamsRison) as object | null;
if (!jobParams) {
throw new Error('missing jobParams!');
}
- response = await handler(exportType, jobParams, legacyRequest, h);
} catch (err) {
throw boom.badRequest(`invalid rison: ${jobParamsRison}`);
}
+ try {
+ response = await handler(exportType, jobParams, legacyRequest, h);
+ } catch (err) {
+ throw handleError(exportType, err);
+ }
return response;
},
});
diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts
new file mode 100644
index 0000000000000..54d9671692c5d
--- /dev/null
+++ b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts
@@ -0,0 +1,140 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import Hapi from 'hapi';
+import { createMockReportingCore } from '../../test_helpers';
+import { Logger, ServerFacade } from '../../types';
+import { ReportingCore, ReportingSetupDeps } from '../../server/types';
+
+jest.mock('./lib/authorized_user_pre_routing', () => ({
+ authorizedUserPreRoutingFactory: () => () => ({}),
+}));
+jest.mock('./lib/reporting_feature_pre_routing', () => ({
+ reportingFeaturePreRoutingFactory: () => () => () => ({
+ jobTypes: ['unencodedJobType', 'base64EncodedJobType'],
+ }),
+}));
+
+import { registerJobGenerationRoutes } from './generation';
+
+let mockServer: Hapi.Server;
+let mockReportingPlugin: ReportingCore;
+const mockLogger = ({
+ error: jest.fn(),
+ debug: jest.fn(),
+} as unknown) as Logger;
+
+beforeEach(async () => {
+ mockServer = new Hapi.Server({
+ debug: false,
+ port: 8080,
+ routes: { log: { collect: true } },
+ });
+ mockServer.config = () => ({ get: jest.fn(), has: jest.fn() });
+ mockReportingPlugin = await createMockReportingCore();
+ mockReportingPlugin.getEnqueueJob = async () =>
+ jest.fn().mockImplementation(() => ({ toJSON: () => '{ "job": "data" }' }));
+});
+
+const mockPlugins = {
+ elasticsearch: {
+ adminClient: { callAsInternalUser: jest.fn() },
+ },
+ security: null,
+};
+
+const getErrorsFromRequest = (request: Hapi.Request) => {
+ // @ts-ignore error property doesn't exist on RequestLog
+ return request.logs.filter(log => log.tags.includes('error')).map(log => log.error); // NOTE: error stack is available
+};
+
+test(`returns 400 if there are no job params`, async () => {
+ registerJobGenerationRoutes(
+ mockReportingPlugin,
+ (mockServer as unknown) as ServerFacade,
+ (mockPlugins as unknown) as ReportingSetupDeps,
+ mockLogger
+ );
+
+ const options = {
+ method: 'POST',
+ url: '/api/reporting/generate/printablePdf',
+ };
+
+ const { payload, request } = await mockServer.inject(options);
+ expect(payload).toMatchInlineSnapshot(
+ `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"A jobParams RISON string is required\\"}"`
+ );
+
+ const errorLogs = getErrorsFromRequest(request);
+ expect(errorLogs).toMatchInlineSnapshot(`
+ Array [
+ [Error: A jobParams RISON string is required],
+ ]
+ `);
+});
+
+test(`returns 400 if job params is invalid`, async () => {
+ registerJobGenerationRoutes(
+ mockReportingPlugin,
+ (mockServer as unknown) as ServerFacade,
+ (mockPlugins as unknown) as ReportingSetupDeps,
+ mockLogger
+ );
+
+ const options = {
+ method: 'POST',
+ url: '/api/reporting/generate/printablePdf',
+ payload: { jobParams: `foo:` },
+ };
+
+ const { payload, request } = await mockServer.inject(options);
+ expect(payload).toMatchInlineSnapshot(
+ `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"invalid rison: foo:\\"}"`
+ );
+
+ const errorLogs = getErrorsFromRequest(request);
+ expect(errorLogs).toMatchInlineSnapshot(`
+ Array [
+ [Error: invalid rison: foo:],
+ ]
+ `);
+});
+
+test(`returns 500 if job handler throws an error`, async () => {
+ mockReportingPlugin.getEnqueueJob = async () =>
+ jest.fn().mockImplementation(() => ({
+ toJSON: () => {
+ throw new Error('you found me');
+ },
+ }));
+
+ registerJobGenerationRoutes(
+ mockReportingPlugin,
+ (mockServer as unknown) as ServerFacade,
+ (mockPlugins as unknown) as ReportingSetupDeps,
+ mockLogger
+ );
+
+ const options = {
+ method: 'POST',
+ url: '/api/reporting/generate/printablePdf',
+ payload: { jobParams: `abc` },
+ };
+
+ const { payload, request } = await mockServer.inject(options);
+ expect(payload).toMatchInlineSnapshot(
+ `"{\\"statusCode\\":500,\\"error\\":\\"Internal Server Error\\",\\"message\\":\\"An internal server error occurred\\"}"`
+ );
+
+ const errorLogs = getErrorsFromRequest(request);
+ expect(errorLogs).toMatchInlineSnapshot(`
+ Array [
+ [Error: you found me],
+ [Error: you found me],
+ ]
+ `);
+});
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx
index 9718b4e4ef8cd..b900a0a35dbf5 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx
@@ -233,7 +233,7 @@ export const AlertIndex = memo(() => {
-
+
{
+ it('validate that ack event schema expect action id', async () => {
+ expect(() =>
+ AckEventSchema.validate({
+ type: 'ACTION_RESULT',
+ subtype: 'CONFIG',
+ timestamp: '2019-01-04T14:32:03.36764-05:00',
+ agent_id: 'agent',
+ message: 'hello',
+ payload: 'payload',
+ })
+ ).toThrow(Error);
+
+ expect(
+ AckEventSchema.validate({
+ type: 'ACTION_RESULT',
+ subtype: 'CONFIG',
+ timestamp: '2019-01-04T14:32:03.36764-05:00',
+ agent_id: 'agent',
+ action_id: 'actionId',
+ message: 'hello',
+ payload: 'payload',
+ })
+ ).toBeTruthy();
+ });
+});
+
+describe('test acks handlers', () => {
+ let mockResponse: jest.Mocked;
+ let mockSavedObjectsClient: jest.Mocked;
+
+ beforeEach(() => {
+ mockSavedObjectsClient = savedObjectsClientMock.create();
+ mockResponse = httpServerMock.createResponseFactory();
+ });
+
+ it('should succeed on valid agent event', async () => {
+ const mockRequest = httpServerMock.createKibanaRequest({
+ headers: {
+ authorization: 'ApiKey TmVqTDBIQUJsRkw1em52R1ZIUF86NS1NaTItdHFUTHFHbThmQW1Fb0ljUQ==',
+ },
+ body: {
+ events: [
+ {
+ type: 'ACTION_RESULT',
+ subtype: 'CONFIG',
+ timestamp: '2019-01-04T14:32:03.36764-05:00',
+ action_id: 'action1',
+ agent_id: 'agent',
+ message: 'message',
+ },
+ ],
+ },
+ });
+
+ const ackService: AcksService = {
+ acknowledgeAgentActions: jest.fn().mockReturnValueOnce([
+ {
+ type: 'CONFIG_CHANGE',
+ id: 'action1',
+ },
+ ]),
+ getAgentByAccessAPIKeyId: jest.fn().mockReturnValueOnce({
+ id: 'agent',
+ }),
+ getSavedObjectsClientContract: jest.fn().mockReturnValueOnce(mockSavedObjectsClient),
+ saveAgentEvents: jest.fn(),
+ } as jest.Mocked;
+
+ const postAgentAcksHandler = postAgentAcksHandlerBuilder(ackService);
+ await postAgentAcksHandler(({} as unknown) as RequestHandlerContext, mockRequest, mockResponse);
+ expect(mockResponse.ok.mock.calls[0][0]?.body as PostAgentAcksResponse).toEqual({
+ action: 'acks',
+ success: true,
+ });
+ });
+});
diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts
new file mode 100644
index 0000000000000..53b677bb1389e
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+// handlers that handle events from agents in response to actions received
+
+import { RequestHandler } from 'kibana/server';
+import { TypeOf } from '@kbn/config-schema';
+import { PostAgentAcksRequestSchema } from '../../types/rest_spec';
+import * as APIKeyService from '../../services/api_keys';
+import { AcksService } from '../../services/agents';
+import { AgentEvent } from '../../../common/types/models';
+import { PostAgentAcksResponse } from '../../../common/types/rest_spec';
+
+export const postAgentAcksHandlerBuilder = function(
+ ackService: AcksService
+): RequestHandler<
+ TypeOf,
+ undefined,
+ TypeOf
+> {
+ return async (context, request, response) => {
+ try {
+ const soClient = ackService.getSavedObjectsClientContract(request);
+ const res = APIKeyService.parseApiKey(request.headers);
+ const agent = await ackService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId as string);
+ const agentEvents = request.body.events as AgentEvent[];
+
+ // validate that all events are for the authorized agent obtained from the api key
+ const notAuthorizedAgentEvent = agentEvents.filter(
+ agentEvent => agentEvent.agent_id !== agent.id
+ );
+
+ if (notAuthorizedAgentEvent && notAuthorizedAgentEvent.length > 0) {
+ return response.badRequest({
+ body:
+ 'agent events contains events with different agent id from currently authorized agent',
+ });
+ }
+
+ const agentActions = await ackService.acknowledgeAgentActions(soClient, agent, agentEvents);
+
+ if (agentActions.length > 0) {
+ await ackService.saveAgentEvents(soClient, agentEvents);
+ }
+
+ const body: PostAgentAcksResponse = {
+ action: 'acks',
+ success: true,
+ };
+
+ return response.ok({ body });
+ } catch (e) {
+ if (e.isBoom) {
+ return response.customError({
+ statusCode: e.output.statusCode,
+ body: { message: e.message },
+ });
+ }
+
+ return response.customError({
+ statusCode: 500,
+ body: { message: e.message },
+ });
+ }
+ };
+};
diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts
index 199a61bd4fcef..7d991f5ad2cc2 100644
--- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts
@@ -23,7 +23,6 @@ import {
GetOneAgentEventsRequestSchema,
PostAgentCheckinRequestSchema,
PostAgentEnrollRequestSchema,
- PostAgentAcksRequestSchema,
PostAgentUnenrollRequestSchema,
GetAgentStatusRequestSchema,
} from '../../types';
@@ -31,7 +30,7 @@ import * as AgentService from '../../services/agents';
import * as APIKeyService from '../../services/api_keys';
import { appContextService } from '../../services/app_context';
-function getInternalUserSOClient(request: KibanaRequest) {
+export function getInternalUserSOClient(request: KibanaRequest) {
// soClient as kibana internal users, be carefull on how you use it, security is not enabled
return appContextService.getSavedObjects().getScopedClient(request, {
excludedWrappers: ['security'],
@@ -210,39 +209,6 @@ export const postAgentCheckinHandler: RequestHandler<
}
};
-export const postAgentAcksHandler: RequestHandler<
- TypeOf,
- undefined,
- TypeOf
-> = async (context, request, response) => {
- try {
- const soClient = getInternalUserSOClient(request);
- const res = APIKeyService.parseApiKey(request.headers);
- const agent = await AgentService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId as string);
-
- await AgentService.acknowledgeAgentActions(soClient, agent, request.body.action_ids);
-
- const body = {
- action: 'acks',
- success: true,
- };
-
- return response.ok({ body });
- } catch (e) {
- if (e.isBoom) {
- return response.customError({
- statusCode: e.output.statusCode,
- body: { message: e.message },
- });
- }
-
- return response.customError({
- statusCode: 500,
- body: { message: e.message },
- });
- }
-};
-
export const postAgentEnrollHandler: RequestHandler<
undefined,
undefined,
diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts
index 24c541b88e928..414d2d79e9067 100644
--- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts
@@ -31,10 +31,12 @@ import {
getAgentEventsHandler,
postAgentCheckinHandler,
postAgentEnrollHandler,
- postAgentAcksHandler,
postAgentsUnenrollHandler,
getAgentStatusForConfigHandler,
+ getInternalUserSOClient,
} from './handlers';
+import { postAgentAcksHandlerBuilder } from './acks_handlers';
+import * as AgentService from '../../services/agents';
export const registerRoutes = (router: IRouter) => {
// Get one
@@ -101,7 +103,12 @@ export const registerRoutes = (router: IRouter) => {
validate: PostAgentAcksRequestSchema,
options: { tags: [] },
},
- postAgentAcksHandler
+ postAgentAcksHandlerBuilder({
+ acknowledgeAgentActions: AgentService.acknowledgeAgentActions,
+ getAgentByAccessAPIKeyId: AgentService.getAgentByAccessAPIKeyId,
+ getSavedObjectsClientContract: getInternalUserSOClient,
+ saveAgentEvents: AgentService.saveAgentEvents,
+ })
);
router.post(
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts
new file mode 100644
index 0000000000000..3c07463e3af5d
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts
@@ -0,0 +1,118 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock';
+import { Agent, AgentAction, AgentEvent } from '../../../common/types/models';
+import { AGENT_TYPE_PERMANENT } from '../../../common/constants';
+import { acknowledgeAgentActions } from './acks';
+import { isBoom } from 'boom';
+
+describe('test agent acks services', () => {
+ it('should succeed on valid and matched actions', async () => {
+ const mockSavedObjectsClient = savedObjectsClientMock.create();
+ const agentActions = await acknowledgeAgentActions(
+ mockSavedObjectsClient,
+ ({
+ id: 'id',
+ type: AGENT_TYPE_PERMANENT,
+ actions: [
+ {
+ type: 'CONFIG_CHANGE',
+ id: 'action1',
+ sent_at: '2020-03-14T19:45:02.620Z',
+ timestamp: '2019-01-04T14:32:03.36764-05:00',
+ created_at: '2020-03-14T19:45:02.620Z',
+ },
+ ],
+ } as unknown) as Agent,
+ [
+ {
+ type: 'ACTION_RESULT',
+ subtype: 'CONFIG',
+ timestamp: '2019-01-04T14:32:03.36764-05:00',
+ action_id: 'action1',
+ agent_id: 'id',
+ } as AgentEvent,
+ ]
+ );
+ expect(agentActions).toEqual([
+ ({
+ type: 'CONFIG_CHANGE',
+ id: 'action1',
+ sent_at: '2020-03-14T19:45:02.620Z',
+ timestamp: '2019-01-04T14:32:03.36764-05:00',
+ created_at: '2020-03-14T19:45:02.620Z',
+ } as unknown) as AgentAction,
+ ]);
+ });
+
+ it('should fail for actions that cannot be found on agent actions list', async () => {
+ const mockSavedObjectsClient = savedObjectsClientMock.create();
+ try {
+ await acknowledgeAgentActions(
+ mockSavedObjectsClient,
+ ({
+ id: 'id',
+ type: AGENT_TYPE_PERMANENT,
+ actions: [
+ {
+ type: 'CONFIG_CHANGE',
+ id: 'action1',
+ sent_at: '2020-03-14T19:45:02.620Z',
+ timestamp: '2019-01-04T14:32:03.36764-05:00',
+ created_at: '2020-03-14T19:45:02.620Z',
+ },
+ ],
+ } as unknown) as Agent,
+ [
+ ({
+ type: 'ACTION_RESULT',
+ subtype: 'CONFIG',
+ timestamp: '2019-01-04T14:32:03.36764-05:00',
+ action_id: 'action2',
+ agent_id: 'id',
+ } as unknown) as AgentEvent,
+ ]
+ );
+ expect(true).toBeFalsy();
+ } catch (e) {
+ expect(isBoom(e)).toBeTruthy();
+ }
+ });
+
+ it('should fail for events that have types not in the allowed acknowledgement type list', async () => {
+ const mockSavedObjectsClient = savedObjectsClientMock.create();
+ try {
+ await acknowledgeAgentActions(
+ mockSavedObjectsClient,
+ ({
+ id: 'id',
+ type: AGENT_TYPE_PERMANENT,
+ actions: [
+ {
+ type: 'CONFIG_CHANGE',
+ id: 'action1',
+ sent_at: '2020-03-14T19:45:02.620Z',
+ timestamp: '2019-01-04T14:32:03.36764-05:00',
+ created_at: '2020-03-14T19:45:02.620Z',
+ },
+ ],
+ } as unknown) as Agent,
+ [
+ ({
+ type: 'ACTION',
+ subtype: 'FAILED',
+ timestamp: '2019-01-04T14:32:03.36764-05:00',
+ action_id: 'action1',
+ agent_id: 'id',
+ } as unknown) as AgentEvent,
+ ]
+ );
+ expect(true).toBeFalsy();
+ } catch (e) {
+ expect(isBoom(e)).toBeTruthy();
+ }
+ });
+});
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts
index 21120fc20b29f..98a5f69f9d2b0 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts
@@ -4,25 +4,100 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SavedObjectsClientContract } from 'src/core/server';
-import { Agent, AgentSOAttributes } from '../../types';
-import { AGENT_SAVED_OBJECT_TYPE } from '../../constants';
+import {
+ KibanaRequest,
+ SavedObjectsBulkCreateObject,
+ SavedObjectsBulkResponse,
+ SavedObjectsClientContract,
+} from 'src/core/server';
+import Boom from 'boom';
+import {
+ Agent,
+ AgentAction,
+ AgentEvent,
+ AgentEventSOAttributes,
+ AgentSOAttributes,
+} from '../../types';
+import { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../constants';
+
+const ALLOWED_ACKNOWLEDGEMENT_TYPE: string[] = ['ACTION_RESULT'];
export async function acknowledgeAgentActions(
soClient: SavedObjectsClientContract,
agent: Agent,
- actionIds: string[]
-) {
+ agentEvents: AgentEvent[]
+): Promise {
const now = new Date().toISOString();
- const updatedActions = agent.actions.map(action => {
- if (action.sent_at) {
- return action;
+ const agentActionMap: Map = new Map(
+ agent.actions.map(agentAction => [agentAction.id, agentAction])
+ );
+
+ const matchedUpdatedActions: AgentAction[] = [];
+
+ agentEvents.forEach(agentEvent => {
+ if (!isAllowedType(agentEvent.type)) {
+ throw Boom.badRequest(`${agentEvent.type} not allowed for acknowledgment only ACTION_RESULT`);
+ }
+ if (agentActionMap.has(agentEvent.action_id!)) {
+ const action = agentActionMap.get(agentEvent.action_id!) as AgentAction;
+ if (!action.sent_at) {
+ action.sent_at = now;
+ }
+ matchedUpdatedActions.push(action);
+ } else {
+ throw Boom.badRequest('all actions should belong to current agent');
}
- return { ...action, sent_at: actionIds.indexOf(action.id) >= 0 ? now : undefined };
});
- await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, {
- actions: updatedActions,
- });
+ if (matchedUpdatedActions.length > 0) {
+ await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, {
+ actions: matchedUpdatedActions,
+ });
+ }
+
+ return matchedUpdatedActions;
+}
+
+function isAllowedType(eventType: string): boolean {
+ return ALLOWED_ACKNOWLEDGEMENT_TYPE.indexOf(eventType) >= 0;
+}
+
+export async function saveAgentEvents(
+ soClient: SavedObjectsClientContract,
+ events: AgentEvent[]
+): Promise> {
+ const objects: Array> = events.map(
+ eventData => {
+ return {
+ attributes: {
+ ...eventData,
+ payload: eventData.payload ? JSON.stringify(eventData.payload) : undefined,
+ },
+ type: AGENT_EVENT_SAVED_OBJECT_TYPE,
+ };
+ }
+ );
+
+ return await soClient.bulkCreate(objects);
+}
+
+export interface AcksService {
+ acknowledgeAgentActions: (
+ soClient: SavedObjectsClientContract,
+ agent: Agent,
+ actionIds: AgentEvent[]
+ ) => Promise;
+
+ getAgentByAccessAPIKeyId: (
+ soClient: SavedObjectsClientContract,
+ accessAPIKeyId: string
+ ) => Promise;
+
+ getSavedObjectsClientContract: (kibanaRequest: KibanaRequest) => SavedObjectsClientContract;
+
+ saveAgentEvents: (
+ soClient: SavedObjectsClientContract,
+ events: AgentEvent[]
+ ) => Promise>;
}
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts
index c301d2ecb6cf3..41bd2476c99a1 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts
@@ -68,7 +68,7 @@ export async function getAgent(soClient: SavedObjectsClientContract, agentId: st
export async function getAgentByAccessAPIKeyId(
soClient: SavedObjectsClientContract,
accessAPIKeyId: string
-) {
+): Promise {
const response = await soClient.find({
type: AGENT_SAVED_OBJECT_TYPE,
searchFields: ['access_api_key_id'],
diff --git a/x-pack/plugins/ingest_manager/server/types/models/agent.ts b/x-pack/plugins/ingest_manager/server/types/models/agent.ts
index 276dddf9e3d1c..e0d252faaaf87 100644
--- a/x-pack/plugins/ingest_manager/server/types/models/agent.ts
+++ b/x-pack/plugins/ingest_manager/server/types/models/agent.ts
@@ -44,6 +44,11 @@ const AgentEventBase = {
stream_id: schema.maybe(schema.string()),
};
+export const AckEventSchema = schema.object({
+ ...AgentEventBase,
+ ...{ action_id: schema.string() },
+});
+
export const AgentEventSchema = schema.object({
...AgentEventBase,
});
diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts
index 92422274d5cf4..9fe84c12521ad 100644
--- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts
+++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts
@@ -5,7 +5,7 @@
*/
import { schema } from '@kbn/config-schema';
-import { AgentEventSchema, AgentTypeSchema } from '../models';
+import { AckEventSchema, AgentEventSchema, AgentTypeSchema } from '../models';
export const GetAgentsRequestSchema = {
query: schema.object({
@@ -45,7 +45,7 @@ export const PostAgentEnrollRequestSchema = {
export const PostAgentAcksRequestSchema = {
body: schema.object({
- action_ids: schema.arrayOf(schema.string()),
+ events: schema.arrayOf(AckEventSchema),
}),
params: schema.object({
agentId: schema.string(),
diff --git a/x-pack/plugins/maps/common/map_saved_object_type.ts b/x-pack/plugins/maps/common/map_saved_object_type.ts
new file mode 100644
index 0000000000000..e5b4876186fd8
--- /dev/null
+++ b/x-pack/plugins/maps/common/map_saved_object_type.ts
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+/* eslint-disable @typescript-eslint/consistent-type-definitions */
+
+import { SavedObject } from '../../../../src/core/types/saved_objects';
+
+export type MapSavedObjectAttributes = {
+ title?: string;
+ description?: string;
+ mapStateJSON?: string;
+ layerListJSON?: string;
+ uiStateJSON?: string;
+ bounds?: {
+ type?: string;
+ coordinates?: [];
+ };
+};
+
+export type MapSavedObject = SavedObject;
diff --git a/x-pack/test/api_integration/apis/fleet/agents/acks.ts b/x-pack/test/api_integration/apis/fleet/agents/acks.ts
index 1ab54554d62f0..a2eba2c23c39d 100644
--- a/x-pack/test/api_integration/apis/fleet/agents/acks.ts
+++ b/x-pack/test/api_integration/apis/fleet/agents/acks.ts
@@ -59,7 +59,7 @@ export default function(providerContext: FtrProviderContext) {
.expect(401);
});
- it('should return a 200 if this a valid acks access', async () => {
+ it('should return a 200 if this a valid acks request', async () => {
const { body: apiResponse } = await supertest
.post(`/api/ingest_manager/fleet/agents/agent1/acks`)
.set('kbn-xsrf', 'xx')
@@ -68,12 +68,144 @@ export default function(providerContext: FtrProviderContext) {
`ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}`
)
.send({
- action_ids: ['action1'],
+ events: [
+ {
+ type: 'ACTION_RESULT',
+ subtype: 'CONFIG',
+ timestamp: '2019-01-04T14:32:03.36764-05:00',
+ action_id: '48cebde1-c906-4893-b89f-595d943b72a1',
+ agent_id: 'agent1',
+ message: 'hello',
+ payload: 'payload',
+ },
+ {
+ type: 'ACTION_RESULT',
+ subtype: 'CONFIG',
+ timestamp: '2019-01-05T14:32:03.36764-05:00',
+ action_id: '48cebde1-c906-4893-b89f-595d943b72a2',
+ agent_id: 'agent1',
+ message: 'hello2',
+ payload: 'payload2',
+ },
+ ],
})
.expect(200);
-
expect(apiResponse.action).to.be('acks');
expect(apiResponse.success).to.be(true);
+ const { body: eventResponse } = await supertest
+ .get(`/api/ingest_manager/fleet/agents/agent1/events`)
+ .set('kbn-xsrf', 'xx')
+ .set(
+ 'Authorization',
+ `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}`
+ )
+ .expect(200);
+ const expectedEvents = eventResponse.list.filter(
+ (item: Record) =>
+ item.action_id === '48cebde1-c906-4893-b89f-595d943b72a1' ||
+ item.action_id === '48cebde1-c906-4893-b89f-595d943b72a2'
+ );
+ expect(expectedEvents.length).to.eql(2);
+ const expectedEvent = expectedEvents.find(
+ (item: Record) => item.action_id === '48cebde1-c906-4893-b89f-595d943b72a1'
+ );
+ expect(expectedEvent).to.eql({
+ type: 'ACTION_RESULT',
+ subtype: 'CONFIG',
+ timestamp: '2019-01-04T14:32:03.36764-05:00',
+ action_id: '48cebde1-c906-4893-b89f-595d943b72a1',
+ agent_id: 'agent1',
+ message: 'hello',
+ payload: 'payload',
+ });
+ });
+
+ it('should return a 400 when request event list contains event for another agent id', async () => {
+ const { body: apiResponse } = await supertest
+ .post(`/api/ingest_manager/fleet/agents/agent1/acks`)
+ .set('kbn-xsrf', 'xx')
+ .set(
+ 'Authorization',
+ `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}`
+ )
+ .send({
+ events: [
+ {
+ type: 'ACTION_RESULT',
+ subtype: 'CONFIG',
+ timestamp: '2019-01-04T14:32:03.36764-05:00',
+ action_id: '48cebde1-c906-4893-b89f-595d943b72a1',
+ agent_id: 'agent2',
+ message: 'hello',
+ payload: 'payload',
+ },
+ ],
+ })
+ .expect(400);
+ expect(apiResponse.message).to.eql(
+ 'agent events contains events with different agent id from currently authorized agent'
+ );
+ });
+
+ it('should return a 400 when request event list contains action that does not belong to agent current actions', async () => {
+ const { body: apiResponse } = await supertest
+ .post(`/api/ingest_manager/fleet/agents/agent1/acks`)
+ .set('kbn-xsrf', 'xx')
+ .set(
+ 'Authorization',
+ `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}`
+ )
+ .send({
+ events: [
+ {
+ type: 'ACTION_RESULT',
+ subtype: 'CONFIG',
+ timestamp: '2019-01-04T14:32:03.36764-05:00',
+ action_id: '48cebde1-c906-4893-b89f-595d943b72a1',
+ agent_id: 'agent1',
+ message: 'hello',
+ payload: 'payload',
+ },
+ {
+ type: 'ACTION_RESULT',
+ subtype: 'CONFIG',
+ timestamp: '2019-01-04T14:32:03.36764-05:00',
+ action_id: 'does-not-exist',
+ agent_id: 'agent1',
+ message: 'hello',
+ payload: 'payload',
+ },
+ ],
+ })
+ .expect(400);
+ expect(apiResponse.message).to.eql('all actions should belong to current agent');
+ });
+
+ it('should return a 400 when request event list contains action types that are not allowed for acknowledgement', async () => {
+ const { body: apiResponse } = await supertest
+ .post(`/api/ingest_manager/fleet/agents/agent1/acks`)
+ .set('kbn-xsrf', 'xx')
+ .set(
+ 'Authorization',
+ `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}`
+ )
+ .send({
+ events: [
+ {
+ type: 'ACTION',
+ subtype: 'FAILED',
+ timestamp: '2019-01-04T14:32:03.36764-05:00',
+ action_id: '48cebde1-c906-4893-b89f-595d943b72a1',
+ agent_id: 'agent1',
+ message: 'hello',
+ payload: 'payload',
+ },
+ ],
+ })
+ .expect(400);
+ expect(apiResponse.message).to.eql(
+ 'ACTION not allowed for acknowledgment only ACTION_RESULT'
+ );
});
});
}
diff --git a/x-pack/test/functional/apps/endpoint/alert_list.ts b/x-pack/test/functional/apps/endpoint/alert_list.ts
index 4a92a8152b1ce..ac00258ff9c02 100644
--- a/x-pack/test/functional/apps/endpoint/alert_list.ts
+++ b/x-pack/test/functional/apps/endpoint/alert_list.ts
@@ -12,16 +12,20 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const browser = getService('browser');
- describe('Endpoint Alert List', function() {
+ describe('Endpoint Alert List page', function() {
this.tags(['ciGroup7']);
before(async () => {
await esArchiver.load('endpoint/alerts/api_feature');
await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/alerts');
});
- it('loads the Alert List Page', async () => {
+ it('loads in the browser', async () => {
await testSubjects.existOrFail('alertListPage');
});
+ it('contains the Alert List Page title', async () => {
+ const alertsTitle = await testSubjects.getVisibleText('alertsViewTitle');
+ expect(alertsTitle).to.equal('Alerts');
+ });
it('includes alerts search bar', async () => {
await testSubjects.existOrFail('alertsSearchBar');
});
diff --git a/x-pack/test/functional/es_archives/fleet/agents/data.json b/x-pack/test/functional/es_archives/fleet/agents/data.json
index 36928018d15a0..9b29767d5162d 100644
--- a/x-pack/test/functional/es_archives/fleet/agents/data.json
+++ b/x-pack/test/functional/es_archives/fleet/agents/data.json
@@ -23,6 +23,18 @@
"type": "PAUSE",
"created_at": "2019-09-04T15:01:07+0000",
"sent_at": "2019-09-04T15:03:07+0000"
+ },
+ {
+ "created_at" : "2020-03-15T03:47:15.129Z",
+ "id" : "48cebde1-c906-4893-b89f-595d943b72a1",
+ "type" : "CONFIG_CHANGE",
+ "sent_at": "2020-03-04T15:03:07+0000"
+ },
+ {
+ "created_at" : "2020-03-16T03:47:15.129Z",
+ "id" : "48cebde1-c906-4893-b89f-595d943b72a2",
+ "type" : "CONFIG_CHANGE",
+ "sent_at": "2020-03-04T15:03:07+0000"
}]
}
}