diff --git a/x-pack/plugins/security/server/lib/authorization/__snapshots__/check_privileges.test.js.snap b/x-pack/plugins/security/server/lib/authorization/__snapshots__/check_privileges.test.js.snap index 4c0c88a8f8313c..7609d57b702faf 100644 --- a/x-pack/plugins/security/server/lib/authorization/__snapshots__/check_privileges.test.js.snap +++ b/x-pack/plugins/security/server/lib/authorization/__snapshots__/check_privileges.test.js.snap @@ -1,5 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`with a malformed Elasticsearch response throws a validation error when an extra index privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "index" fails because [child "default-index" fails because ["oopsAnExtraPrivilege" is not allowed]]]`; + +exports[`with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because ["oops-an-unexpected-privilege" is not allowed]]]]`; + +exports[`with a malformed Elasticsearch response throws a validation error when index privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "index" fails because [child "default-index" fails because [child "read" fails because ["read" is required]]]]`; + +exports[`with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]`; + exports[`with index privileges throws error if missing version privilege and has login privilege 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; exports[`with no index privileges throws error if missing version privilege and has login privilege 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; diff --git a/x-pack/plugins/security/server/lib/authorization/__snapshots__/validate_es_response.test.js.snap b/x-pack/plugins/security/server/lib/authorization/__snapshots__/validate_es_response.test.js.snap new file mode 100644 index 00000000000000..5e1e26a9023ae7 --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/__snapshots__/validate_es_response.test.js.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`validateEsPrivilegeResponse fails validation when an action is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [child \\"action3\\" fails because [\\"action3\\" must be a boolean]]]]"`; + +exports[`validateEsPrivilegeResponse fails validation when an action is missing in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [child \\"action2\\" fails because [\\"action2\\" is required]]]]"`; + +exports[`validateEsPrivilegeResponse fails validation when an extra action is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [\\"action4\\" is not allowed]]]"`; + +exports[`validateEsPrivilegeResponse fails validation when an extra application is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [\\"otherApplication\\" is not allowed]"`; + +exports[`validateEsPrivilegeResponse fails validation when an unexpected resource property is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [\\"foo-resource\\" is required]]]"`; + +exports[`validateEsPrivilegeResponse fails validation when the "application" property is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [\\"application\\" is required]"`; + +exports[`validateEsPrivilegeResponse fails validation when the expected resource property is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [\\"foo-resource\\" is required]]]"`; + +exports[`validateEsPrivilegeResponse fails validation when the requested application is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [\\"foo-application\\" is required]]"`; + +exports[`validateEsPrivilegeResponse fails validation when the resource propertry is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [\\"foo-resource\\" must be an object]]]"`; + +exports[`validateEsPrivilegeResponse legacy should fail if the create index privilege is malformed 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"create\\" fails because [\\"create\\" must be a boolean]]]"`; + +exports[`validateEsPrivilegeResponse legacy should fail if the create index privilege is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"create\\" fails because [\\"create\\" is required]]]"`; + +exports[`validateEsPrivilegeResponse legacy should fail if the delete index privilege is malformed 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"delete\\" fails because [\\"delete\\" must be a boolean]]]"`; + +exports[`validateEsPrivilegeResponse legacy should fail if the delete index privilege is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"delete\\" fails because [\\"delete\\" is required]]]"`; + +exports[`validateEsPrivilegeResponse legacy should fail if the index privilege response contains an extra privilege 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [\\"foo-permission\\" is not allowed]]"`; + +exports[`validateEsPrivilegeResponse legacy should fail if the index privilege response returns an extra index 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [\\"anotherIndex\\" is not allowed]"`; + +exports[`validateEsPrivilegeResponse legacy should fail if the index property is missing 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [\\"index\\" is required]"`; + +exports[`validateEsPrivilegeResponse legacy should fail if the kibana index is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [\\".kibana\\" is required]]"`; + +exports[`validateEsPrivilegeResponse legacy should fail if the read index privilege is malformed 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"read\\" fails because [\\"read\\" must be a boolean]]]"`; + +exports[`validateEsPrivilegeResponse legacy should fail if the read index privilege is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"read\\" fails because [\\"read\\" is required]]]"`; + +exports[`validateEsPrivilegeResponse legacy should fail if the view_index_metadata index privilege is malformed 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"view_index_metadata\\" fails because [\\"view_index_metadata\\" must be a boolean]]]"`; + +exports[`validateEsPrivilegeResponse legacy should fail if the view_index_metadata index privilege is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"view_index_metadata\\" fails because [\\"view_index_metadata\\" is required]]]"`; diff --git a/x-pack/plugins/security/server/lib/authorization/check_privileges.js b/x-pack/plugins/security/server/lib/authorization/check_privileges.js index 594966e5b3e4ef..2f5c58293d15f2 100644 --- a/x-pack/plugins/security/server/lib/authorization/check_privileges.js +++ b/x-pack/plugins/security/server/lib/authorization/check_privileges.js @@ -6,6 +6,8 @@ import { uniq } from 'lodash'; import { ALL_RESOURCE } from '../../../common/constants'; +import { buildLegacyIndexPrivileges } from './privileges'; +import { validateEsPrivilegeResponse } from './validate_es_response'; export const CHECK_PRIVILEGES_RESULT = { UNAUTHORIZED: Symbol('Unauthorized'), @@ -59,13 +61,15 @@ export function checkPrivilegesWithRequestFactory(shieldClient, config, actions, }], index: [{ names: [kibanaIndex], - privileges: ['create', 'delete', 'read', 'view_index_metadata'] + privileges: buildLegacyIndexPrivileges() }], } }); + validateEsPrivilegeResponse(hasPrivilegesResponse, application, allApplicationPrivileges, [ALL_RESOURCE], kibanaIndex); + const applicationPrivilegesResponse = hasPrivilegesResponse.application[application][ALL_RESOURCE]; - const indexPrivilegesResponse = hasPrivilegesResponse.index[kibanaIndex]; + const indexPrivilegesResponse = hasPrivilegesResponse.index[kibanaIndex]; if (hasIncompatibileVersion(applicationPrivilegesResponse)) { throw new Error('Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.'); diff --git a/x-pack/plugins/security/server/lib/authorization/check_privileges.test.js b/x-pack/plugins/security/server/lib/authorization/check_privileges.test.js index edf3b42d11c28f..a64110515e803c 100644 --- a/x-pack/plugins/security/server/lib/authorization/check_privileges.test.js +++ b/x-pack/plugins/security/server/lib/authorization/check_privileges.test.js @@ -94,7 +94,7 @@ const checkPrivilegesTest = ( ]) }], index: [{ - names: [ defaultKibanaIndex ], + names: [defaultKibanaIndex], privileges: ['create', 'delete', 'read', 'view_index_metadata'] }], } @@ -328,3 +328,75 @@ describe('with no application privileges', () => { }); }); }); + +describe('with a malformed Elasticsearch response', () => { + const indexPrivilegesResponse = { + create: true, + delete: true, + read: true, + view_index_metadata: true, + }; + + checkPrivilegesTest('throws a validation error when an extra privilege is present in the response', { + username: 'foo-username', + privileges: [ + `action:saved_objects/${savedObjectTypes[0]}/get`, + ], + applicationPrivilegesResponse: { + [mockActions.version]: true, + [mockActions.login]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + ['oops-an-unexpected-privilege']: true, + }, + indexPrivilegesResponse, + expectErrorThrown: true, + }); + + checkPrivilegesTest('throws a validation error when privileges are missing in the response', { + username: 'foo-username', + privileges: [ + `action:saved_objects/${savedObjectTypes[0]}/get`, + ], + applicationPrivilegesResponse: { + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + }, + indexPrivilegesResponse, + expectErrorThrown: true, + }); + + checkPrivilegesTest('throws a validation error when an extra index privilege is present in the response', { + username: 'foo-username', + privileges: [ + `action:saved_objects/${savedObjectTypes[0]}/get`, + ], + applicationPrivilegesResponse: { + [mockActions.version]: true, + [mockActions.login]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + }, + indexPrivilegesResponse: { + ...indexPrivilegesResponse, + oopsAnExtraPrivilege: true, + }, + expectErrorThrown: true, + }); + + const missingIndexPrivileges = { + ...indexPrivilegesResponse + }; + delete missingIndexPrivileges.read; + + checkPrivilegesTest('throws a validation error when index privileges are missing in the response', { + username: 'foo-username', + privileges: [ + `action:saved_objects/${savedObjectTypes[0]}/get`, + ], + applicationPrivilegesResponse: { + [mockActions.version]: true, + [mockActions.login]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + }, + indexPrivilegesResponse: missingIndexPrivileges, + expectErrorThrown: true, + }); +}); diff --git a/x-pack/plugins/security/server/lib/authorization/privileges.js b/x-pack/plugins/security/server/lib/authorization/privileges.js index 9ded71662ed51c..6f64871ed75566 100644 --- a/x-pack/plugins/security/server/lib/authorization/privileges.js +++ b/x-pack/plugins/security/server/lib/authorization/privileges.js @@ -28,3 +28,7 @@ export function buildPrivilegeMap(savedObjectTypes, application, actions) { } }; } + +export function buildLegacyIndexPrivileges() { + return ['create', 'delete', 'read', 'view_index_metadata']; +} diff --git a/x-pack/plugins/security/server/lib/authorization/validate_es_response.js b/x-pack/plugins/security/server/lib/authorization/validate_es_response.js new file mode 100644 index 00000000000000..34d618398bc3d3 --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/validate_es_response.js @@ -0,0 +1,65 @@ +/* + * 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 Joi from 'joi'; +import { buildLegacyIndexPrivileges } from './privileges'; + +const legacyIndexPrivilegesSchema = Joi.object({ + ...buildLegacyIndexPrivileges().reduce((acc, privilege) => { + return { + ...acc, + [privilege]: Joi.bool().required() + }; + }, {}) +}).required(); + +export function validateEsPrivilegeResponse(response, application, actions, resources, kibanaIndex) { + const schema = buildValidationSchema(application, actions, resources, kibanaIndex); + const { error, value } = schema.validate(response); + + if (error) { + throw new Error(`Invalid response received from Elasticsearch has_privilege endpoint. ${error}`); + } + + return value; +} + +function buildActionsValidationSchema(actions) { + return Joi.object({ + ...actions.reduce((acc, action) => { + return { + ...acc, + [action]: Joi.bool().required() + }; + }, {}) + }).required(); +} + +function buildValidationSchema(application, actions, resources, kibanaIndex) { + + const actionValidationSchema = buildActionsValidationSchema(actions); + + const resourceValidationSchema = Joi.object({ + ...resources.reduce((acc, resource) => { + return { + ...acc, + [resource]: actionValidationSchema + }; + }, {}) + }).required(); + + return Joi.object({ + username: Joi.string().required(), + has_all_requested: Joi.bool(), + cluster: Joi.object(), + application: Joi.object({ + [application]: resourceValidationSchema, + }).required(), + index: Joi.object({ + [kibanaIndex]: legacyIndexPrivilegesSchema + }).required() + }).required(); +} diff --git a/x-pack/plugins/security/server/lib/authorization/validate_es_response.test.js b/x-pack/plugins/security/server/lib/authorization/validate_es_response.test.js new file mode 100644 index 00000000000000..f3dbad1b56ac95 --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/validate_es_response.test.js @@ -0,0 +1,357 @@ +/* + * 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 { validateEsPrivilegeResponse } from "./validate_es_response"; +import { buildLegacyIndexPrivileges } from "./privileges"; + +const resource = 'foo-resource'; +const application = 'foo-application'; +const kibanaIndex = '.kibana'; + +const commonResponse = { + username: 'user', + has_all_requested: true, +}; + +describe('validateEsPrivilegeResponse', () => { + const legacyIndexResponse = { + [kibanaIndex]: { + 'create': true, + 'delete': true, + 'read': true, + 'view_index_metadata': true, + } + }; + + it('should validate a proper response', () => { + const response = { + ...commonResponse, + application: { + [application]: { + [resource]: { + action1: true, + action2: true, + action3: true + } + } + }, + index: legacyIndexResponse + }; + + const result = validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex); + expect(result).toEqual(response); + }); + + it('fails validation when an action is missing in the response', () => { + const response = { + ...commonResponse, + application: { + [application]: { + [resource]: { + action1: true, + action3: true + } + } + }, + index: legacyIndexResponse + }; + + expect(() => + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex) + ).toThrowErrorMatchingSnapshot(); + }); + + it('fails validation when an extra action is present in the response', () => { + const response = { + ...commonResponse, + application: { + [application]: { + [resource]: { + action1: true, + action2: true, + action3: true, + action4: true, + } + } + }, + index: legacyIndexResponse + }; + + expect(() => + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex) + ).toThrowErrorMatchingSnapshot(); + }); + + it('fails validation when an action is malformed in the response', () => { + const response = { + ...commonResponse, + application: { + [application]: { + [resource]: { + action1: true, + action2: true, + action3: 'not a boolean', + } + } + }, + index: legacyIndexResponse + }; + + expect(() => + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex) + ).toThrowErrorMatchingSnapshot(); + }); + + it('fails validation when an extra application is present in the response', () => { + const response = { + ...commonResponse, + application: { + [application]: { + [resource]: { + action1: true, + action2: true, + action3: true, + } + }, + otherApplication: { + [resource]: { + action1: true, + action2: true, + action3: true, + } + } + }, + index: legacyIndexResponse + }; + + expect(() => + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex) + ).toThrowErrorMatchingSnapshot(); + }); + + it('fails validation when the requested application is missing from the response', () => { + const response = { + ...commonResponse, + application: {}, + index: legacyIndexResponse + }; + + expect(() => + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex) + ).toThrowErrorMatchingSnapshot(); + }); + + it('fails validation when the "application" property is missing from the response', () => { + const response = { + ...commonResponse, + index: {} + }; + + expect(() => + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex) + ).toThrowErrorMatchingSnapshot(); + }); + + it('fails validation when the expected resource property is missing from the response', () => { + const response = { + ...commonResponse, + application: { + [application]: {} + }, + index: legacyIndexResponse + }; + + expect(() => + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex) + ).toThrowErrorMatchingSnapshot(); + }); + + it('fails validation when an unexpected resource property is present in the response', () => { + const response = { + ...commonResponse, + application: { + [application]: { + 'other-resource': { + action1: true, + action2: true, + action3: true, + } + } + }, + index: legacyIndexResponse + }; + + expect(() => + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex) + ).toThrowErrorMatchingSnapshot(); + }); + + it('fails validation when the resource propertry is malformed in the response', () => { + const response = { + ...commonResponse, + application: { + [application]: { + [resource]: 'not-an-object' + } + }, + index: legacyIndexResponse + }; + + expect(() => + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex) + ).toThrowErrorMatchingSnapshot(); + }); + + describe('legacy', () => { + it('should validate a proper response', () => { + const response = { + ...commonResponse, + application: { + [application]: { + [resource]: { + action1: true + } + } + }, + index: legacyIndexResponse + }; + + const result = validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex); + expect(result).toEqual(response); + }); + + it('should fail if the index property is missing', () => { + const response = { + ...commonResponse, + application: { + [application]: { + [resource]: { + action1: true + } + } + } + }; + + expect(() => + validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex) + ).toThrowErrorMatchingSnapshot(); + }); + + it('should fail if the kibana index is missing from the response', () => { + const response = { + ...commonResponse, + application: { + [application]: { + [resource]: { + action1: true + } + } + }, + index: {} + }; + + expect(() => + validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex) + ).toThrowErrorMatchingSnapshot(); + }); + + it('should fail if the index privilege response returns an extra index', () => { + const response = { + ...commonResponse, + application: { + [application]: { + [resource]: { + action1: true + } + } + }, + index: { + ...legacyIndexResponse, + 'anotherIndex': { + foo: true + } + } + }; + + expect(() => + validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex) + ).toThrowErrorMatchingSnapshot(); + }); + + it('should fail if the index privilege response contains an extra privilege', () => { + const response = { + ...commonResponse, + application: { + [application]: { + [resource]: { + action1: true + } + } + }, + index: { + [kibanaIndex]: { + ...legacyIndexResponse[kibanaIndex], + 'foo-permission': true + } + } + }; + + expect(() => + validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex) + ).toThrowErrorMatchingSnapshot(); + }); + + buildLegacyIndexPrivileges().forEach(privilege => { + test(`should fail if the ${privilege} index privilege is missing from the response`, () => { + const response = { + ...commonResponse, + application: { + [application]: { + [resource]: { + action1: true + } + } + }, + index: { + [kibanaIndex]: { + ...legacyIndexResponse[kibanaIndex] + } + } + }; + + delete response.index[kibanaIndex][privilege]; + + expect(() => + validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex) + ).toThrowErrorMatchingSnapshot(); + }); + + test(`should fail if the ${privilege} index privilege is malformed`, () => { + const response = { + ...commonResponse, + application: { + [application]: { + [resource]: { + action1: true + } + } + }, + index: { + [kibanaIndex]: { + ...legacyIndexResponse[kibanaIndex] + } + } + }; + + response.index[kibanaIndex][privilege] = 'not a boolean'; + + expect(() => + validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex) + ).toThrowErrorMatchingSnapshot(); + }); + }); + }); +});