kibana/test/api_integration/apis/saved_queries/saved_queries.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

443 lines
15 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 {
ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common';
import { SavedQueryAttributes, SAVED_QUERY_BASE_URL } from '@kbn/data-plugin/common';
import { FtrProviderContext } from '../../ftr_provider_context';
// node scripts/functional_tests --config test/api_integration/config.js --grep="search session"
const mockSavedQuery: SavedQueryAttributes = {
title: 'my title',
description: 'my description',
query: {
query: 'foo: bar',
language: 'kql',
},
filters: [],
};
export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer');
const createQuery = (query: Partial<typeof mockSavedQuery> = mockSavedQuery) =>
supertest
.post(`${SAVED_QUERY_BASE_URL}/_create`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(query);
const updateQuery = (id: string, query: Partial<typeof mockSavedQuery> = mockSavedQuery) =>
supertest
.put(`${SAVED_QUERY_BASE_URL}/${id}`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(query);
const deleteQuery = (id: string) =>
supertest
.delete(`${SAVED_QUERY_BASE_URL}/${id}`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
const getQuery = (id: string) =>
supertest
.get(`${SAVED_QUERY_BASE_URL}/${id}`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
const findQueries = (options: { search?: string; perPage?: number; page?: number } = {}) =>
supertest
.post(`${SAVED_QUERY_BASE_URL}/_find`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(options);
const countQueries = () =>
supertest
.get(`${SAVED_QUERY_BASE_URL}/_count`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
const isDuplicateTitle = (title: string, id?: string) =>
supertest
.post(`${SAVED_QUERY_BASE_URL}/_is_duplicate_title`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ title, id });
describe('Saved queries API', function () {
before(async () => {
await esArchiver.emptyKibanaIndex();
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
});
after(async () => {
await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
});
afterEach(async () => {
await kibanaServer.savedObjects.clean({ types: ['query'] });
});
describe('create', () => {
it('should return 200 for create saved query', () =>
createQuery()
.expect(200)
.then(({ body }) => {
expect(body.id).to.have.length(36);
expect(body.attributes.title).to.be('my title');
expect(body.attributes.description).to.be('my description');
}));
it('should return 400 for create invalid saved query', () =>
createQuery({ description: 'my description' })
.expect(400)
.then(({ body }) => {
expect(body.message).to.be(
'[request body.title]: expected value of type [string] but got [undefined]'
);
}));
it('should return 400 for create saved query with duplicate title', () =>
createQuery()
.expect(200)
.then(() =>
createQuery()
.expect(400)
.then(({ body }) => {
expect(body.message).to.be('Query with title "my title" already exists');
})
));
it('should leave filters and timefilter undefined if not provided', () =>
createQuery({ ...mockSavedQuery, filters: undefined, timefilter: undefined })
.expect(200)
.then(({ body }) =>
getQuery(body.id)
.expect(200)
.then(({ body: body2 }) => {
expect(body.attributes.filters).to.be(undefined);
expect(body.attributes.timefilter).to.be(undefined);
expect(body2.attributes.filters).to.be(undefined);
expect(body2.attributes.timefilter).to.be(undefined);
})
));
});
describe('update', () => {
it('should return 200 for update saved query', () =>
createQuery()
.expect(200)
.then(({ body }) =>
updateQuery(body.id, {
...mockSavedQuery,
title: 'my updated title',
})
.expect(200)
.then((res) => {
expect(res.body.id).to.be(body.id);
expect(res.body.attributes.title).to.be('my updated title');
})
));
it('should return 404 for update non-existent saved query', () =>
updateQuery('invalid_id').expect(404));
it('should return 400 for update saved query with duplicate title', () =>
createQuery()
.expect(200)
.then(({ body }) =>
createQuery({ ...mockSavedQuery, title: 'my duplicate title' })
.expect(200)
.then(() =>
updateQuery(body.id, { ...mockSavedQuery, title: 'my duplicate title' })
.expect(400)
.then(({ body: body2 }) => {
expect(body2.message).to.be(
'Query with title "my duplicate title" already exists'
);
})
)
));
it('should remove filters and timefilter if not provided', () =>
createQuery({
...mockSavedQuery,
filters: [{ meta: {}, query: {} }],
timefilter: {
from: 'now-7d',
to: 'now',
refreshInterval: {
pause: false,
value: 60000,
},
},
})
.expect(200)
.then(({ body }) =>
updateQuery(body.id, {
...mockSavedQuery,
filters: undefined,
timefilter: undefined,
})
.expect(200)
.then(({ body: body2 }) =>
getQuery(body2.id)
.expect(200)
.then(({ body: body3 }) => {
expect(body.attributes.filters).not.to.be(undefined);
expect(body.attributes.timefilter).not.to.be(undefined);
expect(body2.attributes.filters).to.be(undefined);
expect(body2.attributes.timefilter).to.be(undefined);
expect(body3.attributes.filters).to.be(undefined);
expect(body3.attributes.timefilter).to.be(undefined);
})
)
));
});
describe('delete', () => {
it('should return 200 for delete saved query', () =>
createQuery()
.expect(200)
.then(({ body }) => deleteQuery(body.id).expect(200)));
it('should return 404 for delete non-existent saved query', () =>
deleteQuery('invalid_id').expect(404));
});
describe('get', () => {
it('should return 200 for get saved query', () =>
createQuery()
.expect(200)
.then(({ body }) =>
getQuery(body.id)
.expect(200)
.then((res) => {
expect(res.body.id).to.be(body.id);
expect(res.body.attributes.title).to.be(body.attributes.title);
})
));
it('should return 404 for get non-existent saved query', () =>
getQuery('invalid_id').expect(404));
});
describe('find', () => {
it('should return 200 for find saved queries', () => findQueries().expect(200));
it('should return 400 for bad find saved queries request', () =>
findQueries({ foo: 'bar' } as any)
.expect(400)
.then(({ body }) => {
expect(body.message).to.be('[request body.foo]: definition for this key is missing');
}));
it('should return expected queries for find saved queries', async () => {
await createQuery().expect(200);
const result = await createQuery({ ...mockSavedQuery, title: 'my title 2' }).expect(200);
await findQueries()
.expect(200)
.then((res) => {
expect(res.body.total).to.be(2);
expect(res.body.savedQueries.length).to.be(2);
expect(res.body.savedQueries.map((q: any) => q.attributes.title)).to.eql([
'my title',
'my title 2',
]);
});
await deleteQuery(result.body.id).expect(200);
await findQueries()
.expect(200)
.then((res) => {
expect(res.body.total).to.be(1);
expect(res.body.savedQueries.length).to.be(1);
expect(res.body.savedQueries.map((q: any) => q.attributes.title)).to.eql(['my title']);
});
});
it('should return expected queries for find saved queries with a search', async () => {
await createQuery().expect(200);
await createQuery({ ...mockSavedQuery, title: 'my title 2' }).expect(200);
const result = await createQuery({ ...mockSavedQuery, title: 'my title 2 again' }).expect(
200
);
await findQueries({ search: 'itle 2' })
.expect(200)
.then((res) => {
expect(res.body.total).to.be(2);
expect(res.body.savedQueries.length).to.be(2);
expect(res.body.savedQueries.map((q: any) => q.attributes.title)).to.eql([
'my title 2',
'my title 2 again',
]);
});
await deleteQuery(result.body.id).expect(200);
await findQueries({ search: 'itle 2' })
.expect(200)
.then((res) => {
expect(res.body.total).to.be(1);
expect(res.body.savedQueries.length).to.be(1);
expect(res.body.savedQueries.map((q: any) => q.attributes.title)).to.eql([
'my title 2',
]);
});
});
it('should support pagination for find saved queries', async () => {
await createQuery().expect(200);
await createQuery({ ...mockSavedQuery, title: 'my title 2' }).expect(200);
await createQuery({ ...mockSavedQuery, title: 'my title 3' }).expect(200);
await findQueries({ perPage: 2 })
.expect(200)
.then((res) => {
expect(res.body.total).to.be(3);
expect(res.body.savedQueries.length).to.be(2);
expect(res.body.savedQueries.map((q: any) => q.attributes.title)).to.eql([
'my title',
'my title 2',
]);
});
await findQueries({ perPage: 2, page: 2 })
.expect(200)
.then((res) => {
expect(res.body.total).to.be(3);
expect(res.body.savedQueries.length).to.be(1);
expect(res.body.savedQueries.map((q: any) => q.attributes.title)).to.eql([
'my title 3',
]);
});
});
it('should support pagination for find saved queries with a search', async () => {
await createQuery().expect(200);
await createQuery({ ...mockSavedQuery, title: 'my title 2' }).expect(200);
await createQuery({ ...mockSavedQuery, title: 'my title 3' }).expect(200);
await createQuery({ ...mockSavedQuery, title: 'not a match' }).expect(200);
await findQueries({ perPage: 2, search: 'itle' })
.expect(200)
.then((res) => {
expect(res.body.total).to.be(3);
expect(res.body.savedQueries.length).to.be(2);
expect(res.body.savedQueries.map((q: any) => q.attributes.title)).to.eql([
'my title',
'my title 2',
]);
});
await findQueries({ perPage: 2, page: 2, search: 'itle' })
.expect(200)
.then((res) => {
expect(res.body.total).to.be(3);
expect(res.body.savedQueries.length).to.be(1);
expect(res.body.savedQueries.map((q: any) => q.attributes.title)).to.eql([
'my title 3',
]);
});
});
it('should support searching for queries containing special characters', async () => {
await createQuery({ ...mockSavedQuery, title: 'query <> title' }).expect(200);
await findQueries({ search: 'ry <> ti' })
.expect(200)
.then((res) => {
expect(res.body.total).to.be(1);
expect(res.body.savedQueries.length).to.be(1);
expect(res.body.savedQueries.map((q: any) => q.attributes.title)).to.eql([
'query <> title',
]);
});
});
});
describe('count', () => {
it('should return 200 for saved query count', () => countQueries().expect(200));
it('should return expected counts for saved query count', async () => {
await countQueries()
.expect(200)
.then((res) => {
expect(res.text).to.be('0');
});
await createQuery().expect(200);
const result = await createQuery({ ...mockSavedQuery, title: 'my title 2' }).expect(200);
await countQueries()
.expect(200)
.then((res) => {
expect(res.text).to.be('2');
});
await deleteQuery(result.body.id).expect(200);
await countQueries()
.expect(200)
.then((res) => {
expect(res.text).to.be('1');
});
});
});
describe('isDuplicateTitle', () => {
it('should return isDuplicate = true for _is_duplicate_title check with a duplicate title', () =>
createQuery()
.expect(200)
.then(({ body }) =>
isDuplicateTitle(body.attributes.title)
.expect(200)
.then(({ body: body2 }) => {
expect(body2.isDuplicate).to.be(true);
})
));
it('should return isDuplicate = false for _is_duplicate_title check with a duplicate title and matching ID', () =>
createQuery()
.expect(200)
.then(({ body }) =>
isDuplicateTitle(body.attributes.title, body.id)
.expect(200)
.then(({ body: body2 }) => {
expect(body2.isDuplicate).to.be(false);
})
));
it('should return isDuplicate = false for _is_duplicate_title check with a unique title', () =>
createQuery()
.expect(200)
.then(() =>
isDuplicateTitle('my unique title')
.expect(200)
.then(({ body }) => {
expect(body.isDuplicate).to.be(false);
})
));
});
});
}