kibana/test/api_integration/apis/event_annotations/event_annotations.ts
Christiane (Tina) Heiligers 3a68f8b3ae
[http] api_integration tests handle internal route restriction (#192407)
fix https://github.com/elastic/kibana/issues/192052
## Summary

Internal APIs will be
[restricted](https://github.com/elastic/kibana/issues/163654) from
public access as of 9.0.0. In non-serverless environments, this breaking
change will result in a 400 error if an external request is made to an
internal Kibana API (route `access` option as `"internal"` or
`"public"`).
This PR allows API owners of non-xpack plugins to run their `ftr` API
integration tests against the restriction and adds examples of how to
handle it.

### Checklist
- [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


Note to reviewers: The header needed to allow access to internal apis
shouldn't change your test output, with or without the restriction
enabled.

### How to test the changes work:
#### Non x-pack:
1. Set `server.restrictInternalApis: true` in `test/common/config.js`
2. Ensure your tests pass

#### x-pack:
1. Set `server.restrictInternalApis: true` in
`x-pack/test/api_integration/apis/security/config.ts`
2. Ensure the spaces tests pass

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
2024-09-12 09:23:10 +02:00

519 lines
17 KiB
TypeScript

/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import expect from '@kbn/expect';
import type {
EventAnnotationGroupSavedObjectAttributes,
EventAnnotationGroupCreateIn,
EventAnnotationGroupCreateOut,
EventAnnotationGroupUpdateIn,
EventAnnotationGroupSearchIn,
EventAnnotationGroupSearchOut,
EventAnnotationGroupGetIn,
EventAnnotationGroupGetOut,
EventAnnotationGroupDeleteIn,
EventAnnotationGroupDeleteOut,
} from '@kbn/event-annotation-plugin/common';
import { CONTENT_ID } from '@kbn/event-annotation-plugin/common';
import { EVENT_ANNOTATION_GROUP_TYPE } from '@kbn/event-annotation-common';
import { X_ELASTIC_INTERNAL_ORIGIN_REQUEST } from '@kbn/core-http-common';
import { FtrProviderContext } from '../../ftr_provider_context';
const CONTENT_ENDPOINT = '/api/content_management/rpc';
const API_VERSION = 1;
// IDs come from from loaded archive
const EXISTING_ID_1 = '46c2a460-4e77-11ee-bb97-116581699678';
const EXISTING_ID_2 = '425d2760-4e77-11ee-bb97-116581699678';
const DESCRIPTION_2 = 'i am a description you can search for!';
const EXISTING_ID_3 = '3905a4d0-4e77-11ee-bb97-116581699678';
const TAG_ID = '36a8f020-4e77-11ee-bb97-116581699678';
const DEFAULT_EVENT_ANNOTATION_GROUP: EventAnnotationGroupSavedObjectAttributes = {
title: 'a group',
description: '',
ignoreGlobalFilters: true,
dataViewSpec: null,
annotations: [
{
label: 'Event',
type: 'manual',
key: {
type: 'point_in_time',
timestamp: '2023-08-10T15:00:00.000Z',
},
icon: 'triangle',
id: '499ee351-f541-46e0-b327-b3dcae91aff5',
},
],
};
const DEFAULT_REFERENCES = [
{
type: 'index-pattern',
id: '90943e30-9a47-11e8-b64d-95841ca0b247',
name: 'event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247',
},
];
export default function ({ getService }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const supertest = getService('supertest');
describe('group API', () => {
before(async () => {
await kibanaServer.savedObjects.clean({ types: [EVENT_ANNOTATION_GROUP_TYPE] });
await kibanaServer.importExport.load(
'test/api_integration/fixtures/kbn_archiver/event_annotations/event_annotations.json'
);
});
after(async () => {
await kibanaServer.importExport.unload(
'test/api_integration/fixtures/kbn_archiver/event_annotations/event_annotations.json'
);
await kibanaServer.savedObjects.clean({ types: [EVENT_ANNOTATION_GROUP_TYPE] });
});
describe('get', () => {
it(`should retrieve an existing group`, async () => {
const payload: EventAnnotationGroupGetIn = {
contentTypeId: CONTENT_ID,
id: EXISTING_ID_1,
version: API_VERSION,
};
const resp = await supertest
.post(`${CONTENT_ENDPOINT}/get`)
.set('kbn-xsrf', 'kibana')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(payload)
.expect(200);
const result = resp.body.result.result as EventAnnotationGroupGetOut;
expect(result.item.id).to.be(EXISTING_ID_1);
expect(result.meta.outcome).to.be('exactMatch');
expect(result.item.references.length).to.be(1);
expect(result.item.attributes).to.eql({
annotations: [
{
filter: {
language: 'kuery',
query:
'agent.keyword : "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)" ',
type: 'kibana_query',
},
icon: 'triangle',
id: 'fdede168-eff1-400f-b106-0f62061f5099',
key: {
type: 'point_in_time',
},
label: 'Event',
timeField: 'timestamp',
type: 'query',
},
],
dataViewSpec: null,
description: '',
ignoreGlobalFilters: true,
title: 'group3',
});
});
it(`should reject a group that does not exist`, async () => {
const payload: EventAnnotationGroupGetIn = {
contentTypeId: CONTENT_ID,
id: 'does-not-exist',
version: API_VERSION,
};
const resp = await supertest
.post(`${CONTENT_ENDPOINT}/get`)
.set('kbn-xsrf', 'kibana')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(payload)
.expect(404);
expect(resp.body).to.eql({
error: 'Not Found',
message: 'Saved object [event-annotation-group/does-not-exist] not found',
statusCode: 404,
});
});
});
describe('search', () => {
const performSearch = (payload: EventAnnotationGroupSearchIn) =>
supertest
.post(`${CONTENT_ENDPOINT}/search`)
.set('kbn-xsrf', 'kibana')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(payload);
it(`should retrieve existing groups`, async () => {
const payload: EventAnnotationGroupSearchIn = {
contentTypeId: CONTENT_ID,
query: {
limit: 1000,
tags: {
included: [],
excluded: [],
},
},
version: API_VERSION,
};
const resp = await performSearch(payload).expect(200);
const results = resp.body.result.result.hits;
expect(results.length).to.be(3);
expect(results.map(({ id }: { id: string }) => id)).to.eql([
EXISTING_ID_1,
EXISTING_ID_2,
EXISTING_ID_3,
]);
});
it(`should filter by tag`, async () => {
const payload: EventAnnotationGroupSearchIn = {
contentTypeId: CONTENT_ID,
query: {
limit: 1000,
tags: {
included: [TAG_ID],
excluded: [],
},
},
version: API_VERSION,
};
const resp = await performSearch(payload).expect(200);
const result = resp.body.result.result as EventAnnotationGroupSearchOut;
expect(result.hits.length).to.be(2);
expect(
result.hits.every(({ references }) => references.map(({ id }) => id).includes(TAG_ID))
).to.be(true);
});
it(`should filter by text`, async () => {
const payload: EventAnnotationGroupSearchIn = {
contentTypeId: CONTENT_ID,
query: {
limit: 1000,
text: DESCRIPTION_2,
tags: {
included: [],
excluded: [],
},
},
version: API_VERSION,
};
const resp = await performSearch(payload).expect(200);
const result = resp.body.result.result as EventAnnotationGroupSearchOut;
expect(result.hits.length).to.be(1);
expect(result.hits[0].id).to.be(EXISTING_ID_2);
});
it(`should paginate`, async () => {
const payload: EventAnnotationGroupSearchIn = {
contentTypeId: CONTENT_ID,
query: {
limit: 1,
cursor: '1',
tags: {
included: [],
excluded: [],
},
},
version: API_VERSION,
};
const resp = await performSearch(payload).expect(200);
const result = resp.body.result.result as EventAnnotationGroupSearchOut;
expect(result.hits.length).to.be(1);
expect(result.hits[0].id).to.be(EXISTING_ID_1);
expect(result.pagination.total).to.be(3);
// get second page
payload.query.cursor = '2';
const resp2 = await performSearch(payload).expect(200);
const result2 = resp2.body.result.result as EventAnnotationGroupSearchOut;
expect(result2.hits.length).to.be(1);
expect(result2.hits[0].id).to.be(EXISTING_ID_2);
expect(result2.pagination.total).to.be(3);
// get third page
payload.query.cursor = '3';
const resp3 = await performSearch(payload).expect(200);
const result3 = resp3.body.result.result as EventAnnotationGroupSearchOut;
expect(result3.hits.length).to.be(1);
expect(result3.hits[0].id).to.be(EXISTING_ID_3);
expect(result3.pagination.total).to.be(3);
});
});
describe('create', () => {
it(`should create a new group`, async () => {
const payload: EventAnnotationGroupCreateIn = {
contentTypeId: CONTENT_ID,
data: DEFAULT_EVENT_ANNOTATION_GROUP,
options: {
references: DEFAULT_REFERENCES,
},
version: API_VERSION,
};
const resp = await supertest
.post(`${CONTENT_ENDPOINT}/create`)
.set('kbn-xsrf', 'kibana')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(payload)
.expect(200);
const result = resp.body.result.result as EventAnnotationGroupCreateOut;
expect(result.item.attributes).to.eql(DEFAULT_EVENT_ANNOTATION_GROUP);
expect(result.item.id).to.be.a('string');
expect(result.item.namespaces).to.eql(['default']);
});
it(`should reject malformed groups`, async () => {
const badGroups = [
// extra property
{
...DEFAULT_EVENT_ANNOTATION_GROUP,
extraProp: 'some-value',
},
// missing title
{
...DEFAULT_EVENT_ANNOTATION_GROUP,
title: undefined,
},
// wrong type for property
{
...DEFAULT_EVENT_ANNOTATION_GROUP,
ignoreGlobalFilters: 'not-a-boolean',
},
] as unknown as EventAnnotationGroupSavedObjectAttributes[]; // (coerce the types because these are intentionally malformed)
const expectedMessages = [
'Invalid data. [extraProp]: definition for this key is missing',
'Invalid data. [title]: expected value of type [string] but got [undefined]',
'Invalid data. [ignoreGlobalFilters]: expected value of type [boolean] but got [string]',
];
for (let i = 0; i < badGroups.length; i++) {
const payload: EventAnnotationGroupCreateIn = {
contentTypeId: CONTENT_ID,
data: badGroups[i],
options: {
references: DEFAULT_REFERENCES,
},
version: API_VERSION,
};
const resp = await supertest
.post(`${CONTENT_ENDPOINT}/create`)
.set('kbn-xsrf', 'kibana')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(payload)
.expect(400);
expect(resp.body.message).to.be(expectedMessages[i]);
}
});
it(`should require dataViewSpec to be specified`, async () => {
const createWithDataViewSpec = (dataViewSpec: any) => {
const payload: EventAnnotationGroupCreateIn = {
contentTypeId: CONTENT_ID,
data: { ...DEFAULT_EVENT_ANNOTATION_GROUP, dataViewSpec },
options: {
references: DEFAULT_REFERENCES,
},
version: API_VERSION,
};
return supertest
.post(`${CONTENT_ENDPOINT}/create`)
.set('kbn-xsrf', 'kibana')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(payload);
};
const errorResp = await createWithDataViewSpec(undefined).expect(400);
expect(errorResp.body.message).to.be(
'Invalid data. [dataViewSpec]: expected at least one defined value but got [undefined]'
);
await createWithDataViewSpec(null).expect(200);
await createWithDataViewSpec({
someDataViewProp: 'some-value',
}).expect(200);
});
});
describe('update', () => {
it(`should update a group`, async () => {
const payload: EventAnnotationGroupUpdateIn = {
contentTypeId: CONTENT_ID,
data: {
...DEFAULT_EVENT_ANNOTATION_GROUP,
description: 'updated description',
},
id: EXISTING_ID_1,
options: {
references: DEFAULT_REFERENCES,
},
version: API_VERSION,
};
const resp = await supertest
.post(`${CONTENT_ENDPOINT}/update`)
.set('kbn-xsrf', 'kibana')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(payload)
.expect(200);
const result = resp.body.result.result as EventAnnotationGroupCreateOut;
expect(result.item.attributes).to.eql({
...DEFAULT_EVENT_ANNOTATION_GROUP,
description: 'updated description',
});
expect(result.item.id).to.be(EXISTING_ID_1);
});
it(`should reject malformed groups`, async () => {
const badGroups = [
// extra property
{
...DEFAULT_EVENT_ANNOTATION_GROUP,
extraProp: 'some-value',
},
// missing title
{
...DEFAULT_EVENT_ANNOTATION_GROUP,
title: undefined,
},
// wrong type for property
{
...DEFAULT_EVENT_ANNOTATION_GROUP,
ignoreGlobalFilters: 'not-a-boolean',
},
] as unknown as EventAnnotationGroupSavedObjectAttributes[]; // (coerce the types because these are intentionally malformed)
const expectedMessages = [
'Invalid data. [extraProp]: definition for this key is missing',
'Invalid data. [title]: expected value of type [string] but got [undefined]',
'Invalid data. [ignoreGlobalFilters]: expected value of type [boolean] but got [string]',
];
for (let i = 0; i < badGroups.length; i++) {
const payload: EventAnnotationGroupUpdateIn = {
contentTypeId: CONTENT_ID,
data: badGroups[i],
id: EXISTING_ID_1,
options: {
references: DEFAULT_REFERENCES,
},
version: API_VERSION,
};
const resp = await supertest
.post(`${CONTENT_ENDPOINT}/update`)
.set('kbn-xsrf', 'kibana')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(payload)
.expect(400);
expect(resp.body.message).to.be(expectedMessages[i]);
}
});
it(`should require dataViewSpec to be specified`, async () => {
const updateWithDataViewSpec = (dataViewSpec: any) => {
const payload: EventAnnotationGroupUpdateIn = {
contentTypeId: CONTENT_ID,
data: { ...DEFAULT_EVENT_ANNOTATION_GROUP, dataViewSpec },
id: EXISTING_ID_1,
options: {
references: DEFAULT_REFERENCES,
},
version: API_VERSION,
};
return supertest
.post(`${CONTENT_ENDPOINT}/update`)
.set('kbn-xsrf', 'kibana')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(payload);
};
const errorResp = await updateWithDataViewSpec(undefined).expect(400);
expect(errorResp.body.message).to.be(
'Invalid data. [dataViewSpec]: expected at least one defined value but got [undefined]'
);
await updateWithDataViewSpec(null).expect(200);
await updateWithDataViewSpec({
someDataViewProp: 'some-value',
}).expect(200);
});
});
describe('delete', () => {
const deleteGroupByID = (id: string) => {
const payload: EventAnnotationGroupDeleteIn = {
contentTypeId: CONTENT_ID,
id,
version: API_VERSION,
};
return supertest
.post(`${CONTENT_ENDPOINT}/delete`)
.set('kbn-xsrf', 'kibana')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(payload);
};
it(`should delete a group`, async () => {
const resp = await deleteGroupByID(EXISTING_ID_1).expect(200);
const result = resp.body.result.result as EventAnnotationGroupDeleteOut;
expect(result.success).to.be(true);
});
it(`should reject deleting a group that does not exist`, async () => {
const resp = await deleteGroupByID('does-not-exist').expect(404);
expect(resp.body).to.eql({
error: 'Not Found',
message: 'Saved object [event-annotation-group/does-not-exist] not found',
statusCode: 404,
});
});
});
});
}