Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SavedObjectClient.find filtering #19708

Merged
merged 12 commits into from
Jun 7, 2018
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() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is a pretty trivial wrapper around a well-tested function, but what do you think about adding a test or two for this function, since it is part of the Repository's public interface?

Copy link
Contributor Author

@kobelb kobelb Jun 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some basic tests here, I've been going back and forth where this should "live" as I also want to use it https:/elastic/kibana/blob/rbac-phase-1/x-pack/plugins/security/server/lib/privileges/privileges.js#L48 but we don't have access to the Repository there, nor do we need to access the actual saved objects themselves.

I've been pondering whether we should createa "kibana mappings service" that uses the mappings defined in src/server/mappings applied against the mappings that we've discovered via the various plugins, but I've been postponing doing so...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the tests! I think a mappings service would absolutely be worthwhile, but I don't know that we need to work on that as part of RBAC Phase 1

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