Skip to content

Commit

Permalink
SavedObjectClient.find filtering (#19708)
Browse files Browse the repository at this point in the history
* Adding ability to specify filters when calling the repository

* Implementing find filtering

* Revert "Adding ability to specify filters when calling the repository"

This reverts commit 9da30a1.

* Adding integration tests for find filtering

* Adding forbidden auth logging

* Adding asserts to make sure some audit log isn't used

* Adding more audit log specific tests

* Necessarly is not a work, unfortunately

* Fixing test

* More descriptive name than "result"

* Better unauthorized find message?

* Adding getTypes tests
  • Loading branch information
kobelb committed Jun 7, 2018
1 parent d793056 commit 7f2c9b0
Show file tree
Hide file tree
Showing 8 changed files with 1,405 additions and 29 deletions.
6 changes: 5 additions & 1 deletion src/server/saved_objects/service/lib/repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import uuid from 'uuid';

import { getRootType } from '../../../mappings';
import { getRootType, getRootPropertiesObjects } from '../../../mappings';
import { getSearchDsl } from './search_dsl';
import { trimIdPrefix } from './trim_id_prefix';
import { includedFields } from './included_fields';
Expand Down Expand Up @@ -406,6 +406,10 @@ export class SavedObjectsRepository {
};
}

getTypes() {
return Object.keys(getRootPropertiesObjects(this._mappings));
}

async _writeToCluster(method, params) {
try {
await this._onBeforeWrite();
Expand Down
77 changes: 77 additions & 0 deletions src/server/saved_objects/service/lib/repository.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,83 @@ describe('SavedObjectsRepository', () => {
});
});

describe('#getTypes', () => {
it(`returns no types if mappings have no types`, () => {
const mappings = {
doc: {
properties: {
'updated_at': {
type: 'date'
},
}
}
};

savedObjectsRepository = new SavedObjectsRepository({
index: '.kibana-test',
mappings,
callCluster: callAdminCluster,
onBeforeWrite
});

const types = savedObjectsRepository.getTypes();
expect(types).toEqual([]);
});

it(`returns single type defined in mappings`, () => {
const mappings = {
doc: {
properties: {
'updated_at': {
type: 'date'
},
'index-pattern': {
properties: {}
}
}
}
};

savedObjectsRepository = new SavedObjectsRepository({
index: '.kibana-test',
mappings,
callCluster: callAdminCluster,
onBeforeWrite
});

const types = savedObjectsRepository.getTypes();
expect(types).toEqual(['index-pattern']);
});

it(`returns multiple types defined in mappings`, () => {
const mappings = {
doc: {
properties: {
'updated_at': {
type: 'date'
},
'index-pattern': {
properties: {}
},
'visualization': {
properties: {}
},
}
}
};

savedObjectsRepository = new SavedObjectsRepository({
index: '.kibana-test',
mappings,
callCluster: callAdminCluster,
onBeforeWrite
});

const types = savedObjectsRepository.getTypes();
expect(types).toEqual(['index-pattern', 'visualization']);
});
});

describe('onBeforeWrite', () => {
it('blocks calls to callCluster of requests', async () => {
onBeforeWrite.returns(delay(500));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

import { get, uniq } from 'lodash';

const getPrivilege = (type, action) => {
return `action:saved_objects/${type}/${action}`;
};

export class SecureSavedObjectsClient {
constructor(options) {
const {
Expand Down Expand Up @@ -51,11 +55,38 @@ export class SecureSavedObjectsClient {
}

async find(options = {}) {
await this._performAuthorizationCheck(options.type, 'find', {
options,
});
const action = 'find';

return await this._repository.find(options);
// when we have the type or types, it makes our life easy
if (options.type) {
await this._performAuthorizationCheck(options.type, action, { options });
return await this._repository.find(options);
}

// otherwise, we have to filter for only their authorized types
const types = this._repository.getTypes();
const typesToPrivilegesMap = new Map(types.map(type => [type, getPrivilege(type, action)]));
const hasPrivilegesResult = await this._hasSavedObjectPrivileges(Array.from(typesToPrivilegesMap.values()));
const authorizedTypes = Array.from(typesToPrivilegesMap.entries())
.filter(([ , privilege]) => !hasPrivilegesResult.missing.includes(privilege))
.map(([type]) => type);

if (authorizedTypes.length === 0) {
this._auditLogger.savedObjectsAuthorizationFailure(
hasPrivilegesResult.username,
action,
types,
hasPrivilegesResult.missing,
{ options }
);
throw this.errors.decorateForbiddenError(new Error(`Not authorized to find saved_object`));
}
this._auditLogger.savedObjectsAuthorizationSuccess(hasPrivilegesResult.username, action, authorizedTypes, { options });

return await this._repository.find({
...options,
type: authorizedTypes
});
}

async bulkGet(objects = []) {
Expand Down Expand Up @@ -89,15 +120,8 @@ export class SecureSavedObjectsClient {

async _performAuthorizationCheck(typeOrTypes, action, args) {
const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes];
const actions = types.map(type => `action:saved_objects/${type}/${action}`);

let result;
try {
result = await this._hasPrivileges(actions);
} catch(error) {
const { reason } = get(error, 'body.error', {});
throw this.errors.decorateGeneralError(error, reason);
}
const privileges = types.map(type => getPrivilege(type, action));
const result = await this._hasSavedObjectPrivileges(privileges);

if (result.success) {
this._auditLogger.savedObjectsAuthorizationSuccess(result.username, action, types, args);
Expand All @@ -107,4 +131,13 @@ export class SecureSavedObjectsClient {
throw this.errors.decorateForbiddenError(new Error(msg));
}
}

async _hasSavedObjectPrivileges(privileges) {
try {
return await this._hasPrivileges(privileges);
} catch(error) {
const { reason } = get(error, 'body.error', {});
throw this.errors.decorateGeneralError(error, reason);
}
}
}
Loading

0 comments on commit 7f2c9b0

Please sign in to comment.