Skip to content

Commit

Permalink
RBAC Integration Tests (#19647)
Browse files Browse the repository at this point in the history
* Porting over the saved objects tests, a bunch are failing, I believe
because security is preventing the requests

* Running saved objects tests with rbac and xsrf disabled

* Adding users

* BulkGet now tests under 3 users

* Adding create tests

* Adding delete tests

* Adding find tests

* Adding get tests

* Adding bulkGet forbidden tests

* Adding not a kibana user tests

* Update tests

* Renaming the actions/privileges to be closer to the functions on the
saved object client itself

* Cleaning up tests and removing without index tests

I'm considering the without index tests to be out of scope for the RBAC
API testing, and we already have unit coverage for these and integration
coverage via the OSS Saved Objects API tests.

* Fixing misspelling
  • Loading branch information
kobelb committed Jun 4, 2018
1 parent d8d9810 commit 3e8e694
Show file tree
Hide file tree
Showing 16 changed files with 1,083 additions and 11 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/security/server/lib/audit_logger.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe(`#savedObjectsAuthorizationFailure`, () => {
const username = 'foo-user';
const action = 'foo-action';
const types = [ 'foo-type-1', 'foo-type-2' ];
const missing = [`action:saved-objects/${types[0]}/foo-action`, `action:saved-objects/${types[1]}/foo-action`];
const missing = [`action:saved_objects/${types[0]}/foo-action`, `action:saved_objects/${types[1]}/foo-action`];
const args = {
'foo': 'bar',
'baz': 'quz',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export function hasPrivilegesWithServer(server) {

return {
success,
missing: missingPrivileges,
// We don't want to expose the version privilege to consumers, as it's an implementation detail only to detect version mismatch
missing: missingPrivileges.filter(p => p !== versionPrivilege),
username: privilegeCheck.username,
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,12 +241,26 @@ test(`throws error if missing version privilege and has login privilege`, async
test(`doesn't throw error if missing version privilege and missing login privilege`, async () => {
const mockServer = createMockServer();
mockResponse(false, {
[getVersionPrivilege(defaultVersion)]: true,
[getLoginPrivilege()]: true,
[getVersionPrivilege(defaultVersion)]: false,
[getLoginPrivilege()]: false,
foo: true,
});

const hasPrivilegesWithRequest = hasPrivilegesWithServer(mockServer);
const hasPrivileges = hasPrivilegesWithRequest({});
await hasPrivileges(['foo']);
});

test(`excludes version privilege when missing version privilege and missing login privilege`, async () => {
const mockServer = createMockServer();
mockResponse(false, {
[getVersionPrivilege(defaultVersion)]: false,
[getLoginPrivilege()]: false,
foo: true,
});

const hasPrivilegesWithRequest = hasPrivilegesWithServer(mockServer);
const hasPrivileges = hasPrivilegesWithRequest({});
const result = await hasPrivileges(['foo']);
expect(result.missing).toEqual([getLoginPrivilege()]);
});
4 changes: 2 additions & 2 deletions x-pack/plugins/security/server/lib/privileges/privileges.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ export function buildPrivilegeMap(application, kibanaVersion) {
}

function buildSavedObjectsReadPrivileges() {
const readActions = ['get', 'mget', 'search'];
const readActions = ['get', 'bulk_get', 'find'];
return buildSavedObjectsPrivileges(readActions);
}

function buildSavedObjectsPrivileges(actions) {
const objectTypes = ['config', 'dashboard', 'graph-workspace', 'index-pattern', 'search', 'timelion-sheet', 'url', 'visualization'];
return objectTypes
.map(type => actions.map(action => `action:saved-objects/${type}/${action}`))
.map(type => actions.map(action => `action:saved_objects/${type}/${action}`))
.reduce((acc, types) => [...acc, ...types], []);
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class SecureSavedObjectsClient {

async bulkCreate(objects, options = {}) {
const types = uniq(objects.map(o => o.type));
await this._performAuthorizationCheck(types, 'create', {
await this._performAuthorizationCheck(types, 'bulk_create', {
objects,
options,
});
Expand All @@ -52,7 +52,7 @@ export class SecureSavedObjectsClient {
}

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

Expand All @@ -61,7 +61,7 @@ export class SecureSavedObjectsClient {

async bulkGet(objects = []) {
const types = uniq(objects.map(o => o.type));
await this._performAuthorizationCheck(types, 'mget', {
await this._performAuthorizationCheck(types, 'bulk_get', {
objects,
});

Expand Down Expand Up @@ -90,7 +90,7 @@ 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}`);
const actions = types.map(type => `action:saved_objects/${type}/${action}`);

let result;
try {
Expand All @@ -104,7 +104,7 @@ export class SecureSavedObjectsClient {
this._auditLogger.savedObjectsAuthorizationSuccess(result.username, action, types, args);
} else {
this._auditLogger.savedObjectsAuthorizationFailure(result.username, action, types, result.missing, args);
const msg = `Unable to ${action} ${types.join(',')}, missing ${result.missing.join(',')}`;
const msg = `Unable to ${action} ${types.sort().join(',')}, missing ${result.missing.sort().join(',')}`;
throw this._client.errors.decorateForbiddenError(new Error(msg));
}
}
Expand Down
11 changes: 11 additions & 0 deletions x-pack/test/rbac_api_integration/apis/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export default function ({ loadTestFile }) {
describe('apis RBAC', () => {
loadTestFile(require.resolve('./saved_objects'));
});
}
149 changes: 149 additions & 0 deletions x-pack/test/rbac_api_integration/apis/saved_objects/bulk_get.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import expect from 'expect.js';
import { AUTHENTICATION } from './lib/authentication';

export default function ({ getService }) {
const supertest = getService('supertestWithoutAuth');
const esArchiver = getService('esArchiver');

const BULK_REQUESTS = [
{
type: 'visualization',
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
},
{
type: 'dashboard',
id: 'does not exist',
},
{
type: 'config',
id: '7.0.0-alpha1',
},
];

describe('_bulk_get', () => {
const expectResults = resp => {
expect(resp.body).to.eql({
saved_objects: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
type: 'visualization',
updated_at: '2017-09-21T18:51:23.794Z',
version: resp.body.saved_objects[0].version,
attributes: {
title: 'Count of requests',
description: '',
version: 1,
// cheat for some of the more complex attributes
visState: resp.body.saved_objects[0].attributes.visState,
uiStateJSON: resp.body.saved_objects[0].attributes.uiStateJSON,
kibanaSavedObjectMeta:
resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta,
},
},
{
id: 'does not exist',
type: 'dashboard',
error: {
statusCode: 404,
message: 'Not found',
},
},
{
id: '7.0.0-alpha1',
type: 'config',
updated_at: '2017-09-21T18:49:16.302Z',
version: resp.body.saved_objects[2].version,
attributes: {
buildNum: 8467,
defaultIndex: '91200a00-9efd-11e7-acb3-3dab96693fab',
},
},
],
});
};

const expectForbidden = resp => {
//eslint-disable-next-line max-len
const missingActions = `action:login,action:saved_objects/config/bulk_get,action:saved_objects/dashboard/bulk_get,action:saved_objects/visualization/bulk_get`;
expect(resp.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: `Unable to bulk_get config,dashboard,visualization, missing ${missingActions}`
});
};

const bulkGetTest = (description, { auth, tests }) => {
describe(description, () => {
before(() => esArchiver.load('saved_objects/basic'));
after(() => esArchiver.unload('saved_objects/basic'));

it(`should return ${tests.default.statusCode}`, async () => {
await supertest
.post(`/api/saved_objects/_bulk_get`)
.auth(auth.username, auth.password)
.send(BULK_REQUESTS)
.expect(tests.default.statusCode)
.then(tests.default.response);
});
});
};

bulkGetTest(`not a kibana user`, {
auth: {
username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME,
password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD,
},
tests: {
default: {
statusCode: 403,
response: expectForbidden,
}
}
});

bulkGetTest(`superuser`, {
auth: {
username: AUTHENTICATION.SUPERUSER.USERNAME,
password: AUTHENTICATION.SUPERUSER.PASSWORD,
},
tests: {
default: {
statusCode: 200,
response: expectResults,
},
}
});

bulkGetTest(`kibana rbac user`, {
auth: {
username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME,
password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD,
},
tests: {
default: {
statusCode: 200,
response: expectResults,
},
}
});

bulkGetTest(`kibana rbac dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
default: {
statusCode: 200,
response: expectResults,
},
}
});
});
}
111 changes: 111 additions & 0 deletions x-pack/test/rbac_api_integration/apis/saved_objects/create.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import expect from 'expect.js';
import { AUTHENTICATION } from './lib/authentication';

export default function ({ getService }) {
const supertest = getService('supertestWithoutAuth');
const esArchiver = getService('esArchiver');

describe('create', () => {
const expectResults = (resp) => {
expect(resp.body).to.have.property('id').match(/^[0-9a-f-]{36}$/);

// loose ISO8601 UTC time with milliseconds validation
expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/);

expect(resp.body).to.eql({
id: resp.body.id,
type: 'visualization',
updated_at: resp.body.updated_at,
version: 1,
attributes: {
title: 'My favorite vis'
}
});
};

const createExpectForbidden = canLogin => resp => {
expect(resp.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: `Unable to create visualization, missing ${canLogin ? '' : 'action:login,'}action:saved_objects/visualization/create`
});
};

const createTest = (description, { auth, tests }) => {
describe(description, () => {
before(() => esArchiver.load('saved_objects/basic'));
after(() => esArchiver.unload('saved_objects/basic'));
it(`should return ${tests.default.statusCode}`, async () => {
await supertest
.post(`/api/saved_objects/visualization`)
.auth(auth.username, auth.password)
.send({
attributes: {
title: 'My favorite vis'
}
})
.expect(tests.default.statusCode)
.then(tests.default.response);
});
});
};

createTest(`not a kibana user`, {
auth: {
username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME,
password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD,
},
tests: {
default: {
statusCode: 403,
response: createExpectForbidden(false),
},
}
});

createTest(`superuser`, {
auth: {
username: AUTHENTICATION.SUPERUSER.USERNAME,
password: AUTHENTICATION.SUPERUSER.PASSWORD,
},
tests: {
default: {
statusCode: 200,
response: expectResults,
},
}
});

createTest(`kibana rbac user`, {
auth: {
username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME,
password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD,
},
tests: {
default: {
statusCode: 200,
response: expectResults,
},
}
});

createTest(`kibana rbac dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
default: {
statusCode: 403,
response: createExpectForbidden(true),
},
}
});
});
}
Loading

0 comments on commit 3e8e694

Please sign in to comment.