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] + }); +}