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

RBAC Integration Tests #19647

Merged
merged 15 commits into from
Jun 4, 2018
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