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`
|
||||
|
||||
==== Query Parameters
|
||||
|
||||
`type` (required)::
|
||||
(array|string) The saved object type(s) that the response should be limited to
|
||||
`per_page` (optional)::
|
||||
(number) The number of objects to return per page
|
||||
`page` (optional)::
|
||||
(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)::
|
||||
(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)::
|
||||
|
|
|
@ -29,7 +29,7 @@ export const createFindRoute = (prereqs) => ({
|
|||
query: Joi.object().keys({
|
||||
per_page: Joi.number().min(0).default(20),
|
||||
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_fields: 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();
|
||||
});
|
||||
|
||||
it('formats successful response', async () => {
|
||||
it('returns with status 400 when type is missing', async () => {
|
||||
const request = {
|
||||
method: 'GET',
|
||||
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 = {
|
||||
total: 2,
|
||||
data: [
|
||||
|
@ -81,7 +97,7 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
it('calls upon savedObjectClient.find with defaults', async () => {
|
||||
const request = {
|
||||
method: 'GET',
|
||||
url: '/api/saved_objects/_find'
|
||||
url: '/api/saved_objects/_find?type=foo&type=bar'
|
||||
};
|
||||
|
||||
await server.inject(request);
|
||||
|
@ -89,13 +105,13 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
expect(savedObjectsClient.find.calledOnce).toBe(true);
|
||||
|
||||
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 () => {
|
||||
const request = {
|
||||
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);
|
||||
|
@ -103,13 +119,13 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
expect(savedObjectsClient.find.calledOnce).toBe(true);
|
||||
|
||||
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 () => {
|
||||
const request = {
|
||||
method: 'GET',
|
||||
url: '/api/saved_objects/_find?search_fields=title'
|
||||
url: '/api/saved_objects/_find?type=foo&search_fields=title'
|
||||
};
|
||||
|
||||
await server.inject(request);
|
||||
|
@ -117,13 +133,13 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
expect(savedObjectsClient.find.calledOnce).toBe(true);
|
||||
|
||||
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 () => {
|
||||
const request = {
|
||||
method: 'GET',
|
||||
url: '/api/saved_objects/_find?fields=title'
|
||||
url: '/api/saved_objects/_find?type=foo&fields=title'
|
||||
};
|
||||
|
||||
await server.inject(request);
|
||||
|
@ -131,13 +147,13 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
expect(savedObjectsClient.find.calledOnce).toBe(true);
|
||||
|
||||
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 () => {
|
||||
const request = {
|
||||
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);
|
||||
|
@ -146,7 +162,7 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
|
||||
const options = savedObjectsClient.find.getCall(0).args[0];
|
||||
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);
|
||||
|
||||
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 () => {
|
||||
|
|
|
@ -251,6 +251,10 @@ export class SavedObjectsRepository {
|
|||
fields,
|
||||
} = options;
|
||||
|
||||
if (!type) {
|
||||
throw new TypeError(`options.type must be a string or an array of strings`);
|
||||
}
|
||||
|
||||
if (searchFields && !Array.isArray(searchFields)) {
|
||||
throw new TypeError('options.searchFields must be an array');
|
||||
}
|
||||
|
|
|
@ -394,7 +394,7 @@ describe('SavedObjectsRepository', () => {
|
|||
|
||||
try {
|
||||
await savedObjectsRepository.delete('index-pattern', 'logstash-*');
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
expect(e.output.statusCode).toEqual(404);
|
||||
}
|
||||
});
|
||||
|
@ -423,9 +423,15 @@ describe('SavedObjectsRepository', () => {
|
|||
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 () => {
|
||||
try {
|
||||
await savedObjectsRepository.find({ searchFields: 'string' });
|
||||
await savedObjectsRepository.find({ type: 'foo', searchFields: 'string' });
|
||||
throw new Error('expected find() to reject');
|
||||
} catch (error) {
|
||||
sinon.assert.notCalled(callAdminCluster);
|
||||
|
@ -436,7 +442,7 @@ describe('SavedObjectsRepository', () => {
|
|||
|
||||
it('requires fields be an array if defined', async () => {
|
||||
try {
|
||||
await savedObjectsRepository.find({ fields: 'string' });
|
||||
await savedObjectsRepository.find({ type: 'foo', fields: 'string' });
|
||||
throw new Error('expected find() to reject');
|
||||
} catch (error) {
|
||||
sinon.assert.notCalled(callAdminCluster);
|
||||
|
@ -461,7 +467,7 @@ describe('SavedObjectsRepository', () => {
|
|||
|
||||
it('merges output of getSearchDsl into es request body', async () => {
|
||||
getSearchDsl.returns({ query: 1, aggregations: 2 });
|
||||
await savedObjectsRepository.find();
|
||||
await savedObjectsRepository.find({ type: 'foo' });
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, 'search', sinon.match({
|
||||
|
@ -475,7 +481,7 @@ describe('SavedObjectsRepository', () => {
|
|||
it('formats Elasticsearch response', async () => {
|
||||
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.saved_objects).toHaveLength(count);
|
||||
|
@ -492,7 +498,7 @@ describe('SavedObjectsRepository', () => {
|
|||
});
|
||||
|
||||
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.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
|
@ -504,12 +510,12 @@ describe('SavedObjectsRepository', () => {
|
|||
});
|
||||
|
||||
it('can filter by fields', async () => {
|
||||
await savedObjectsRepository.find({ fields: ['title'] });
|
||||
await savedObjectsRepository.find({ type: 'foo', fields: ['title'] });
|
||||
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
_source: [
|
||||
'*.title', 'type', 'title'
|
||||
'foo.title', 'type', 'title'
|
||||
]
|
||||
}));
|
||||
|
||||
|
|
|
@ -31,8 +31,8 @@ export function getSearchDsl(mappings, options = {}) {
|
|||
sortOrder
|
||||
} = options;
|
||||
|
||||
if (!type && sortField) {
|
||||
throw Boom.notAcceptable('Cannot sort without filtering by type');
|
||||
if (!type) {
|
||||
throw Boom.notAcceptable('type must be specified');
|
||||
}
|
||||
|
||||
if (sortOrder && !sortField) {
|
||||
|
|
|
@ -27,13 +27,13 @@ describe('getSearchDsl', () => {
|
|||
afterEach(() => sandbox.restore());
|
||||
|
||||
describe('validation', () => {
|
||||
it('throws when sortField is passed without type', () => {
|
||||
it('throws when type is not specified', () => {
|
||||
expect(() => {
|
||||
getSearchDsl({}, {
|
||||
type: undefined,
|
||||
sortField: 'title'
|
||||
});
|
||||
}).toThrowError(/sort without .+ type/);
|
||||
}).toThrowError(/type must be specified/);
|
||||
});
|
||||
it('throws when sortOrder without sortField', () => {
|
||||
expect(() => {
|
||||
|
@ -89,7 +89,7 @@ describe('getSearchDsl', () => {
|
|||
it('returns combination of getQueryParams and getSortingParams', () => {
|
||||
sandbox.stub(queryParamsNS, 'getQueryParams').returns({ a: 'a' });
|
||||
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 options = {};
|
||||
const options = { type: 'foo' };
|
||||
const result = await client.find(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', () => {
|
||||
it('should return 200 with empty response', async () => (
|
||||
await supertest
|
||||
|
|
|
@ -139,7 +139,6 @@ export const security = (kibana) => new kibana.Plugin({
|
|||
errors: savedObjects.SavedObjectsClient.errors,
|
||||
checkPrivileges,
|
||||
auditLogger,
|
||||
savedObjectTypes: savedObjects.types,
|
||||
actions: authorization.actions,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,7 +15,6 @@ export class SecureSavedObjectsClient {
|
|||
callWithRequestRepository,
|
||||
checkPrivileges,
|
||||
auditLogger,
|
||||
savedObjectTypes,
|
||||
actions,
|
||||
} = options;
|
||||
|
||||
|
@ -24,7 +23,6 @@ export class SecureSavedObjectsClient {
|
|||
this._callWithRequestRepository = callWithRequestRepository;
|
||||
this._checkPrivileges = checkPrivileges;
|
||||
this._auditLogger = auditLogger;
|
||||
this._savedObjectTypes = savedObjectTypes;
|
||||
this._actions = actions;
|
||||
}
|
||||
|
||||
|
@ -57,11 +55,12 @@ export class SecureSavedObjectsClient {
|
|||
}
|
||||
|
||||
async find(options = {}) {
|
||||
if (options.type) {
|
||||
return await this._findWithTypes(options);
|
||||
}
|
||||
|
||||
return await this._findAcrossAllTypes(options);
|
||||
return await this._execute(
|
||||
options.type,
|
||||
'find',
|
||||
{ options },
|
||||
repository => repository.find(options)
|
||||
);
|
||||
}
|
||||
|
||||
async bulkGet(objects = []) {
|
||||
|
@ -95,7 +94,7 @@ export class SecureSavedObjectsClient {
|
|||
async _checkSavedObjectPrivileges(actions) {
|
||||
try {
|
||||
return await this._checkPrivileges(actions);
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
const { reason } = get(error, 'body.error', {});
|
||||
throw this.errors.decorateGeneralError(error, reason);
|
||||
}
|
||||
|
@ -120,48 +119,4 @@ export class SecureSavedObjectsClient {
|
|||
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,
|
||||
actions: mockActions,
|
||||
});
|
||||
const options = { type: [ type1, type2 ] };
|
||||
const options = { type: [type1, type2] };
|
||||
|
||||
await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError);
|
||||
|
||||
|
@ -598,9 +598,7 @@ describe('#find', () => {
|
|||
});
|
||||
|
||||
describe('no type', () => {
|
||||
test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => {
|
||||
const type1 = 'foo';
|
||||
const type2 = 'bar';
|
||||
test(`throws error`, async () => {
|
||||
const mockRepository = {};
|
||||
const mockErrors = createMockErrors();
|
||||
const mockCheckPrivileges = jest.fn().mockImplementation(async () => {
|
||||
|
@ -613,161 +611,18 @@ describe('#find', () => {
|
|||
repository: mockRepository,
|
||||
checkPrivileges: mockCheckPrivileges,
|
||||
auditLogger: mockAuditLogger,
|
||||
savedObjectTypes: [type1, type2],
|
||||
actions: mockActions,
|
||||
});
|
||||
|
||||
await expect(client.find()).rejects.toThrowError(mockErrors.generalError);
|
||||
|
||||
expect(mockCheckPrivileges).toHaveBeenCalledWith([
|
||||
mockActions.getSavedObjectAction(type1, 'find'),
|
||||
mockActions.getSavedObjectAction(type2, 'find'),
|
||||
mockActions.getSavedObjectAction(undefined, 'find'),
|
||||
]);
|
||||
expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1);
|
||||
expect(mockAuditLogger.savedObjectsAuthorizationFailure).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({
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 4,
|
||||
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
|
||||
},
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
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
|
||||
},
|
||||
]
|
||||
error: 'Bad Request',
|
||||
message: 'child "type" fails because ["type" is required]',
|
||||
statusCode: 400,
|
||||
validation: {
|
||||
keys: ['type'],
|
||||
source: 'query'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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 }) => {
|
||||
describe(description, () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
|
@ -221,8 +143,8 @@ export default function ({ getService }) {
|
|||
},
|
||||
noType: {
|
||||
description: `forbidded can't find any types`,
|
||||
statusCode: 403,
|
||||
response: expectForbiddenCantFindAnyTypes,
|
||||
statusCode: 400,
|
||||
response: expectBadRequest,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -255,8 +177,8 @@ export default function ({ getService }) {
|
|||
},
|
||||
noType: {
|
||||
description: 'all objects',
|
||||
statusCode: 200,
|
||||
response: expectResultsWithValidTypes,
|
||||
statusCode: 400,
|
||||
response: expectBadRequest,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -289,8 +211,8 @@ export default function ({ getService }) {
|
|||
},
|
||||
noType: {
|
||||
description: 'all objects',
|
||||
statusCode: 200,
|
||||
response: expectAllResultsIncludingInvalidTypes,
|
||||
statusCode: 400,
|
||||
response: expectBadRequest,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -323,8 +245,8 @@ export default function ({ getService }) {
|
|||
},
|
||||
noType: {
|
||||
description: 'all objects',
|
||||
statusCode: 200,
|
||||
response: expectAllResultsIncludingInvalidTypes,
|
||||
statusCode: 400,
|
||||
response: expectBadRequest,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
@ -357,8 +279,8 @@ export default function ({ getService }) {
|
|||
},
|
||||
noType: {
|
||||
description: 'all objects',
|
||||
statusCode: 200,
|
||||
response: expectResultsWithValidTypes,
|
||||
statusCode: 400,
|
||||
response: expectBadRequest,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -391,8 +313,8 @@ export default function ({ getService }) {
|
|||
},
|
||||
noType: {
|
||||
description: 'all objects',
|
||||
statusCode: 200,
|
||||
response: expectResultsWithValidTypes,
|
||||
statusCode: 400,
|
||||
response: expectBadRequest,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
@ -425,8 +347,8 @@ export default function ({ getService }) {
|
|||
},
|
||||
noType: {
|
||||
description: 'all objects',
|
||||
statusCode: 200,
|
||||
response: expectResultsWithValidTypes,
|
||||
statusCode: 400,
|
||||
response: expectBadRequest,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -459,8 +381,8 @@ export default function ({ getService }) {
|
|||
},
|
||||
noType: {
|
||||
description: 'all objects',
|
||||
statusCode: 200,
|
||||
response: expectResultsWithValidTypes,
|
||||
statusCode: 400,
|
||||
response: expectBadRequest,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue