Remove ability to implicitly find across all types (#23198)
This commit is contained in:
parent
c940c6d111
commit
33acd60f9f
13 changed files with 109 additions and 334 deletions
|
@ -11,13 +11,12 @@ saved objects by various conditions.
|
||||||
`GET /api/saved_objects/_find`
|
`GET /api/saved_objects/_find`
|
||||||
|
|
||||||
==== Query Parameters
|
==== Query Parameters
|
||||||
|
`type` (required)::
|
||||||
|
(array|string) The saved object type(s) that the response should be limited to
|
||||||
`per_page` (optional)::
|
`per_page` (optional)::
|
||||||
(number) The number of objects to return per page
|
(number) The number of objects to return per page
|
||||||
`page` (optional)::
|
`page` (optional)::
|
||||||
(number) The page of objects to return
|
(number) The page of objects to return
|
||||||
`type` (optional)::
|
|
||||||
(array|string) The saved object type(s) that the response should be limited to
|
|
||||||
`search` (optional)::
|
`search` (optional)::
|
||||||
(string) A {ref}/query-dsl-simple-query-string-query.html[simple_query_string] Elasticsearch query to filter the objects in the response
|
(string) A {ref}/query-dsl-simple-query-string-query.html[simple_query_string] Elasticsearch query to filter the objects in the response
|
||||||
`search_fields` (optional)::
|
`search_fields` (optional)::
|
||||||
|
|
|
@ -29,7 +29,7 @@ export const createFindRoute = (prereqs) => ({
|
||||||
query: Joi.object().keys({
|
query: Joi.object().keys({
|
||||||
per_page: Joi.number().min(0).default(20),
|
per_page: Joi.number().min(0).default(20),
|
||||||
page: Joi.number().min(0).default(1),
|
page: Joi.number().min(0).default(1),
|
||||||
type: Joi.array().items(Joi.string()).single(),
|
type: Joi.array().items(Joi.string()).single().required(),
|
||||||
search: Joi.string().allow('').optional(),
|
search: Joi.string().allow('').optional(),
|
||||||
search_fields: Joi.array().items(Joi.string()).single(),
|
search_fields: Joi.array().items(Joi.string()).single(),
|
||||||
sort_field: Joi.array().items(Joi.string()).single(),
|
sort_field: Joi.array().items(Joi.string()).single(),
|
||||||
|
|
|
@ -44,12 +44,28 @@ describe('GET /api/saved_objects/_find', () => {
|
||||||
savedObjectsClient.find.resetHistory();
|
savedObjectsClient.find.resetHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats successful response', async () => {
|
it('returns with status 400 when type is missing', async () => {
|
||||||
const request = {
|
const request = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/saved_objects/_find'
|
url: '/api/saved_objects/_find'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { payload, statusCode } = await server.inject(request);
|
||||||
|
|
||||||
|
expect(statusCode).toEqual(400);
|
||||||
|
expect(JSON.parse(payload)).toMatchObject({
|
||||||
|
statusCode: 400,
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'child "type" fails because ["type" is required]',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats successful response', async () => {
|
||||||
|
const request = {
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/saved_objects/_find?type=index-pattern'
|
||||||
|
};
|
||||||
|
|
||||||
const clientResponse = {
|
const clientResponse = {
|
||||||
total: 2,
|
total: 2,
|
||||||
data: [
|
data: [
|
||||||
|
@ -81,7 +97,7 @@ describe('GET /api/saved_objects/_find', () => {
|
||||||
it('calls upon savedObjectClient.find with defaults', async () => {
|
it('calls upon savedObjectClient.find with defaults', async () => {
|
||||||
const request = {
|
const request = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/saved_objects/_find'
|
url: '/api/saved_objects/_find?type=foo&type=bar'
|
||||||
};
|
};
|
||||||
|
|
||||||
await server.inject(request);
|
await server.inject(request);
|
||||||
|
@ -89,13 +105,13 @@ describe('GET /api/saved_objects/_find', () => {
|
||||||
expect(savedObjectsClient.find.calledOnce).toBe(true);
|
expect(savedObjectsClient.find.calledOnce).toBe(true);
|
||||||
|
|
||||||
const options = savedObjectsClient.find.getCall(0).args[0];
|
const options = savedObjectsClient.find.getCall(0).args[0];
|
||||||
expect(options).toEqual({ perPage: 20, page: 1 });
|
expect(options).toEqual({ perPage: 20, page: 1, type: ['foo', 'bar'] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts the query parameter page/per_page', async () => {
|
it('accepts the query parameter page/per_page', async () => {
|
||||||
const request = {
|
const request = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/saved_objects/_find?per_page=10&page=50'
|
url: '/api/saved_objects/_find?type=foo&per_page=10&page=50'
|
||||||
};
|
};
|
||||||
|
|
||||||
await server.inject(request);
|
await server.inject(request);
|
||||||
|
@ -103,13 +119,13 @@ describe('GET /api/saved_objects/_find', () => {
|
||||||
expect(savedObjectsClient.find.calledOnce).toBe(true);
|
expect(savedObjectsClient.find.calledOnce).toBe(true);
|
||||||
|
|
||||||
const options = savedObjectsClient.find.getCall(0).args[0];
|
const options = savedObjectsClient.find.getCall(0).args[0];
|
||||||
expect(options).toEqual({ perPage: 10, page: 50 });
|
expect(options).toEqual({ perPage: 10, page: 50, type: ['foo'] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts the query parameter search_fields', async () => {
|
it('accepts the query parameter search_fields', async () => {
|
||||||
const request = {
|
const request = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/saved_objects/_find?search_fields=title'
|
url: '/api/saved_objects/_find?type=foo&search_fields=title'
|
||||||
};
|
};
|
||||||
|
|
||||||
await server.inject(request);
|
await server.inject(request);
|
||||||
|
@ -117,13 +133,13 @@ describe('GET /api/saved_objects/_find', () => {
|
||||||
expect(savedObjectsClient.find.calledOnce).toBe(true);
|
expect(savedObjectsClient.find.calledOnce).toBe(true);
|
||||||
|
|
||||||
const options = savedObjectsClient.find.getCall(0).args[0];
|
const options = savedObjectsClient.find.getCall(0).args[0];
|
||||||
expect(options).toEqual({ perPage: 20, page: 1, searchFields: ['title'] });
|
expect(options).toEqual({ perPage: 20, page: 1, searchFields: ['title'], type: ['foo'] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts the query parameter fields as a string', async () => {
|
it('accepts the query parameter fields as a string', async () => {
|
||||||
const request = {
|
const request = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/saved_objects/_find?fields=title'
|
url: '/api/saved_objects/_find?type=foo&fields=title'
|
||||||
};
|
};
|
||||||
|
|
||||||
await server.inject(request);
|
await server.inject(request);
|
||||||
|
@ -131,13 +147,13 @@ describe('GET /api/saved_objects/_find', () => {
|
||||||
expect(savedObjectsClient.find.calledOnce).toBe(true);
|
expect(savedObjectsClient.find.calledOnce).toBe(true);
|
||||||
|
|
||||||
const options = savedObjectsClient.find.getCall(0).args[0];
|
const options = savedObjectsClient.find.getCall(0).args[0];
|
||||||
expect(options).toEqual({ perPage: 20, page: 1, fields: ['title'] });
|
expect(options).toEqual({ perPage: 20, page: 1, fields: ['title'], type: ['foo'] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts the query parameter fields as an array', async () => {
|
it('accepts the query parameter fields as an array', async () => {
|
||||||
const request = {
|
const request = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/saved_objects/_find?fields=title&fields=description'
|
url: '/api/saved_objects/_find?type=foo&fields=title&fields=description'
|
||||||
};
|
};
|
||||||
|
|
||||||
await server.inject(request);
|
await server.inject(request);
|
||||||
|
@ -146,7 +162,7 @@ describe('GET /api/saved_objects/_find', () => {
|
||||||
|
|
||||||
const options = savedObjectsClient.find.getCall(0).args[0];
|
const options = savedObjectsClient.find.getCall(0).args[0];
|
||||||
expect(options).toEqual({
|
expect(options).toEqual({
|
||||||
perPage: 20, page: 1, fields: ['title', 'description']
|
perPage: 20, page: 1, fields: ['title', 'description'], type: ['foo']
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -161,7 +177,7 @@ describe('GET /api/saved_objects/_find', () => {
|
||||||
expect(savedObjectsClient.find.calledOnce).toBe(true);
|
expect(savedObjectsClient.find.calledOnce).toBe(true);
|
||||||
|
|
||||||
const options = savedObjectsClient.find.getCall(0).args[0];
|
const options = savedObjectsClient.find.getCall(0).args[0];
|
||||||
expect(options).toEqual({ perPage: 20, page: 1, type: [ 'index-pattern' ] });
|
expect(options).toEqual({ perPage: 20, page: 1, type: ['index-pattern'] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts the query parameter type as an array', async () => {
|
it('accepts the query parameter type as an array', async () => {
|
||||||
|
|
|
@ -251,6 +251,10 @@ export class SavedObjectsRepository {
|
||||||
fields,
|
fields,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
throw new TypeError(`options.type must be a string or an array of strings`);
|
||||||
|
}
|
||||||
|
|
||||||
if (searchFields && !Array.isArray(searchFields)) {
|
if (searchFields && !Array.isArray(searchFields)) {
|
||||||
throw new TypeError('options.searchFields must be an array');
|
throw new TypeError('options.searchFields must be an array');
|
||||||
}
|
}
|
||||||
|
|
|
@ -385,7 +385,7 @@ describe('SavedObjectsRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#delete', () => {
|
describe('#delete', () => {
|
||||||
it('throws notFound when ES is unable to find the document', async () => {
|
it('throws notFound when ES is unable to find the document', async () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
callAdminCluster.returns(Promise.resolve({
|
callAdminCluster.returns(Promise.resolve({
|
||||||
|
@ -394,7 +394,7 @@ describe('SavedObjectsRepository', () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await savedObjectsRepository.delete('index-pattern', 'logstash-*');
|
await savedObjectsRepository.delete('index-pattern', 'logstash-*');
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
expect(e.output.statusCode).toEqual(404);
|
expect(e.output.statusCode).toEqual(404);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -423,9 +423,15 @@ describe('SavedObjectsRepository', () => {
|
||||||
callAdminCluster.returns(searchResults);
|
callAdminCluster.returns(searchResults);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('requires type to be defined', async () => {
|
||||||
|
await expect(savedObjectsRepository.find({})).rejects.toThrow(/options\.type must be/);
|
||||||
|
sinon.assert.notCalled(callAdminCluster);
|
||||||
|
sinon.assert.notCalled(onBeforeWrite);
|
||||||
|
});
|
||||||
|
|
||||||
it('requires searchFields be an array if defined', async () => {
|
it('requires searchFields be an array if defined', async () => {
|
||||||
try {
|
try {
|
||||||
await savedObjectsRepository.find({ searchFields: 'string' });
|
await savedObjectsRepository.find({ type: 'foo', searchFields: 'string' });
|
||||||
throw new Error('expected find() to reject');
|
throw new Error('expected find() to reject');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sinon.assert.notCalled(callAdminCluster);
|
sinon.assert.notCalled(callAdminCluster);
|
||||||
|
@ -436,7 +442,7 @@ describe('SavedObjectsRepository', () => {
|
||||||
|
|
||||||
it('requires fields be an array if defined', async () => {
|
it('requires fields be an array if defined', async () => {
|
||||||
try {
|
try {
|
||||||
await savedObjectsRepository.find({ fields: 'string' });
|
await savedObjectsRepository.find({ type: 'foo', fields: 'string' });
|
||||||
throw new Error('expected find() to reject');
|
throw new Error('expected find() to reject');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sinon.assert.notCalled(callAdminCluster);
|
sinon.assert.notCalled(callAdminCluster);
|
||||||
|
@ -461,7 +467,7 @@ describe('SavedObjectsRepository', () => {
|
||||||
|
|
||||||
it('merges output of getSearchDsl into es request body', async () => {
|
it('merges output of getSearchDsl into es request body', async () => {
|
||||||
getSearchDsl.returns({ query: 1, aggregations: 2 });
|
getSearchDsl.returns({ query: 1, aggregations: 2 });
|
||||||
await savedObjectsRepository.find();
|
await savedObjectsRepository.find({ type: 'foo' });
|
||||||
sinon.assert.calledOnce(callAdminCluster);
|
sinon.assert.calledOnce(callAdminCluster);
|
||||||
sinon.assert.notCalled(onBeforeWrite);
|
sinon.assert.notCalled(onBeforeWrite);
|
||||||
sinon.assert.calledWithExactly(callAdminCluster, 'search', sinon.match({
|
sinon.assert.calledWithExactly(callAdminCluster, 'search', sinon.match({
|
||||||
|
@ -475,7 +481,7 @@ describe('SavedObjectsRepository', () => {
|
||||||
it('formats Elasticsearch response', async () => {
|
it('formats Elasticsearch response', async () => {
|
||||||
const count = searchResults.hits.hits.length;
|
const count = searchResults.hits.hits.length;
|
||||||
|
|
||||||
const response = await savedObjectsRepository.find();
|
const response = await savedObjectsRepository.find({ type: 'foo' });
|
||||||
|
|
||||||
expect(response.total).toBe(count);
|
expect(response.total).toBe(count);
|
||||||
expect(response.saved_objects).toHaveLength(count);
|
expect(response.saved_objects).toHaveLength(count);
|
||||||
|
@ -492,7 +498,7 @@ describe('SavedObjectsRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts per_page/page', async () => {
|
it('accepts per_page/page', async () => {
|
||||||
await savedObjectsRepository.find({ perPage: 10, page: 6 });
|
await savedObjectsRepository.find({ type: 'foo', perPage: 10, page: 6 });
|
||||||
|
|
||||||
sinon.assert.calledOnce(callAdminCluster);
|
sinon.assert.calledOnce(callAdminCluster);
|
||||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||||
|
@ -504,12 +510,12 @@ describe('SavedObjectsRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can filter by fields', async () => {
|
it('can filter by fields', async () => {
|
||||||
await savedObjectsRepository.find({ fields: ['title'] });
|
await savedObjectsRepository.find({ type: 'foo', fields: ['title'] });
|
||||||
|
|
||||||
sinon.assert.calledOnce(callAdminCluster);
|
sinon.assert.calledOnce(callAdminCluster);
|
||||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||||
_source: [
|
_source: [
|
||||||
'*.title', 'type', 'title'
|
'foo.title', 'type', 'title'
|
||||||
]
|
]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,8 @@ export function getSearchDsl(mappings, options = {}) {
|
||||||
sortOrder
|
sortOrder
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
if (!type && sortField) {
|
if (!type) {
|
||||||
throw Boom.notAcceptable('Cannot sort without filtering by type');
|
throw Boom.notAcceptable('type must be specified');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortOrder && !sortField) {
|
if (sortOrder && !sortField) {
|
||||||
|
|
|
@ -27,13 +27,13 @@ describe('getSearchDsl', () => {
|
||||||
afterEach(() => sandbox.restore());
|
afterEach(() => sandbox.restore());
|
||||||
|
|
||||||
describe('validation', () => {
|
describe('validation', () => {
|
||||||
it('throws when sortField is passed without type', () => {
|
it('throws when type is not specified', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
getSearchDsl({}, {
|
getSearchDsl({}, {
|
||||||
type: undefined,
|
type: undefined,
|
||||||
sortField: 'title'
|
sortField: 'title'
|
||||||
});
|
});
|
||||||
}).toThrowError(/sort without .+ type/);
|
}).toThrowError(/type must be specified/);
|
||||||
});
|
});
|
||||||
it('throws when sortOrder without sortField', () => {
|
it('throws when sortOrder without sortField', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
|
@ -89,7 +89,7 @@ describe('getSearchDsl', () => {
|
||||||
it('returns combination of getQueryParams and getSortingParams', () => {
|
it('returns combination of getQueryParams and getSortingParams', () => {
|
||||||
sandbox.stub(queryParamsNS, 'getQueryParams').returns({ a: 'a' });
|
sandbox.stub(queryParamsNS, 'getQueryParams').returns({ a: 'a' });
|
||||||
sandbox.stub(sortParamsNS, 'getSortingParams').returns({ b: 'b' });
|
sandbox.stub(sortParamsNS, 'getSortingParams').returns({ b: 'b' });
|
||||||
expect(getSearchDsl({})).toEqual({ a: 'a', b: 'b' });
|
expect(getSearchDsl(null, { type: 'foo' })).toEqual({ a: 'a', b: 'b' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -72,7 +72,7 @@ test(`#find`, async () => {
|
||||||
};
|
};
|
||||||
const client = new SavedObjectsClient(mockRepository);
|
const client = new SavedObjectsClient(mockRepository);
|
||||||
|
|
||||||
const options = {};
|
const options = { type: 'foo' };
|
||||||
const result = await client.find(options);
|
const result = await client.find(options);
|
||||||
|
|
||||||
expect(mockRepository.find).toHaveBeenCalledWith(options);
|
expect(mockRepository.find).toHaveBeenCalledWith(options);
|
||||||
|
|
|
@ -140,6 +140,25 @@ export default function ({ getService }) {
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('missing type', () => {
|
||||||
|
it('should return 400', async () => (
|
||||||
|
await supertest
|
||||||
|
.get('/api/saved_objects/_find')
|
||||||
|
.expect(400)
|
||||||
|
.then(resp => {
|
||||||
|
expect(resp.body).to.eql({
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'child "type" fails because ["type" is required]',
|
||||||
|
statusCode: 400,
|
||||||
|
validation: {
|
||||||
|
keys: ['type'],
|
||||||
|
source: 'query'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
describe('page beyond total', () => {
|
describe('page beyond total', () => {
|
||||||
it('should return 200 with empty response', async () => (
|
it('should return 200 with empty response', async () => (
|
||||||
await supertest
|
await supertest
|
||||||
|
|
|
@ -139,7 +139,6 @@ export const security = (kibana) => new kibana.Plugin({
|
||||||
errors: savedObjects.SavedObjectsClient.errors,
|
errors: savedObjects.SavedObjectsClient.errors,
|
||||||
checkPrivileges,
|
checkPrivileges,
|
||||||
auditLogger,
|
auditLogger,
|
||||||
savedObjectTypes: savedObjects.types,
|
|
||||||
actions: authorization.actions,
|
actions: authorization.actions,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,7 +15,6 @@ export class SecureSavedObjectsClient {
|
||||||
callWithRequestRepository,
|
callWithRequestRepository,
|
||||||
checkPrivileges,
|
checkPrivileges,
|
||||||
auditLogger,
|
auditLogger,
|
||||||
savedObjectTypes,
|
|
||||||
actions,
|
actions,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
@ -24,7 +23,6 @@ export class SecureSavedObjectsClient {
|
||||||
this._callWithRequestRepository = callWithRequestRepository;
|
this._callWithRequestRepository = callWithRequestRepository;
|
||||||
this._checkPrivileges = checkPrivileges;
|
this._checkPrivileges = checkPrivileges;
|
||||||
this._auditLogger = auditLogger;
|
this._auditLogger = auditLogger;
|
||||||
this._savedObjectTypes = savedObjectTypes;
|
|
||||||
this._actions = actions;
|
this._actions = actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,11 +55,12 @@ export class SecureSavedObjectsClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
async find(options = {}) {
|
async find(options = {}) {
|
||||||
if (options.type) {
|
return await this._execute(
|
||||||
return await this._findWithTypes(options);
|
options.type,
|
||||||
}
|
'find',
|
||||||
|
{ options },
|
||||||
return await this._findAcrossAllTypes(options);
|
repository => repository.find(options)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkGet(objects = []) {
|
async bulkGet(objects = []) {
|
||||||
|
@ -95,7 +94,7 @@ export class SecureSavedObjectsClient {
|
||||||
async _checkSavedObjectPrivileges(actions) {
|
async _checkSavedObjectPrivileges(actions) {
|
||||||
try {
|
try {
|
||||||
return await this._checkPrivileges(actions);
|
return await this._checkPrivileges(actions);
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
const { reason } = get(error, 'body.error', {});
|
const { reason } = get(error, 'body.error', {});
|
||||||
throw this.errors.decorateGeneralError(error, reason);
|
throw this.errors.decorateGeneralError(error, reason);
|
||||||
}
|
}
|
||||||
|
@ -120,48 +119,4 @@ export class SecureSavedObjectsClient {
|
||||||
throw new Error('Unexpected result from hasPrivileges');
|
throw new Error('Unexpected result from hasPrivileges');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _findAcrossAllTypes(options) {
|
|
||||||
const action = 'find';
|
|
||||||
|
|
||||||
// we have to filter for only their authorized types
|
|
||||||
const types = this._savedObjectTypes;
|
|
||||||
const typesToPrivilegesMap = new Map(types.map(type => [type, this._actions.getSavedObjectAction(type, action)]));
|
|
||||||
const { result, username, missing } = await this._checkSavedObjectPrivileges(Array.from(typesToPrivilegesMap.values()));
|
|
||||||
|
|
||||||
if (result === CHECK_PRIVILEGES_RESULT.LEGACY) {
|
|
||||||
return await this._callWithRequestRepository.find(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
const authorizedTypes = Array.from(typesToPrivilegesMap.entries())
|
|
||||||
.filter(([ , privilege]) => !missing.includes(privilege))
|
|
||||||
.map(([type]) => type);
|
|
||||||
|
|
||||||
if (authorizedTypes.length === 0) {
|
|
||||||
this._auditLogger.savedObjectsAuthorizationFailure(
|
|
||||||
username,
|
|
||||||
action,
|
|
||||||
types,
|
|
||||||
missing,
|
|
||||||
{ options }
|
|
||||||
);
|
|
||||||
throw this.errors.decorateForbiddenError(new Error(`Not authorized to find saved_object`));
|
|
||||||
}
|
|
||||||
|
|
||||||
this._auditLogger.savedObjectsAuthorizationSuccess(username, action, authorizedTypes, { options });
|
|
||||||
|
|
||||||
return await this._internalRepository.find({
|
|
||||||
...options,
|
|
||||||
type: authorizedTypes
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async _findWithTypes(options) {
|
|
||||||
return await this._execute(
|
|
||||||
options.type,
|
|
||||||
'find',
|
|
||||||
{ options },
|
|
||||||
repository => repository.find(options)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -516,7 +516,7 @@ describe('#find', () => {
|
||||||
auditLogger: mockAuditLogger,
|
auditLogger: mockAuditLogger,
|
||||||
actions: mockActions,
|
actions: mockActions,
|
||||||
});
|
});
|
||||||
const options = { type: [ type1, type2 ] };
|
const options = { type: [type1, type2] };
|
||||||
|
|
||||||
await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError);
|
await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError);
|
||||||
|
|
||||||
|
@ -598,9 +598,7 @@ describe('#find', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('no type', () => {
|
describe('no type', () => {
|
||||||
test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => {
|
test(`throws error`, async () => {
|
||||||
const type1 = 'foo';
|
|
||||||
const type2 = 'bar';
|
|
||||||
const mockRepository = {};
|
const mockRepository = {};
|
||||||
const mockErrors = createMockErrors();
|
const mockErrors = createMockErrors();
|
||||||
const mockCheckPrivileges = jest.fn().mockImplementation(async () => {
|
const mockCheckPrivileges = jest.fn().mockImplementation(async () => {
|
||||||
|
@ -613,161 +611,18 @@ describe('#find', () => {
|
||||||
repository: mockRepository,
|
repository: mockRepository,
|
||||||
checkPrivileges: mockCheckPrivileges,
|
checkPrivileges: mockCheckPrivileges,
|
||||||
auditLogger: mockAuditLogger,
|
auditLogger: mockAuditLogger,
|
||||||
savedObjectTypes: [type1, type2],
|
|
||||||
actions: mockActions,
|
actions: mockActions,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(client.find()).rejects.toThrowError(mockErrors.generalError);
|
await expect(client.find()).rejects.toThrowError(mockErrors.generalError);
|
||||||
|
|
||||||
expect(mockCheckPrivileges).toHaveBeenCalledWith([
|
expect(mockCheckPrivileges).toHaveBeenCalledWith([
|
||||||
mockActions.getSavedObjectAction(type1, 'find'),
|
mockActions.getSavedObjectAction(undefined, 'find'),
|
||||||
mockActions.getSavedObjectAction(type2, 'find'),
|
|
||||||
]);
|
]);
|
||||||
expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1);
|
expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1);
|
||||||
expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
|
expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
|
||||||
expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
|
expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`throws decorated ForbiddenError when unauthorized`, async () => {
|
|
||||||
const type = 'foo';
|
|
||||||
const username = Symbol();
|
|
||||||
const mockRepository = {};
|
|
||||||
const mockErrors = createMockErrors();
|
|
||||||
const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({
|
|
||||||
result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED,
|
|
||||||
username,
|
|
||||||
missing: [
|
|
||||||
privileges[0]
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
const mockAuditLogger = createMockAuditLogger();
|
|
||||||
const mockActions = createMockActions();
|
|
||||||
const client = new SecureSavedObjectsClient({
|
|
||||||
errors: mockErrors,
|
|
||||||
repository: mockRepository,
|
|
||||||
checkPrivileges: mockCheckPrivileges,
|
|
||||||
auditLogger: mockAuditLogger,
|
|
||||||
savedObjectTypes: [type],
|
|
||||||
actions: mockActions,
|
|
||||||
});
|
|
||||||
const options = Symbol();
|
|
||||||
|
|
||||||
await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError);
|
|
||||||
|
|
||||||
expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'find')]);
|
|
||||||
expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
|
|
||||||
username,
|
|
||||||
'find',
|
|
||||||
[type],
|
|
||||||
[mockActions.getSavedObjectAction(type, 'find')],
|
|
||||||
{
|
|
||||||
options
|
|
||||||
}
|
|
||||||
);
|
|
||||||
expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test(`returns result of callWithRequestRepository.find when legacy`, async () => {
|
|
||||||
const type = 'foo';
|
|
||||||
const username = Symbol();
|
|
||||||
const returnValue = Symbol();
|
|
||||||
const mockRepository = {
|
|
||||||
find: jest.fn().mockReturnValue(returnValue)
|
|
||||||
};
|
|
||||||
const mockErrors = createMockErrors();
|
|
||||||
const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({
|
|
||||||
result: CHECK_PRIVILEGES_RESULT.LEGACY,
|
|
||||||
username,
|
|
||||||
missing: privileges,
|
|
||||||
}));
|
|
||||||
const mockAuditLogger = createMockAuditLogger();
|
|
||||||
const mockActions = createMockActions();
|
|
||||||
const client = new SecureSavedObjectsClient({
|
|
||||||
errors: mockErrors,
|
|
||||||
callWithRequestRepository: mockRepository,
|
|
||||||
checkPrivileges: mockCheckPrivileges,
|
|
||||||
auditLogger: mockAuditLogger,
|
|
||||||
savedObjectTypes: [type],
|
|
||||||
actions: mockActions,
|
|
||||||
});
|
|
||||||
const options = Symbol();
|
|
||||||
|
|
||||||
const result = await client.find(options);
|
|
||||||
|
|
||||||
expect(result).toBe(returnValue);
|
|
||||||
expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'find')]);
|
|
||||||
expect(mockRepository.find).toHaveBeenCalledWith(options);
|
|
||||||
expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
|
|
||||||
expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test(`specifies authorized types when calling repository.find()`, async () => {
|
|
||||||
const type1 = 'foo';
|
|
||||||
const type2 = 'bar';
|
|
||||||
const mockRepository = {
|
|
||||||
find: jest.fn(),
|
|
||||||
};
|
|
||||||
const mockErrors = createMockErrors();
|
|
||||||
const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({
|
|
||||||
result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED,
|
|
||||||
missing: [
|
|
||||||
privileges[0]
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
const mockAuditLogger = createMockAuditLogger();
|
|
||||||
const mockActions = createMockActions();
|
|
||||||
const client = new SecureSavedObjectsClient({
|
|
||||||
errors: mockErrors,
|
|
||||||
internalRepository: mockRepository,
|
|
||||||
checkPrivileges: mockCheckPrivileges,
|
|
||||||
auditLogger: mockAuditLogger,
|
|
||||||
savedObjectTypes: [type1, type2],
|
|
||||||
actions: mockActions,
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.find();
|
|
||||||
|
|
||||||
expect(mockCheckPrivileges).toHaveBeenCalledWith([
|
|
||||||
mockActions.getSavedObjectAction(type1, 'find'),
|
|
||||||
mockActions.getSavedObjectAction(type2, 'find'),
|
|
||||||
]);
|
|
||||||
expect(mockRepository.find).toHaveBeenCalledWith(expect.objectContaining({
|
|
||||||
type: [type2]
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
test(`returns result of repository.find`, async () => {
|
|
||||||
const type = 'foo';
|
|
||||||
const username = Symbol();
|
|
||||||
const returnValue = Symbol();
|
|
||||||
const mockRepository = {
|
|
||||||
find: jest.fn().mockReturnValue(returnValue)
|
|
||||||
};
|
|
||||||
const mockCheckPrivileges = jest.fn().mockImplementation(async () => ({
|
|
||||||
result: CHECK_PRIVILEGES_RESULT.AUTHORIZED,
|
|
||||||
username,
|
|
||||||
missing: [],
|
|
||||||
}));
|
|
||||||
const mockAuditLogger = createMockAuditLogger();
|
|
||||||
const client = new SecureSavedObjectsClient({
|
|
||||||
internalRepository: mockRepository,
|
|
||||||
checkPrivileges: mockCheckPrivileges,
|
|
||||||
auditLogger: mockAuditLogger,
|
|
||||||
savedObjectTypes: [type],
|
|
||||||
actions: createMockActions(),
|
|
||||||
});
|
|
||||||
const options = Symbol();
|
|
||||||
|
|
||||||
const result = await client.find(options);
|
|
||||||
|
|
||||||
expect(result).toBe(returnValue);
|
|
||||||
expect(mockRepository.find).toHaveBeenCalledWith({ type: [type] });
|
|
||||||
expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
|
|
||||||
expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'find', [type], {
|
|
||||||
options,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -31,85 +31,15 @@ export default function ({ getService }) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const expectResultsWithValidTypes = (resp) => {
|
const expectBadRequest = (resp) => {
|
||||||
expect(resp.body).to.eql({
|
expect(resp.body).to.eql({
|
||||||
page: 1,
|
error: 'Bad Request',
|
||||||
per_page: 20,
|
message: 'child "type" fails because ["type" is required]',
|
||||||
total: 4,
|
statusCode: 400,
|
||||||
saved_objects: [
|
validation: {
|
||||||
{
|
keys: ['type'],
|
||||||
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
source: 'query'
|
||||||
type: 'index-pattern',
|
}
|
||||||
updated_at: '2017-09-21T18:49:16.270Z',
|
|
||||||
version: 1,
|
|
||||||
attributes: resp.body.saved_objects[0].attributes
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '7.0.0-alpha1',
|
|
||||||
type: 'config',
|
|
||||||
updated_at: '2017-09-21T18:49:16.302Z',
|
|
||||||
version: 1,
|
|
||||||
attributes: resp.body.saved_objects[1].attributes
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
|
||||||
type: 'visualization',
|
|
||||||
updated_at: '2017-09-21T18:51:23.794Z',
|
|
||||||
version: 1,
|
|
||||||
attributes: resp.body.saved_objects[2].attributes
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
|
||||||
type: 'dashboard',
|
|
||||||
updated_at: '2017-09-21T18:57:40.826Z',
|
|
||||||
version: 1,
|
|
||||||
attributes: resp.body.saved_objects[3].attributes
|
|
||||||
},
|
|
||||||
]
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const expectAllResultsIncludingInvalidTypes = (resp) => {
|
|
||||||
expect(resp.body).to.eql({
|
|
||||||
page: 1,
|
|
||||||
per_page: 20,
|
|
||||||
total: 5,
|
|
||||||
saved_objects: [
|
|
||||||
{
|
|
||||||
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
|
||||||
type: 'index-pattern',
|
|
||||||
updated_at: '2017-09-21T18:49:16.270Z',
|
|
||||||
version: 1,
|
|
||||||
attributes: resp.body.saved_objects[0].attributes
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '7.0.0-alpha1',
|
|
||||||
type: 'config',
|
|
||||||
updated_at: '2017-09-21T18:49:16.302Z',
|
|
||||||
version: 1,
|
|
||||||
attributes: resp.body.saved_objects[1].attributes
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
|
||||||
type: 'visualization',
|
|
||||||
updated_at: '2017-09-21T18:51:23.794Z',
|
|
||||||
version: 1,
|
|
||||||
attributes: resp.body.saved_objects[2].attributes
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
|
||||||
type: 'dashboard',
|
|
||||||
updated_at: '2017-09-21T18:57:40.826Z',
|
|
||||||
version: 1,
|
|
||||||
attributes: resp.body.saved_objects[3].attributes
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'visualization:dd7caf20-9efd-11e7-acb3-3dab96693faa',
|
|
||||||
type: 'not-a-visualization',
|
|
||||||
updated_at: '2017-09-21T18:51:23.794Z',
|
|
||||||
version: 1
|
|
||||||
},
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -130,14 +60,6 @@ export default function ({ getService }) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const expectForbiddenCantFindAnyTypes = resp => {
|
|
||||||
expect(resp.body).to.eql({
|
|
||||||
statusCode: 403,
|
|
||||||
error: 'Forbidden',
|
|
||||||
message: `Not authorized to find saved_object`
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const findTest = (description, { auth, tests }) => {
|
const findTest = (description, { auth, tests }) => {
|
||||||
describe(description, () => {
|
describe(description, () => {
|
||||||
before(() => esArchiver.load('saved_objects/basic'));
|
before(() => esArchiver.load('saved_objects/basic'));
|
||||||
|
@ -221,8 +143,8 @@ export default function ({ getService }) {
|
||||||
},
|
},
|
||||||
noType: {
|
noType: {
|
||||||
description: `forbidded can't find any types`,
|
description: `forbidded can't find any types`,
|
||||||
statusCode: 403,
|
statusCode: 400,
|
||||||
response: expectForbiddenCantFindAnyTypes,
|
response: expectBadRequest,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -255,8 +177,8 @@ export default function ({ getService }) {
|
||||||
},
|
},
|
||||||
noType: {
|
noType: {
|
||||||
description: 'all objects',
|
description: 'all objects',
|
||||||
statusCode: 200,
|
statusCode: 400,
|
||||||
response: expectResultsWithValidTypes,
|
response: expectBadRequest,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -289,8 +211,8 @@ export default function ({ getService }) {
|
||||||
},
|
},
|
||||||
noType: {
|
noType: {
|
||||||
description: 'all objects',
|
description: 'all objects',
|
||||||
statusCode: 200,
|
statusCode: 400,
|
||||||
response: expectAllResultsIncludingInvalidTypes,
|
response: expectBadRequest,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -323,8 +245,8 @@ export default function ({ getService }) {
|
||||||
},
|
},
|
||||||
noType: {
|
noType: {
|
||||||
description: 'all objects',
|
description: 'all objects',
|
||||||
statusCode: 200,
|
statusCode: 400,
|
||||||
response: expectAllResultsIncludingInvalidTypes,
|
response: expectBadRequest,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -357,8 +279,8 @@ export default function ({ getService }) {
|
||||||
},
|
},
|
||||||
noType: {
|
noType: {
|
||||||
description: 'all objects',
|
description: 'all objects',
|
||||||
statusCode: 200,
|
statusCode: 400,
|
||||||
response: expectResultsWithValidTypes,
|
response: expectBadRequest,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -391,8 +313,8 @@ export default function ({ getService }) {
|
||||||
},
|
},
|
||||||
noType: {
|
noType: {
|
||||||
description: 'all objects',
|
description: 'all objects',
|
||||||
statusCode: 200,
|
statusCode: 400,
|
||||||
response: expectResultsWithValidTypes,
|
response: expectBadRequest,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -425,8 +347,8 @@ export default function ({ getService }) {
|
||||||
},
|
},
|
||||||
noType: {
|
noType: {
|
||||||
description: 'all objects',
|
description: 'all objects',
|
||||||
statusCode: 200,
|
statusCode: 400,
|
||||||
response: expectResultsWithValidTypes,
|
response: expectBadRequest,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -459,8 +381,8 @@ export default function ({ getService }) {
|
||||||
},
|
},
|
||||||
noType: {
|
noType: {
|
||||||
description: 'all objects',
|
description: 'all objects',
|
||||||
statusCode: 200,
|
statusCode: 400,
|
||||||
response: expectResultsWithValidTypes,
|
response: expectBadRequest,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue