From 3e8e694313f205aef3abad1ac8d763d2ed2b6e29 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Mon, 4 Jun 2018 15:59:59 -0400 Subject: [PATCH] RBAC Integration Tests (#19647) * 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 --- .../security/server/lib/audit_logger.test.js | 2 +- .../lib/authorization/has_privileges.js | 3 +- .../lib/authorization/has_privileges.test.js | 18 +- .../server/lib/privileges/privileges.js | 4 +- .../secure_saved_objects_client.js | 10 +- .../test/rbac_api_integration/apis/index.js | 11 + .../apis/saved_objects/bulk_get.js | 149 ++++++++++++ .../apis/saved_objects/create.js | 111 +++++++++ .../apis/saved_objects/delete.js | 127 +++++++++++ .../apis/saved_objects/find.js | 212 ++++++++++++++++++ .../apis/saved_objects/get.js | 143 ++++++++++++ .../apis/saved_objects/index.js | 51 +++++ .../apis/saved_objects/lib/authentication.js | 24 ++ .../apis/saved_objects/update.js | 152 +++++++++++++ x-pack/test/rbac_api_integration/config.js | 57 +++++ .../test/rbac_api_integration/services/es.js | 20 ++ 16 files changed, 1083 insertions(+), 11 deletions(-) create mode 100644 x-pack/test/rbac_api_integration/apis/index.js create mode 100644 x-pack/test/rbac_api_integration/apis/saved_objects/bulk_get.js create mode 100644 x-pack/test/rbac_api_integration/apis/saved_objects/create.js create mode 100644 x-pack/test/rbac_api_integration/apis/saved_objects/delete.js create mode 100644 x-pack/test/rbac_api_integration/apis/saved_objects/find.js create mode 100644 x-pack/test/rbac_api_integration/apis/saved_objects/get.js create mode 100644 x-pack/test/rbac_api_integration/apis/saved_objects/index.js create mode 100644 x-pack/test/rbac_api_integration/apis/saved_objects/lib/authentication.js create mode 100644 x-pack/test/rbac_api_integration/apis/saved_objects/update.js create mode 100644 x-pack/test/rbac_api_integration/config.js create mode 100644 x-pack/test/rbac_api_integration/services/es.js diff --git a/x-pack/plugins/security/server/lib/audit_logger.test.js b/x-pack/plugins/security/server/lib/audit_logger.test.js index da727552aae58..51c8698da818d 100644 --- a/x-pack/plugins/security/server/lib/audit_logger.test.js +++ b/x-pack/plugins/security/server/lib/audit_logger.test.js @@ -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', diff --git a/x-pack/plugins/security/server/lib/authorization/has_privileges.js b/x-pack/plugins/security/server/lib/authorization/has_privileges.js index 03a0642ecb8e8..5f119db027cb6 100644 --- a/x-pack/plugins/security/server/lib/authorization/has_privileges.js +++ b/x-pack/plugins/security/server/lib/authorization/has_privileges.js @@ -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, }; }; diff --git a/x-pack/plugins/security/server/lib/authorization/has_privileges.test.js b/x-pack/plugins/security/server/lib/authorization/has_privileges.test.js index aed3dbcfe94cc..b2e2759c3419b 100644 --- a/x-pack/plugins/security/server/lib/authorization/has_privileges.test.js +++ b/x-pack/plugins/security/server/lib/authorization/has_privileges.test.js @@ -241,8 +241,8 @@ 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, }); @@ -250,3 +250,17 @@ test(`doesn't throw error if missing version privilege and missing login privile 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()]); +}); diff --git a/x-pack/plugins/security/server/lib/privileges/privileges.js b/x-pack/plugins/security/server/lib/privileges/privileges.js index 129d4b3abfb3e..e2dc9b2ff7ece 100644 --- a/x-pack/plugins/security/server/lib/privileges/privileges.js +++ b/x-pack/plugins/security/server/lib/privileges/privileges.js @@ -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], []); } diff --git a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js index 08ee87d5f281b..0a2f9490c1664 100644 --- a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js +++ b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js @@ -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, }); @@ -52,7 +52,7 @@ export class SecureSavedObjectsClient { } async find(options = {}) { - await this._performAuthorizationCheck(options.type, 'search', { + await this._performAuthorizationCheck(options.type, 'find', { options, }); @@ -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, }); @@ -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 { @@ -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)); } } diff --git a/x-pack/test/rbac_api_integration/apis/index.js b/x-pack/test/rbac_api_integration/apis/index.js new file mode 100644 index 0000000000000..eff74e5f38dbe --- /dev/null +++ b/x-pack/test/rbac_api_integration/apis/index.js @@ -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')); + }); +} diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/bulk_get.js b/x-pack/test/rbac_api_integration/apis/saved_objects/bulk_get.js new file mode 100644 index 0000000000000..18fe5a015dc78 --- /dev/null +++ b/x-pack/test/rbac_api_integration/apis/saved_objects/bulk_get.js @@ -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, + }, + } + }); + }); +} diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/create.js b/x-pack/test/rbac_api_integration/apis/saved_objects/create.js new file mode 100644 index 0000000000000..0db0fc41b5c4a --- /dev/null +++ b/x-pack/test/rbac_api_integration/apis/saved_objects/create.js @@ -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), + }, + } + }); + }); +} diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/delete.js b/x-pack/test/rbac_api_integration/apis/saved_objects/delete.js new file mode 100644 index 0000000000000..ea73c927b869e --- /dev/null +++ b/x-pack/test/rbac_api_integration/apis/saved_objects/delete.js @@ -0,0 +1,127 @@ +/* + * 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('delete', () => { + + const expectEmpty = (resp) => { + expect(resp.body).to.eql({}); + }; + + const expectNotFound = (resp) => { + expect(resp.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found' + }); + }; + + const createExpectForbidden = canLogin => resp => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `Unable to delete dashboard, missing ${canLogin ? '' : 'action:login,'}action:saved_objects/dashboard/delete` + }); + }; + + const deleteTest = (description, { auth, tests }) => { + describe(description, () => { + before(() => esArchiver.load('saved_objects/basic')); + after(() => esArchiver.unload('saved_objects/basic')); + + it(`should return ${tests.actualId.statusCode} when deleting a doc`, async () => ( + await supertest + .delete(`/api/saved_objects/dashboard/be3733a0-9efe-11e7-acb3-3dab96693fab`) + .auth(auth.username, auth.password) + .expect(tests.actualId.statusCode) + .then(tests.actualId.response) + )); + + it(`should return ${tests.invalidId.statusCode} when deleting an unknown doc`, async () => ( + await supertest + .delete(`/api/saved_objects/dashboard/not-a-real-id`) + .auth(auth.username, auth.password) + .expect(tests.invalidId.statusCode) + .then(tests.invalidId.response) + )); + }); + }; + + deleteTest(`not a kibana user`, { + auth: { + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + }, + tests: { + actualId: { + statusCode: 403, + response: createExpectForbidden(false), + }, + invalidId: { + statusCode: 403, + response: createExpectForbidden(false), + } + } + }); + + deleteTest(`superuser`, { + auth: { + username: AUTHENTICATION.SUPERUSER.USERNAME, + password: AUTHENTICATION.SUPERUSER.PASSWORD, + }, + tests: { + actualId: { + statusCode: 200, + response: expectEmpty, + }, + invalidId: { + statusCode: 404, + response: expectNotFound, + } + } + }); + + deleteTest(`kibana rbac user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + }, + tests: { + actualId: { + statusCode: 200, + response: expectEmpty, + }, + invalidId: { + statusCode: 404, + response: expectNotFound, + } + } + }); + + deleteTest(`kibana rbac dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + actualId: { + statusCode: 403, + response: createExpectForbidden(true), + }, + invalidId: { + statusCode: 403, + response: createExpectForbidden(true), + } + } + }); + }); +} diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/find.js b/x-pack/test/rbac_api_integration/apis/saved_objects/find.js new file mode 100644 index 0000000000000..72a3fa2ec2bfc --- /dev/null +++ b/x-pack/test/rbac_api_integration/apis/saved_objects/find.js @@ -0,0 +1,212 @@ +/* + * 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('find', () => { + + const expectResults = (resp) => { + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 1, + saved_objects: [ + { + type: 'visualization', + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + version: 1, + attributes: { + 'title': 'Count of requests' + } + } + ] + }); + }; + + const createExpectEmpty = (page, perPage, total) => (resp) => { + expect(resp.body).to.eql({ + page: page, + per_page: perPage, + total: total, + saved_objects: [] + }); + }; + + const createExpectForbidden = (canLogin, type) => resp => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `Unable to find ${type}, missing ${canLogin ? '' : 'action:login,'}action:saved_objects/${type}/find` + }); + }; + + const findTest = (description, { auth, tests }) => { + describe(description, () => { + before(() => esArchiver.load('saved_objects/basic')); + after(() => esArchiver.unload('saved_objects/basic')); + + it(`should return ${tests.normal.statusCode} with ${tests.normal.description}`, async () => ( + await supertest + .get('/api/saved_objects/_find?type=visualization&fields=title') + .auth(auth.username, auth.password) + .expect(tests.normal.statusCode) + .then(tests.normal.response) + )); + + describe('unknown type', () => { + it(`should return ${tests.unknownType.statusCode} with ${tests.unknownType.description}`, async () => ( + await supertest + .get('/api/saved_objects/_find?type=wigwags') + .auth(auth.username, auth.password) + .expect(tests.unknownType.statusCode) + .then(tests.unknownType.response) + )); + }); + + describe('page beyond total', () => { + it(`should return ${tests.pageBeyondTotal.statusCode} with ${tests.pageBeyondTotal.description}`, async () => ( + await supertest + .get('/api/saved_objects/_find?type=visualization&page=100&per_page=100') + .auth(auth.username, auth.password) + .expect(tests.pageBeyondTotal.statusCode) + .then(tests.pageBeyondTotal.response) + )); + }); + + describe('unknown search field', () => { + it(`should return ${tests.unknownSearchField.statusCode} with ${tests.unknownSearchField.description}`, async () => ( + await supertest + .get('/api/saved_objects/_find?type=wigwags&search_fields=a') + .auth(auth.username, auth.password) + .expect(tests.unknownSearchField.statusCode) + .then(tests.unknownSearchField.response) + )); + }); + }); + }; + + findTest(`not a kibana user`, { + auth: { + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + }, + tests: { + normal: { + description: 'forbidden login and find visualization message', + statusCode: 403, + response: createExpectForbidden(false, 'visualization'), + }, + unknownType: { + description: 'forbidden login and find wigwags message', + statusCode: 403, + response: createExpectForbidden(false, 'wigwags'), + }, + pageBeyondTotal: { + description: 'forbidden login and find visualization message', + statusCode: 403, + response: createExpectForbidden(false, 'visualization'), + }, + unknownSearchField: { + description: 'forbidden login and find wigwags message', + statusCode: 403, + response: createExpectForbidden(false, 'wigwags'), + }, + } + }); + + findTest(`superuser`, { + auth: { + username: AUTHENTICATION.SUPERUSER.USERNAME, + password: AUTHENTICATION.SUPERUSER.PASSWORD, + }, + tests: { + normal: { + description: 'individual responses', + statusCode: 200, + response: expectResults, + }, + unknownType: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + }, + }); + + findTest(`kibana rbac user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + }, + tests: { + normal: { + description: 'individual responses', + statusCode: 200, + response: expectResults, + }, + unknownType: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + }, + }); + + findTest(`kibana rbac dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + normal: { + description: 'individual responses', + statusCode: 200, + response: expectResults, + }, + unknownType: { + description: 'forbidden find wigwags message', + statusCode: 403, + response: createExpectForbidden(true, 'wigwags'), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'forbidden find wigwags message', + statusCode: 403, + response: createExpectForbidden(true, 'wigwags'), + }, + } + }); + }); +} diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/get.js b/x-pack/test/rbac_api_integration/apis/saved_objects/get.js new file mode 100644 index 0000000000000..e5a462d30d30c --- /dev/null +++ b/x-pack/test/rbac_api_integration/apis/saved_objects/get.js @@ -0,0 +1,143 @@ +/* + * 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('get', () => { + + const expectResults = (resp) => { + expect(resp.body).to.eql({ + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + type: 'visualization', + updated_at: '2017-09-21T18:51:23.794Z', + version: resp.body.version, + attributes: { + title: 'Count of requests', + description: '', + version: 1, + // cheat for some of the more complex attributes + visState: resp.body.attributes.visState, + uiStateJSON: resp.body.attributes.uiStateJSON, + kibanaSavedObjectMeta: resp.body.attributes.kibanaSavedObjectMeta + } + }); + }; + + const expectNotFound = (resp) => { + expect(resp.body).to.eql({ + error: 'Not Found', + message: 'Not Found', + statusCode: 404, + }); + }; + + const expectForbidden = resp => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `Unable to get visualization, missing action:login,action:saved_objects/visualization/get` + }); + }; + + const getTest = (description, { auth, tests }) => { + describe(description, () => { + before(() => esArchiver.load('saved_objects/basic')); + after(() => esArchiver.unload('saved_objects/basic')); + + it(`should return ${tests.exists.statusCode}`, async () => ( + await supertest + .get(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`) + .auth(auth.username, auth.password) + .expect(tests.exists.statusCode) + .then(tests.exists.response) + )); + + describe('document does not exist', () => { + it(`should return ${tests.doesntExist.statusCode}`, async () => ( + await supertest + .get(`/api/saved_objects/visualization/foobar`) + .auth(auth.username, auth.password) + .expect(tests.doesntExist.statusCode) + .then(tests.doesntExist.response) + )); + }); + }); + }; + + getTest(`not a kibana user`, { + auth: { + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + }, + tests: { + exists: { + statusCode: 403, + response: expectForbidden, + }, + doesntExist: { + statusCode: 403, + response: expectForbidden, + }, + } + }); + + getTest(`superuser`, { + auth: { + username: AUTHENTICATION.SUPERUSER.USERNAME, + password: AUTHENTICATION.SUPERUSER.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: expectResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + } + }); + + getTest(`kibana rbac user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: expectResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + } + }); + + getTest(`kibana rbac dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: expectResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + } + }); + }); +} diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/index.js b/x-pack/test/rbac_api_integration/apis/saved_objects/index.js new file mode 100644 index 0000000000000..644bf23220648 --- /dev/null +++ b/x-pack/test/rbac_api_integration/apis/saved_objects/index.js @@ -0,0 +1,51 @@ +/* + * 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 { AUTHENTICATION } from "./lib/authentication"; + +export default function ({ loadTestFile, getService }) { + const es = getService('es'); + + describe('saved_objects', () => { + before(async () => { + await es.shield.putUser({ + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + body: { + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + roles: [], + full_name: 'not a kibana user', + email: 'not_a_kibana_user@elastic.co', + } + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + roles: ['kibana_rbac_user'], + full_name: 'a kibana user', + email: 'a_kibana_user@elastic.co', + } + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + roles: ["kibana_rbac_dashboard_only_user"], + full_name: 'a kibana dashboard only user', + email: 'a_kibana_dashboard_only_user@elastic.co', + } + }); + }); + loadTestFile(require.resolve('./bulk_get')); + loadTestFile(require.resolve('./create')); + loadTestFile(require.resolve('./delete')); + loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./update')); + }); +} diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/lib/authentication.js b/x-pack/test/rbac_api_integration/apis/saved_objects/lib/authentication.js new file mode 100644 index 0000000000000..e095a032934ea --- /dev/null +++ b/x-pack/test/rbac_api_integration/apis/saved_objects/lib/authentication.js @@ -0,0 +1,24 @@ +/* + * 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 const AUTHENTICATION = { + NOT_A_KIBANA_USER: { + USERNAME: 'not_a_kibana_user', + PASSWORD: 'password' + }, + SUPERUSER: { + USERNAME: 'elastic', + PASSWORD: 'changeme' + }, + KIBANA_RBAC_USER: { + USERNAME: 'a_kibana_rbac_user', + PASSWORD: 'password' + }, + KIBANA_RBAC_DASHBOARD_ONLY_USER: { + USERNAME: 'a_kibana_rbac_dashboard_only_user', + PASSWORD: 'password' + } +}; diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/update.js b/x-pack/test/rbac_api_integration/apis/saved_objects/update.js new file mode 100644 index 0000000000000..a9f5f0ab81aab --- /dev/null +++ b/x-pack/test/rbac_api_integration/apis/saved_objects/update.js @@ -0,0 +1,152 @@ +/* + * 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('update', () => { + const expectResults = resp => { + // loose uuid validation + 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: 2, + attributes: { + title: 'My second favorite vis' + } + }); + }; + + const expectNotFound = resp => { + expect(resp.body).eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found' + }); + }; + + const createExpectForbidden = canLogin => resp => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `Unable to update visualization, missing ${canLogin ? '' : 'action:login,'}action:saved_objects/visualization/update` + }); + }; + + const updateTest = (description, { auth, tests }) => { + describe(description, () => { + before(() => esArchiver.load('saved_objects/basic')); + after(() => esArchiver.unload('saved_objects/basic')); + it(`should return ${tests.exists.statusCode}`, async () => { + await supertest + .put(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`) + .auth(auth.username, auth.password) + .send({ + attributes: { + title: 'My second favorite vis' + } + }) + .expect(tests.exists.statusCode) + .then(tests.exists.response); + }); + + describe('unknown id', () => { + it(`should return ${tests.doesntExist.statusCode}`, async () => { + await supertest + .put(`/api/saved_objects/visualization/not an id`) + .auth(auth.username, auth.password) + .send({ + attributes: { + title: 'My second favorite vis' + } + }) + .expect(tests.doesntExist.statusCode) + .then(tests.doesntExist.response); + }); + }); + }); + }; + + updateTest(`not a kibana user`, { + auth: { + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + }, + tests: { + exists: { + statusCode: 403, + response: createExpectForbidden(false), + }, + doesntExist: { + statusCode: 403, + response: createExpectForbidden(false), + }, + } + }); + + updateTest(`superuser`, { + auth: { + username: AUTHENTICATION.SUPERUSER.USERNAME, + password: AUTHENTICATION.SUPERUSER.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: expectResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + } + }); + + updateTest(`kibana rbac user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: expectResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + } + }); + + updateTest(`kibana rbac dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + exists: { + statusCode: 403, + response: createExpectForbidden(true), + }, + doesntExist: { + statusCode: 403, + response: createExpectForbidden(true), + }, + } + }); + + }); +} diff --git a/x-pack/test/rbac_api_integration/config.js b/x-pack/test/rbac_api_integration/config.js new file mode 100644 index 0000000000000..481b14913da91 --- /dev/null +++ b/x-pack/test/rbac_api_integration/config.js @@ -0,0 +1,57 @@ +/* + * 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 path from 'path'; +import { resolveKibanaPath } from '@kbn/plugin-helpers'; +import { EsProvider } from './services/es'; + +export default async function ({ readConfigFile }) { + + const config = { + kibana: { + api: await readConfigFile(resolveKibanaPath('test/api_integration/config.js')), + functional: await readConfigFile(require.resolve('../../../test/functional/config.js')) + }, + xpack: { + api: await readConfigFile(require.resolve('../api_integration/config.js')) + } + }; + + return { + testFiles: [require.resolve('./apis')], + servers: config.xpack.api.get('servers'), + services: { + es: EsProvider, + supertest: config.kibana.api.get('services.supertest'), + supertestWithoutAuth: config.xpack.api.get('services.supertestWithoutAuth'), + esArchiver: config.kibana.functional.get('services.esArchiver'), + }, + junit: { + reportName: 'X-Pack RBAC API Integration Tests', + }, + + esArchiver: { + directory: resolveKibanaPath(path.join('test', 'api_integration', 'fixtures', 'es_archiver')) + }, + + esTestCluster: { + ...config.xpack.api.get('esTestCluster'), + serverArgs: [ + ...config.xpack.api.get('esTestCluster.serverArgs'), + ], + }, + + kbnTestServer: { + ...config.xpack.api.get('kbnTestServer'), + serverArgs: [ + ...config.xpack.api.get('kbnTestServer.serverArgs'), + '--optimize.enabled=false', + '--xpack.security.rbac.enabled=true', + '--server.xsrf.disableProtection=true', + ], + }, + }; +} diff --git a/x-pack/test/rbac_api_integration/services/es.js b/x-pack/test/rbac_api_integration/services/es.js new file mode 100644 index 0000000000000..420541fa7ec5f --- /dev/null +++ b/x-pack/test/rbac_api_integration/services/es.js @@ -0,0 +1,20 @@ +/* + * 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 { format as formatUrl } from 'url'; + +import elasticsearch from 'elasticsearch'; +import shieldPlugin from '../../../server/lib/esjs_shield_plugin'; + +export function EsProvider({ getService }) { + const config = getService('config'); + + return new elasticsearch.Client({ + host: formatUrl(config.get('servers.elasticsearch')), + requestTimeout: config.get('timeouts.esRequestTimeout'), + plugins: [shieldPlugin] + }); +}