From 70abbfaaf16503238d025ce185fecfa287ffcc78 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 9 May 2019 15:12:21 -0400 Subject: [PATCH 01/35] WIP --- .../basic_login_form/basic_login_form.tsx | 10 +- .../unauthorized_login_form/index.ts | 7 + .../unauthorized_login_form.tsx | 50 ++++ .../get_privileges_with_request.ts | 30 +++ .../server/lib/authorization/index.ts | 1 + .../server/lib/authorization/service.ts | 7 + .../transform_applications_from_es.ts | 227 ++++++++++++++++++ .../server/lib/is_authorized_kibana_user.ts | 18 ++ .../server/routes/api/public/roles/get.js | 143 +---------- .../server/routes/api/v1/authenticate.js | 8 + x-pack/plugins/spaces/types.d.ts | 6 +- x-pack/server/lib/esjs_shield_plugin.js | 7 + 12 files changed, 369 insertions(+), 145 deletions(-) create mode 100644 x-pack/plugins/security/public/views/login/components/unauthorized_login_form/index.ts create mode 100644 x-pack/plugins/security/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx create mode 100644 x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/transform_applications_from_es.ts create mode 100644 x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts diff --git a/x-pack/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx b/x-pack/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx index 9dbb556f5f5f4..68e20ef4caccb 100644 --- a/x-pack/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx +++ b/x-pack/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx @@ -8,6 +8,7 @@ import { EuiButton, EuiCallOut, EuiFieldText, EuiFormRow, EuiPanel, EuiSpacer } import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { ChangeEvent, Component, FormEvent, Fragment, MouseEvent } from 'react'; import { LoginState } from '../../../../../common/login_state'; +import { UnauthorizedLoginForm } from '../unauthorized_login_form'; interface Props { http: any; @@ -19,7 +20,7 @@ interface Props { } interface State { - hasError: boolean; + errorStatusCode: number | null; isLoading: boolean; username: string; password: string; @@ -28,7 +29,7 @@ interface State { class BasicLoginFormUI extends Component { public state = { - hasError: false, + errorStatusCode: null, isLoading: false, username: '', password: '', @@ -36,6 +37,9 @@ class BasicLoginFormUI extends Component { }; public render() { + if (this.state.errorStatusCode === 403) { + return ; + } return ( {this.renderMessage()} @@ -192,7 +196,7 @@ class BasicLoginFormUI extends Component { } this.setState({ - hasError: true, + errorStatusCode: statusCode, message, isLoading: false, }); diff --git a/x-pack/plugins/security/public/views/login/components/unauthorized_login_form/index.ts b/x-pack/plugins/security/public/views/login/components/unauthorized_login_form/index.ts new file mode 100644 index 0000000000000..dcbd92150159c --- /dev/null +++ b/x-pack/plugins/security/public/views/login/components/unauthorized_login_form/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { UnauthorizedLoginForm } from './unauthorized_login_form'; diff --git a/x-pack/plugins/security/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx b/x-pack/plugins/security/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx new file mode 100644 index 0000000000000..de9a6d85d6a51 --- /dev/null +++ b/x-pack/plugins/security/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx @@ -0,0 +1,50 @@ +/* + * 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 chrome from 'ui/chrome'; +import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import React from 'react'; +import { LoginState } from '../../../../../common/login_state'; + +interface Props { + window: any; + intl: InjectedIntl; +} + +export const UnauthorizedLoginForm = injectI18n((props: Props) => { + function handleLogoutClick() { + props.window.location = chrome.addBasePath('/logout'); + } + + return ( + + + + + } + body={ +

+ +

+ } + actions={ + + Logout + + } + /> +
+ ); +}); diff --git a/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.ts b/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.ts new file mode 100644 index 0000000000000..c7e3fe7a6597f --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.ts @@ -0,0 +1,30 @@ +/* + * 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 { Legacy } from 'kibana'; +import { RoleKibanaPrivilege } from '../../../common/model'; +import { transformKibanaApplicationsFromEs } from './transform_applications_from_es'; + +export type GetPrivilegesWithRequest = (request: Legacy.Request) => Promise; + +export function getPrivilegesWithRequestFactory( + application: string, + shieldClient: any +): GetPrivilegesWithRequest { + const { callWithRequest } = shieldClient; + + return async function getPrivilegesWithRequest( + request: Legacy.Request + ): Promise { + const userPrivilegesResponse = await callWithRequest(request, 'shield.userPrivileges'); + const { value = [] } = transformKibanaApplicationsFromEs( + application, + userPrivilegesResponse.applications + ); + + return value; + }; +} diff --git a/x-pack/plugins/security/server/lib/authorization/index.ts b/x-pack/plugins/security/server/lib/authorization/index.ts index 32c05dc8a5ebc..a05c898d6a539 100644 --- a/x-pack/plugins/security/server/lib/authorization/index.ts +++ b/x-pack/plugins/security/server/lib/authorization/index.ts @@ -14,3 +14,4 @@ export { PrivilegeSerializer } from './privilege_serializer'; export { registerPrivilegesWithCluster } from './register_privileges_with_cluster'; export { ResourceSerializer } from './resource_serializer'; export { validateFeaturePrivileges } from './validate_feature_privileges'; +export { transformKibanaApplicationsFromEs } from './transform_applications_from_es'; diff --git a/x-pack/plugins/security/server/lib/authorization/service.ts b/x-pack/plugins/security/server/lib/authorization/service.ts index 493bbe1ccf387..bb56a9c82aeb6 100644 --- a/x-pack/plugins/security/server/lib/authorization/service.ts +++ b/x-pack/plugins/security/server/lib/authorization/service.ts @@ -19,12 +19,17 @@ import { } from './check_privileges_dynamically'; import { AuthorizationMode, authorizationModeFactory } from './mode'; import { privilegesFactory, PrivilegesService } from './privileges'; +import { + GetPrivilegesWithRequest, + getPrivilegesWithRequestFactory, +} from './get_privileges_with_request'; export interface AuthorizationService { actions: Actions; application: string; checkPrivilegesWithRequest: CheckPrivilegesWithRequest; checkPrivilegesDynamicallyWithRequest: CheckPrivilegesDynamicallyWithRequest; + getPrivilegesWithRequest: GetPrivilegesWithRequest; mode: AuthorizationMode; privileges: PrivilegesService; } @@ -49,6 +54,7 @@ export function createAuthorizationService( checkPrivilegesWithRequest, spaces ); + const getPrivilegesWithRequest = getPrivilegesWithRequestFactory(application, shieldClient); const mode = authorizationModeFactory(securityXPackFeature); const privileges = privilegesFactory(actions, xpackMainPlugin); @@ -57,6 +63,7 @@ export function createAuthorizationService( application, checkPrivilegesWithRequest, checkPrivilegesDynamicallyWithRequest, + getPrivilegesWithRequest, mode, privileges, }; diff --git a/x-pack/plugins/security/server/lib/authorization/transform_applications_from_es.ts b/x-pack/plugins/security/server/lib/authorization/transform_applications_from_es.ts new file mode 100644 index 0000000000000..098a1dc4ef5fe --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/transform_applications_from_es.ts @@ -0,0 +1,227 @@ +/* + * 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 _ from 'lodash'; +import { RoleKibanaPrivilege } from '../../../common/model'; +import { + RESERVED_PRIVILEGES_APPLICATION_WILDCARD, + GLOBAL_RESOURCE, +} from '../../../common/constants'; +import { PrivilegeSerializer, ResourceSerializer } from '.'; + +interface EsApplication { + application: string; + privileges: string[]; + resources: string[]; +} + +interface TransformResult { + success: boolean; + value?: RoleKibanaPrivilege[]; +} + +export const transformKibanaApplicationsFromEs = ( + application: string, + esApplications: EsApplication[] +): TransformResult => { + const roleKibanaApplications = esApplications.filter( + roleApplication => + roleApplication.application === application || + roleApplication.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD + ); + + // if any application entry contains an empty resource, we throw an error + if (roleKibanaApplications.some(entry => entry.resources.length === 0)) { + throw new Error(`ES returned an application entry without resources, can't process this`); + } + + // if there is an entry with the reserved privileges application wildcard + // and there are privileges which aren't reserved, we won't transform these + if ( + roleKibanaApplications.some( + entry => + entry.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD && + !entry.privileges.every(privilege => + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ) + ) + ) { + return { + success: false, + }; + } + + // if space privilege assigned globally, we can't transform these + if ( + roleKibanaApplications.some( + entry => + entry.resources.includes(GLOBAL_RESOURCE) && + entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege) + ) + ) + ) { + return { + success: false, + }; + } + + // if global base or reserved privilege assigned at a space, we can't transform these + if ( + roleKibanaApplications.some( + entry => + !entry.resources.includes(GLOBAL_RESOURCE) && + entry.privileges.some( + privilege => + PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) || + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ) + ) + ) { + return { + success: false, + }; + } + + // if reserved privilege assigned with feature or base privileges, we won't transform these + if ( + roleKibanaApplications.some( + entry => + entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ) && + entry.privileges.some( + privilege => !PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ) + ) + ) { + return { + success: false, + }; + } + + // if base privilege assigned with feature privileges, we won't transform these + if ( + roleKibanaApplications.some( + entry => + entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedFeaturePrivilege(privilege) + ) && + (entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) + ) || + entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege) + )) + ) + ) { + return { + success: false, + }; + } + + // if any application entry contains the '*' resource in addition to another resource, we can't transform these + if ( + roleKibanaApplications.some( + entry => entry.resources.includes(GLOBAL_RESOURCE) && entry.resources.length > 1 + ) + ) { + return { + success: false, + }; + } + + const allResources = _.flatten(roleKibanaApplications.map(entry => entry.resources)); + // if we have improperly formatted resource entries, we can't transform these + if ( + allResources.some( + resource => + resource !== GLOBAL_RESOURCE && !ResourceSerializer.isSerializedSpaceResource(resource) + ) + ) { + return { + success: false, + }; + } + + // if we have resources duplicated in entries, we won't transform these + if (allResources.length !== _.uniq(allResources).length) { + return { + success: false, + }; + } + + return { + success: true, + value: roleKibanaApplications.map(({ resources, privileges }) => { + // if we're dealing with a global entry, which we've ensured above is only possible if it's the only item in the array + if (resources.length === 1 && resources[0] === GLOBAL_RESOURCE) { + const reservedPrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ); + const basePrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) + ); + const featurePrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedFeaturePrivilege(privilege) + ); + + return { + ...(reservedPrivileges.length + ? { + _reserved: reservedPrivileges.map(privilege => + PrivilegeSerializer.deserializeReservedPrivilege(privilege) + ), + } + : {}), + base: basePrivileges.map(privilege => + PrivilegeSerializer.serializeGlobalBasePrivilege(privilege) + ), + feature: featurePrivileges.reduce( + (acc, privilege) => { + const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); + return { + ...acc, + [featurePrivilege.featureId]: _.uniq([ + ...(acc[featurePrivilege.featureId] || []), + featurePrivilege.privilege, + ]), + }; + }, + {} as Record + ), + spaces: ['*'], + }; + } + + const basePrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege) + ); + const featurePrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedFeaturePrivilege(privilege) + ); + return { + base: basePrivileges.map(privilege => + PrivilegeSerializer.deserializeSpaceBasePrivilege(privilege) + ), + feature: featurePrivileges.reduce( + (acc, privilege) => { + const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); + return { + ...acc, + [featurePrivilege.featureId]: _.uniq([ + ...(acc[featurePrivilege.featureId] || []), + featurePrivilege.privilege, + ]), + }; + }, + {} as Record + ), + spaces: resources.map(resource => ResourceSerializer.deserializeSpaceResource(resource)), + }; + }), + }; +}; diff --git a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts new file mode 100644 index 0000000000000..d119d97baaf2e --- /dev/null +++ b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts @@ -0,0 +1,18 @@ +/* + * 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 { Legacy } from 'kibana'; +import _ from 'lodash'; +import { AuthorizationService } from './authorization/service'; + +export const isAuthorizedKibanaUser = async ( + authorizationService: AuthorizationService, + request: Legacy.Request +) => { + const userPrivileges = await authorizationService.getPrivilegesWithRequest(request); + + return userPrivileges.length > 0; +}; diff --git a/x-pack/plugins/security/server/routes/api/public/roles/get.js b/x-pack/plugins/security/server/routes/api/public/roles/get.js index d0594e32ba48c..ccc1649a9c331 100644 --- a/x-pack/plugins/security/server/routes/api/public/roles/get.js +++ b/x-pack/plugins/security/server/routes/api/public/roles/get.js @@ -5,151 +5,12 @@ */ import _ from 'lodash'; import Boom from 'boom'; -import { GLOBAL_RESOURCE, RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../../../../common/constants'; +import { RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../../../../common/constants'; import { wrapError } from '../../../../lib/errors'; -import { PrivilegeSerializer, ResourceSerializer } from '../../../../lib/authorization'; +import { transformKibanaApplicationsFromEs } from '../../../../lib/authorization'; export function initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, application) { - const transformKibanaApplicationsFromEs = (roleApplications) => { - const roleKibanaApplications = roleApplications - .filter( - roleApplication => roleApplication.application === application || - roleApplication.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD - ); - - // if any application entry contains an empty resource, we throw an error - if (roleKibanaApplications.some(entry => entry.resources.length === 0)) { - throw new Error(`ES returned an application entry without resources, can't process this`); - } - - // if there is an entry with the reserved privileges application wildcard - // and there are privileges which aren't reserved, we won't transform these - if (roleKibanaApplications.some(entry => - entry.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD && - !entry.privileges.every(privilege => PrivilegeSerializer.isSerializedReservedPrivilege(privilege))) - ) { - return { - success: false - }; - } - - // if space privilege assigned globally, we can't transform these - if (roleKibanaApplications.some(entry => - entry.resources.includes(GLOBAL_RESOURCE) && - entry.privileges.some(privilege => PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege))) - ) { - return { - success: false - }; - } - - // if global base or reserved privilege assigned at a space, we can't transform these - if (roleKibanaApplications.some(entry => - !entry.resources.includes(GLOBAL_RESOURCE) && - entry.privileges.some(privilege => - PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) || - PrivilegeSerializer.isSerializedReservedPrivilege(privilege) - )) - ) { - return { - success: false - }; - } - - // if reserved privilege assigned with feature or base privileges, we won't transform these - if (roleKibanaApplications.some(entry => - entry.privileges.some(privilege => PrivilegeSerializer.isSerializedReservedPrivilege(privilege)) && - entry.privileges.some(privilege => !PrivilegeSerializer.isSerializedReservedPrivilege(privilege))) - ) { - return { - success: false - }; - } - - // if base privilege assigned with feature privileges, we won't transform these - if (roleKibanaApplications.some(entry => - entry.privileges.some(privilege => PrivilegeSerializer.isSerializedFeaturePrivilege(privilege)) && - ( - entry.privileges.some(privilege => PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege)) || - entry.privileges.some(privilege => PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege)) - ) - )) { - return { - success: false - }; - } - - // if any application entry contains the '*' resource in addition to another resource, we can't transform these - if (roleKibanaApplications.some(entry => entry.resources.includes(GLOBAL_RESOURCE) && entry.resources.length > 1)) { - return { - success: false - }; - } - - const allResources = _.flatten(roleKibanaApplications.map(entry => entry.resources)); - // if we have improperly formatted resource entries, we can't transform these - if (allResources.some(resource => resource !== GLOBAL_RESOURCE && !ResourceSerializer.isSerializedSpaceResource(resource))) { - return { - success: false - }; - } - - // if we have resources duplicated in entries, we won't transform these - if (allResources.length !== _.uniq(allResources).length) { - return { - success: false - }; - } - - return { - success: true, - value: roleKibanaApplications.map(({ resources, privileges }) => { - // if we're dealing with a global entry, which we've ensured above is only possible if it's the only item in the array - if (resources.length === 1 && resources[0] === GLOBAL_RESOURCE) { - const reservedPrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedReservedPrivilege(privilege)); - const basePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege)); - const featurePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedFeaturePrivilege(privilege)); - - return { - ...reservedPrivileges.length ? { - _reserved: reservedPrivileges.map(privilege => PrivilegeSerializer.deserializeReservedPrivilege(privilege)) - } : {}, - base: basePrivileges.map(privilege => PrivilegeSerializer.serializeGlobalBasePrivilege(privilege)), - feature: featurePrivileges.reduce((acc, privilege) => { - const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); - return { - ...acc, - [featurePrivilege.featureId]: _.uniq([ - ...acc[featurePrivilege.featureId] || [], - featurePrivilege.privilege - ]) - }; - }, {}), - spaces: ['*'] - }; - } - - const basePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege)); - const featurePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedFeaturePrivilege(privilege)); - return { - base: basePrivileges.map(privilege => PrivilegeSerializer.deserializeSpaceBasePrivilege(privilege)), - feature: featurePrivileges.reduce((acc, privilege) => { - const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); - return { - ...acc, - [featurePrivilege.featureId]: _.uniq([ - ...acc[featurePrivilege.featureId] || [], - featurePrivilege.privilege - ]) - }; - }, {}), - spaces: resources.map(resource => ResourceSerializer.deserializeSpaceResource(resource)) - }; - }) - }; - }; - const transformUnrecognizedApplicationsFromEs = (roleApplications) => { return _.uniq(roleApplications .filter(roleApplication => diff --git a/x-pack/plugins/security/server/routes/api/v1/authenticate.js b/x-pack/plugins/security/server/routes/api/v1/authenticate.js index ffce2137ddfce..d6bb75c996a85 100644 --- a/x-pack/plugins/security/server/routes/api/v1/authenticate.js +++ b/x-pack/plugins/security/server/routes/api/v1/authenticate.js @@ -7,10 +7,13 @@ import Boom from 'boom'; import Joi from 'joi'; import { wrapError } from '../../../lib/errors'; +import { isAuthorizedKibanaUser } from '../../../lib/is_authorized_kibana_user'; import { canRedirectRequest } from '../../../lib/can_redirect_request'; export function initAuthenticateApi(server) { + const { authorization } = server.plugins.security; + server.route({ method: 'POST', path: '/api/security/v1/login', @@ -37,6 +40,11 @@ export function initAuthenticateApi(server) { throw Boom.unauthorized(authenticationResult.error); } + const isAllowed = await isAuthorizedKibanaUser(authorization, request); + if (!isAllowed) { + throw Boom.forbidden('unauthorized for Kibana'); + } + return h.response(); } catch(err) { throw wrapError(err); diff --git a/x-pack/plugins/spaces/types.d.ts b/x-pack/plugins/spaces/types.d.ts index 98a20203c13c1..94c06bfbdce8b 100644 --- a/x-pack/plugins/spaces/types.d.ts +++ b/x-pack/plugins/spaces/types.d.ts @@ -3,8 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Request } from 'hapi'; +import { Legacy } from 'kibana'; +import { SpacesClient } from './server/lib/spaces_client'; export interface SpacesPlugin { getSpaceId(request: Record): string; + spacesClient: { + getScopedClient(request: Legacy.Request): SpacesClient; + }; } diff --git a/x-pack/server/lib/esjs_shield_plugin.js b/x-pack/server/lib/esjs_shield_plugin.js index 22c2c757db4c3..973a36355e742 100644 --- a/x-pack/server/lib/esjs_shield_plugin.js +++ b/x-pack/server/lib/esjs_shield_plugin.js @@ -442,5 +442,12 @@ fmt: '/_security/user/_has_privileges' } }); + + shield.userPrivileges = ca({ + method: 'GET', + url: { + fmt: '/_security/user/_privileges' + } + }); }; })); From addc3b19bdc26c0d150c31e4244a1eb31792d2d9 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 23 May 2019 16:55:56 -0400 Subject: [PATCH 02/35] semi-functional --- x-pack/plugins/security/index.js | 19 +++++ .../unauthorized_login_form.tsx | 3 +- .../security/public/views/not_found/index.ts | 7 ++ .../public/views/not_found/not_found.html | 1 + .../public/views/not_found/not_found.tsx | 70 +++++++++++++++++++ .../server/lib/is_authorized_kibana_user.ts | 8 +++ .../security/server/lib/on_pre_response.ts | 38 ++++++++++ .../server/routes/api/public/roles/get.js | 2 +- .../server/routes/api/v1/authenticate.js | 7 +- 9 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/security/public/views/not_found/index.ts create mode 100644 x-pack/plugins/security/public/views/not_found/not_found.html create mode 100644 x-pack/plugins/security/public/views/not_found/not_found.tsx create mode 100644 x-pack/plugins/security/server/lib/on_pre_response.ts diff --git a/x-pack/plugins/security/index.js b/x-pack/plugins/security/index.js index 473be7cc0bce4..3cba4094e2e60 100644 --- a/x-pack/plugins/security/index.js +++ b/x-pack/plugins/security/index.js @@ -6,6 +6,7 @@ import { resolve } from 'path'; import { getUserProvider } from './server/lib/get_user'; +import { initOnPreResponseHandler } from './server/lib/on_pre_response'; import { initAuthenticateApi } from './server/routes/api/v1/authenticate'; import { initUsersApi } from './server/routes/api/v1/users'; import { initPublicRolesApi } from './server/routes/api/public/roles'; @@ -33,6 +34,7 @@ import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status import { SecureSavedObjectsClientWrapper } from './server/lib/saved_objects_client/secure_saved_objects_client_wrapper'; import { deepFreeze } from './server/lib/deep_freeze'; import { createOptionalPlugin } from './server/lib/optional_plugin'; +import { isAuthorizedKibanaUser } from './server/lib/is_authorized_kibana_user'; export const security = (kibana) => new kibana.Plugin({ id: 'security', @@ -95,6 +97,11 @@ export const security = (kibana) => new kibana.Plugin({ title: 'Logged out', main: 'plugins/security/views/logged_out', hidden: true + }, { + id: 'not_found', + title: 'Not found', + main: 'plugins/security/views/not_found', + hidden: true }], hacks: [ 'plugins/security/hacks/on_session_timeout', @@ -108,7 +115,17 @@ export const security = (kibana) => new kibana.Plugin({ secureCookies: config.get('xpack.security.secureCookies'), sessionTimeout: config.get('xpack.security.sessionTimeout'), enableSpaceAwarePrivileges: config.get('xpack.spaces.enabled'), + canAccessKibana: true, }; + }, + replaceInjectedVars: async function (injectedVars, request, server) { + if (request.auth && request.auth.credentials) { + return { + ...injectedVars, + canAccessKibana: await isAuthorizedKibanaUser(server.plugins.security.authorization, request), + }; + } + return injectedVars; } }, @@ -194,6 +211,8 @@ export const security = (kibana) => new kibana.Plugin({ getUserProvider(server); + initOnPreResponseHandler(server); + await initAuthenticator(server); initAuthenticateApi(server); initAPIAuthorization(server, authorization); diff --git a/x-pack/plugins/security/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx b/x-pack/plugins/security/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx index de9a6d85d6a51..63697914ec9d1 100644 --- a/x-pack/plugins/security/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx +++ b/x-pack/plugins/security/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx @@ -7,7 +7,6 @@ import chrome from 'ui/chrome'; import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React from 'react'; -import { LoginState } from '../../../../../common/login_state'; interface Props { window: any; @@ -22,7 +21,7 @@ export const UnauthorizedLoginForm = injectI18n((props: Props) => { return ( diff --git a/x-pack/plugins/security/public/views/not_found/not_found.tsx b/x-pack/plugins/security/public/views/not_found/not_found.tsx new file mode 100644 index 0000000000000..f308007f57bb8 --- /dev/null +++ b/x-pack/plugins/security/public/views/not_found/not_found.tsx @@ -0,0 +1,70 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; +import { AuthenticationStatePage } from 'plugins/security/components/authentication_state_page'; +// @ts-ignore +import template from 'plugins/security/views/not_found/not_found.html'; +import React from 'react'; +import { render } from 'react-dom'; +import 'ui/autoload/styles'; +import chrome from 'ui/chrome'; +import { I18nContext } from 'ui/i18n'; + +chrome + .setVisible(false) + .setRootTemplate(template) + .setRootController('logout', ($scope: any, canAccessKibana: boolean) => { + $scope.$$postDigest(() => { + const domNode = document.getElementById('reactNotFoundRoot'); + render( + + + } + > + +

+ +

+

+ +

+
+ + + + + {canAccessKibana && ( + + + + + + )} + + + + + + +
+
, + domNode + ); + }); + }); diff --git a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts index d119d97baaf2e..2a24edae10854 100644 --- a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts +++ b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts @@ -12,6 +12,14 @@ export const isAuthorizedKibanaUser = async ( authorizationService: AuthorizationService, request: Legacy.Request ) => { + const { auth } = request; + if (auth && auth.credentials) { + const { roles = [] } = auth.credentials as Record; + if (roles.includes('superuser')) { + return true; + } + } + const userPrivileges = await authorizationService.getPrivilegesWithRequest(request); return userPrivileges.length > 0; diff --git a/x-pack/plugins/security/server/lib/on_pre_response.ts b/x-pack/plugins/security/server/lib/on_pre_response.ts new file mode 100644 index 0000000000000..c9931f8b577f6 --- /dev/null +++ b/x-pack/plugins/security/server/lib/on_pre_response.ts @@ -0,0 +1,38 @@ +/* + * 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 Boom from 'boom'; +import { Legacy } from 'kibana'; +import { ResponseObject } from 'hapi'; +import { canRedirectRequest } from './can_redirect_request'; + +export function initOnPreResponseHandler(server: Legacy.Server) { + server.ext('onPreResponse', async (request: Legacy.Request, h: Legacy.ResponseToolkit) => { + const statusCode = getStatusCode(request.response); + const isForbidden = statusCode === 403; + const isNotFound = statusCode === 404; + + const canRedirect = canRedirectRequest(request); + + if ((isForbidden || isNotFound) && canRedirect) { + const app = server.getHiddenUiAppById('not_found'); + return (await h.renderAppWithDefaultConfig(app)).takeover(); + } + + return h.continue; + }); + + function getStatusCode(response: ResponseObject | Boom | null): number | null { + if (!response) { + return null; + } + + if (Boom.isBoom(response as any)) { + return (response as Boom).output.statusCode; + } + + return (response as ResponseObject).statusCode; + } +} diff --git a/x-pack/plugins/security/server/routes/api/public/roles/get.js b/x-pack/plugins/security/server/routes/api/public/roles/get.js index ccc1649a9c331..94a8082d57d47 100644 --- a/x-pack/plugins/security/server/routes/api/public/roles/get.js +++ b/x-pack/plugins/security/server/routes/api/public/roles/get.js @@ -21,7 +21,7 @@ export function initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, }; const transformRoleFromEs = (role, name) => { - const kibanaTransformResult = transformKibanaApplicationsFromEs(role.applications); + const kibanaTransformResult = transformKibanaApplicationsFromEs(application, role.applications); return { name, diff --git a/x-pack/plugins/security/server/routes/api/v1/authenticate.js b/x-pack/plugins/security/server/routes/api/v1/authenticate.js index d6bb75c996a85..e4b1131904125 100644 --- a/x-pack/plugins/security/server/routes/api/v1/authenticate.js +++ b/x-pack/plugins/security/server/routes/api/v1/authenticate.js @@ -42,7 +42,12 @@ export function initAuthenticateApi(server) { const isAllowed = await isAuthorizedKibanaUser(authorization, request); if (!isAllowed) { - throw Boom.forbidden('unauthorized for Kibana'); + try { + + await server.plugins.security.deauthenticate(request); + } finally { + throw Boom.forbidden('unauthorized for Kibana'); + } } return h.response(); From 67eb4c8716bee0191a372f155e6217884611a09e Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 24 May 2019 06:37:04 -0400 Subject: [PATCH 03/35] cleanup --- .../security/server/lib/is_authorized_kibana_user.ts | 1 - .../plugins/security/server/routes/api/v1/authenticate.js | 8 ++------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts index 2a24edae10854..dd43378bad951 100644 --- a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts +++ b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts @@ -5,7 +5,6 @@ */ import { Legacy } from 'kibana'; -import _ from 'lodash'; import { AuthorizationService } from './authorization/service'; export const isAuthorizedKibanaUser = async ( diff --git a/x-pack/plugins/security/server/routes/api/v1/authenticate.js b/x-pack/plugins/security/server/routes/api/v1/authenticate.js index f8d14a5814be1..378aad2050004 100644 --- a/x-pack/plugins/security/server/routes/api/v1/authenticate.js +++ b/x-pack/plugins/security/server/routes/api/v1/authenticate.js @@ -42,12 +42,8 @@ export function initAuthenticateApi(server) { const isAllowed = await isAuthorizedKibanaUser(authorization, request); if (!isAllowed) { - try { - - await server.plugins.security.deauthenticate(request); - } finally { - throw Boom.forbidden('unauthorized for Kibana'); - } + request.response.header('set-cookie', null); + throw Boom.forbidden('unauthorized for Kibana'); } return h.response(); From f9ed532219142241c31589d0b04c635c9428958d Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 24 May 2019 09:28:02 -0400 Subject: [PATCH 04/35] dedicated messaging for users who aren't authorized Kibana users --- .../public/views/not_found/not_found.tsx | 63 ++++++++++++++----- .../get_privileges_with_request.ts | 3 +- .../transform_applications_from_es.ts | 8 ++- .../server/lib/is_authorized_kibana_user.ts | 27 +++++--- .../server/routes/api/v1/authenticate.js | 3 +- 5 files changed, 74 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/security/public/views/not_found/not_found.tsx b/x-pack/plugins/security/public/views/not_found/not_found.tsx index f308007f57bb8..1e8ca240d8c0f 100644 --- a/x-pack/plugins/security/public/views/not_found/not_found.tsx +++ b/x-pack/plugins/security/public/views/not_found/not_found.tsx @@ -21,26 +21,14 @@ chrome .setRootController('logout', ($scope: any, canAccessKibana: boolean) => { $scope.$$postDigest(() => { const domNode = document.getElementById('reactNotFoundRoot'); + const { title, message, help } = getMessaging(canAccessKibana); + render( - - } - > + -

- -

-

- -

+

{message}

+

{help}

@@ -68,3 +56,44 @@ chrome ); }); }); + +function getMessaging(canAccessKibana: boolean) { + if (canAccessKibana) { + return { + title: , + message: ( + + ), + help: ( + + ), + }; + } + + return { + title: ( + + ), + message: ( + + ), + help: ( + + ), + }; +} diff --git a/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.ts b/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.ts index c7e3fe7a6597f..6a2deda76695d 100644 --- a/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.ts +++ b/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.ts @@ -22,7 +22,8 @@ export function getPrivilegesWithRequestFactory( const userPrivilegesResponse = await callWithRequest(request, 'shield.userPrivileges'); const { value = [] } = transformKibanaApplicationsFromEs( application, - userPrivilegesResponse.applications + userPrivilegesResponse.applications, + { allowDuplicateResources: true } ); return value; diff --git a/x-pack/plugins/security/server/lib/authorization/transform_applications_from_es.ts b/x-pack/plugins/security/server/lib/authorization/transform_applications_from_es.ts index 098a1dc4ef5fe..670a38dff16c5 100644 --- a/x-pack/plugins/security/server/lib/authorization/transform_applications_from_es.ts +++ b/x-pack/plugins/security/server/lib/authorization/transform_applications_from_es.ts @@ -25,7 +25,8 @@ interface TransformResult { export const transformKibanaApplicationsFromEs = ( application: string, - esApplications: EsApplication[] + esApplications: EsApplication[], + transformOptions: { allowDuplicateResources: boolean } = { allowDuplicateResources: false } ): TransformResult => { const roleKibanaApplications = esApplications.filter( roleApplication => @@ -148,7 +149,10 @@ export const transformKibanaApplicationsFromEs = ( } // if we have resources duplicated in entries, we won't transform these - if (allResources.length !== _.uniq(allResources).length) { + if ( + !transformOptions.allowDuplicateResources && + allResources.length !== _.uniq(allResources).length + ) { return { success: false, }; diff --git a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts index dd43378bad951..6a8cf27bf75d6 100644 --- a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts +++ b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts @@ -6,20 +6,31 @@ import { Legacy } from 'kibana'; import { AuthorizationService } from './authorization/service'; +import { AuthenticatedUser } from '../../common/model'; export const isAuthorizedKibanaUser = async ( authorizationService: AuthorizationService, - request: Legacy.Request + request: Legacy.Request, + user?: AuthenticatedUser ) => { - const { auth } = request; - if (auth && auth.credentials) { - const { roles = [] } = auth.credentials as Record; - if (roles.includes('superuser')) { - return true; - } + const roles = getUserRoles(request, user); + if (roles.includes('superuser')) { + return true; } const userPrivileges = await authorizationService.getPrivilegesWithRequest(request); - return userPrivileges.length > 0; + // Reserved privileges on their own do not grant access to kibana.; rather, they augment existing kibana access. + // Therefore, a user is said to be an authorized Kibana user iff they have at least one privilege that isn't reserved. + return userPrivileges.some(privilege => !privilege._reserved); }; + +function getUserRoles(request: Legacy.Request, user?: AuthenticatedUser) { + if (user) { + return user.roles; + } + if (request.auth && request.auth.credentials) { + return (request.auth.credentials as AuthenticatedUser).roles; + } + return []; +} diff --git a/x-pack/plugins/security/server/routes/api/v1/authenticate.js b/x-pack/plugins/security/server/routes/api/v1/authenticate.js index 378aad2050004..112e18a03b5ef 100644 --- a/x-pack/plugins/security/server/routes/api/v1/authenticate.js +++ b/x-pack/plugins/security/server/routes/api/v1/authenticate.js @@ -40,9 +40,8 @@ export function initAuthenticateApi(server) { throw Boom.unauthorized(authenticationResult.error); } - const isAllowed = await isAuthorizedKibanaUser(authorization, request); + const isAllowed = await isAuthorizedKibanaUser(authorization, request, authenticationResult.user); if (!isAllowed) { - request.response.header('set-cookie', null); throw Boom.forbidden('unauthorized for Kibana'); } From af4ecb8474f0bf4dfad92dd88d386a4565be202e Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 24 May 2019 11:09:20 -0400 Subject: [PATCH 05/35] rename not_found page to unavailable --- x-pack/plugins/security/index.js | 6 ++-- .../public/views/not_found/not_found.html | 1 - .../views/{not_found => unavailable}/index.ts | 0 .../public/views/unavailable/unavailable.html | 1 + .../unavailable.tsx} | 28 ++++++++++++------- .../security/server/lib/on_pre_response.ts | 2 +- 6 files changed, 23 insertions(+), 15 deletions(-) delete mode 100644 x-pack/plugins/security/public/views/not_found/not_found.html rename x-pack/plugins/security/public/views/{not_found => unavailable}/index.ts (100%) create mode 100644 x-pack/plugins/security/public/views/unavailable/unavailable.html rename x-pack/plugins/security/public/views/{not_found/not_found.tsx => unavailable/unavailable.tsx} (75%) diff --git a/x-pack/plugins/security/index.js b/x-pack/plugins/security/index.js index 3b7f7129cde9c..7f1a45063966f 100644 --- a/x-pack/plugins/security/index.js +++ b/x-pack/plugins/security/index.js @@ -111,9 +111,9 @@ export const security = (kibana) => new kibana.Plugin({ main: 'plugins/security/views/logged_out', hidden: true }, { - id: 'not_found', - title: 'Not found', - main: 'plugins/security/views/not_found', + id: 'unavailable', + title: 'Unavailable', + main: 'plugins/security/views/unavailable', hidden: true }], hacks: [ diff --git a/x-pack/plugins/security/public/views/not_found/not_found.html b/x-pack/plugins/security/public/views/not_found/not_found.html deleted file mode 100644 index 784f1efbac5b1..0000000000000 --- a/x-pack/plugins/security/public/views/not_found/not_found.html +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/x-pack/plugins/security/public/views/not_found/index.ts b/x-pack/plugins/security/public/views/unavailable/index.ts similarity index 100% rename from x-pack/plugins/security/public/views/not_found/index.ts rename to x-pack/plugins/security/public/views/unavailable/index.ts diff --git a/x-pack/plugins/security/public/views/unavailable/unavailable.html b/x-pack/plugins/security/public/views/unavailable/unavailable.html new file mode 100644 index 0000000000000..c57120a6a2ea9 --- /dev/null +++ b/x-pack/plugins/security/public/views/unavailable/unavailable.html @@ -0,0 +1 @@ +
diff --git a/x-pack/plugins/security/public/views/not_found/not_found.tsx b/x-pack/plugins/security/public/views/unavailable/unavailable.tsx similarity index 75% rename from x-pack/plugins/security/public/views/not_found/not_found.tsx rename to x-pack/plugins/security/public/views/unavailable/unavailable.tsx index 1e8ca240d8c0f..4862bfc5af071 100644 --- a/x-pack/plugins/security/public/views/not_found/not_found.tsx +++ b/x-pack/plugins/security/public/views/unavailable/unavailable.tsx @@ -8,7 +8,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; import { AuthenticationStatePage } from 'plugins/security/components/authentication_state_page'; // @ts-ignore -import template from 'plugins/security/views/not_found/not_found.html'; +import template from 'plugins/security/views/unavailable/unavailable.html'; import React from 'react'; import { render } from 'react-dom'; import 'ui/autoload/styles'; @@ -20,7 +20,7 @@ chrome .setRootTemplate(template) .setRootController('logout', ($scope: any, canAccessKibana: boolean) => { $scope.$$postDigest(() => { - const domNode = document.getElementById('reactNotFoundRoot'); + const domNode = document.getElementById('reactUnavailableRoot'); const { title, message, help } = getMessaging(canAccessKibana); render( @@ -38,7 +38,7 @@ chrome @@ -46,7 +46,10 @@ chrome )} - + @@ -60,16 +63,21 @@ chrome function getMessaging(canAccessKibana: boolean) { if (canAccessKibana) { return { - title: , + title: ( + + ), message: ( ), help: ( ), @@ -79,19 +87,19 @@ function getMessaging(canAccessKibana: boolean) { return { title: ( ), message: ( ), help: ( ), diff --git a/x-pack/plugins/security/server/lib/on_pre_response.ts b/x-pack/plugins/security/server/lib/on_pre_response.ts index c9931f8b577f6..ba8f89999bbcb 100644 --- a/x-pack/plugins/security/server/lib/on_pre_response.ts +++ b/x-pack/plugins/security/server/lib/on_pre_response.ts @@ -17,7 +17,7 @@ export function initOnPreResponseHandler(server: Legacy.Server) { const canRedirect = canRedirectRequest(request); if ((isForbidden || isNotFound) && canRedirect) { - const app = server.getHiddenUiAppById('not_found'); + const app = server.getHiddenUiAppById('unavailable'); return (await h.renderAppWithDefaultConfig(app)).takeover(); } From 576da6f6fbfd7f3c0ec2ee7e99fa6f1e6764132c Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 24 May 2019 13:20:55 -0400 Subject: [PATCH 06/35] tests is_authorized_kibana_user --- .../lib/is_authorized_kibana_user.test.ts | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts diff --git a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts new file mode 100644 index 0000000000000..d764ac59a972c --- /dev/null +++ b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts @@ -0,0 +1,104 @@ +/* + * 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 { Legacy } from 'kibana'; +import { AuthorizationService } from './authorization/service'; +import { RoleKibanaPrivilege, AuthenticatedUser } from '../../common/model'; +import { isAuthorizedKibanaUser } from './is_authorized_kibana_user'; + +function buildAuthorizationService(privileges: RoleKibanaPrivilege[] = []) { + return ({ + getPrivilegesWithRequest: jest.fn().mockResolvedValue([...privileges]), + } as unknown) as AuthorizationService; +} + +function buildRequest(roles: string[] = []): Legacy.Request { + const request: Legacy.Request = ({ + auth: { + credentials: { + roles, + }, + }, + } as unknown) as Legacy.Request; + + return request; +} + +function buildUser(roles: string[] = []): AuthenticatedUser { + return { + username: 'test user', + roles, + } as AuthenticatedUser; +} + +describe('isAuthorizedKibanaUser', () => { + it('returns true for superusers', async () => { + const request = buildRequest(['some role', 'superuser']); + const authService = buildAuthorizationService(); + + await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true); + }); + + it('returns false for users with no privileges', async () => { + const request = buildRequest(['some role']); + const authService = buildAuthorizationService(); + + await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(false); + }); + + it('returns false for users with only reserved privileges', async () => { + const request = buildRequest(['some role']); + const authService = buildAuthorizationService([ + { base: [], feature: {}, spaces: [], _reserved: ['foo'] }, + ]); + + await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(false); + }); + + it('returns true for users with a base privilege', async () => { + const request = buildRequest(['some role']); + const authService = buildAuthorizationService([{ base: ['foo'], feature: {}, spaces: [] }]); + + await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true); + }); + + it('returns true for users with a feature privilege', async () => { + const request = buildRequest(['some role']); + const authService = buildAuthorizationService([ + { base: [], feature: { feature1: ['foo'] }, spaces: [] }, + ]); + + await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true); + }); + + it('returns true for users with both reserved and non-reserved privileges', async () => { + const request = buildRequest(['some role']); + const authService = buildAuthorizationService([ + { base: [], feature: { feature1: ['foo'] }, spaces: ['*'] }, + { base: [], feature: {}, spaces: [], _reserved: ['foo'] }, + ]); + + await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true); + }); + + describe('with the optional user argument', () => { + it('returns true for superusers', async () => { + const request = buildRequest(); + const user = buildUser(['some role', 'superuser']); + const authService = buildAuthorizationService(); + + await expect(isAuthorizedKibanaUser(authService, request, user)).resolves.toEqual(true); + }); + + it('returns false for users with no privileges', async () => { + const request = buildRequest(); + const user = buildUser(['some role']); + const authService = buildAuthorizationService(); + + await expect(isAuthorizedKibanaUser(authService, request, user)).resolves.toEqual(false); + }); + }); +}); From 9c6f8e72e8b30903729c32a899d05247e9b78f8f Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 24 May 2019 14:43:06 -0400 Subject: [PATCH 07/35] simplify privileges check - YAGNI --- .../public/views/unavailable/index.ts | 2 +- .../get_privileges_with_request.test.ts | 132 ++++++++++ .../get_privileges_with_request.ts | 22 +- .../server/lib/authorization/index.ts | 1 - .../transform_applications_from_es.ts | 231 ------------------ .../lib/is_authorized_kibana_user.test.ts | 84 ++++++- .../server/lib/is_authorized_kibana_user.ts | 21 +- .../server/routes/api/external/roles/get.js | 145 ++++++++++- 8 files changed, 382 insertions(+), 256 deletions(-) create mode 100644 x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.test.ts delete mode 100644 x-pack/plugins/security/server/lib/authorization/transform_applications_from_es.ts diff --git a/x-pack/plugins/security/public/views/unavailable/index.ts b/x-pack/plugins/security/public/views/unavailable/index.ts index 0b12169629161..eadb1ba6a616a 100644 --- a/x-pack/plugins/security/public/views/unavailable/index.ts +++ b/x-pack/plugins/security/public/views/unavailable/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './not_found'; +import './unavailable'; diff --git a/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.test.ts b/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.test.ts new file mode 100644 index 0000000000000..e8f7f2a3a0bba --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.test.ts @@ -0,0 +1,132 @@ +/* + * 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 { RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../../common/constants'; +import { EsApplication, getPrivilegesWithRequestFactory } from './get_privileges_with_request'; +import { Legacy } from 'kibana'; + +const application = 'kibana-our_application'; + +const createMockShieldClient = (response: any) => { + const mockCallWithRequest = jest.fn(); + + mockCallWithRequest.mockImplementationOnce(async () => response); + + return { + callWithRequest: mockCallWithRequest, + }; +}; + +describe('#getPrivilegesWithRequest', () => { + const getPrivilegesWithRequestTest = ( + description: string, + options: { + esPrivilegesResponse: { applications: EsApplication[] }; + expectedResult?: any; + expectErrorThrown?: any; + } + ) => { + test(description, async () => { + const mockShieldClient = createMockShieldClient(options.esPrivilegesResponse); + + const getPrivilegesWithRequest = getPrivilegesWithRequestFactory( + application, + mockShieldClient + ); + const request = { foo: Symbol() }; + + let actualResult; + let errorThrown = null; + try { + actualResult = await getPrivilegesWithRequest((request as unknown) as Legacy.Request); + } catch (err) { + errorThrown = err; + } + + expect(mockShieldClient.callWithRequest).toHaveBeenCalledWith( + request, + 'shield.userPrivileges' + ); + + if (options.expectedResult) { + expect(errorThrown).toBeNull(); + expect(actualResult).toEqual(options.expectedResult); + } + }); + }; + + getPrivilegesWithRequestTest('returns ES Applications for this kibana instance', { + esPrivilegesResponse: { + applications: [ + { + application, + privileges: ['all', 'read'], + resources: ['*'], + }, + ], + }, + expectedResult: [ + { + application, + privileges: ['all', 'read'], + resources: ['*'], + }, + ], + }); + + getPrivilegesWithRequestTest('inclues ES Applications for the reserved privileges wildcard', { + esPrivilegesResponse: { + applications: [ + { + application, + privileges: ['all', 'read'], + resources: ['*'], + }, + { + application: RESERVED_PRIVILEGES_APPLICATION_WILDCARD, + privileges: ['reserved_foo'], + resources: ['*'], + }, + ], + }, + expectedResult: [ + { + application, + privileges: ['all', 'read'], + resources: ['*'], + }, + { + application: RESERVED_PRIVILEGES_APPLICATION_WILDCARD, + privileges: ['reserved_foo'], + resources: ['*'], + }, + ], + }); + + getPrivilegesWithRequestTest('excludes unknown ES Applications', { + esPrivilegesResponse: { + applications: [ + { + application, + privileges: ['all', 'read'], + resources: ['*'], + }, + { + application: 'kibana-.unknownApp', + privileges: ['reserved_foo'], + resources: ['*'], + }, + ], + }, + expectedResult: [ + { + application, + privileges: ['all', 'read'], + resources: ['*'], + }, + ], + }); +}); diff --git a/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.ts b/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.ts index 6a2deda76695d..ed84bd6b6ebef 100644 --- a/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.ts +++ b/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.ts @@ -5,10 +5,14 @@ */ import { Legacy } from 'kibana'; -import { RoleKibanaPrivilege } from '../../../common/model'; -import { transformKibanaApplicationsFromEs } from './transform_applications_from_es'; +import { RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../../common/constants'; -export type GetPrivilegesWithRequest = (request: Legacy.Request) => Promise; +export type GetPrivilegesWithRequest = (request: Legacy.Request) => Promise; +export interface EsApplication { + application: string; + privileges: string[]; + resources: string[]; +} export function getPrivilegesWithRequestFactory( application: string, @@ -18,14 +22,12 @@ export function getPrivilegesWithRequestFactory( return async function getPrivilegesWithRequest( request: Legacy.Request - ): Promise { + ): Promise { const userPrivilegesResponse = await callWithRequest(request, 'shield.userPrivileges'); - const { value = [] } = transformKibanaApplicationsFromEs( - application, - userPrivilegesResponse.applications, - { allowDuplicateResources: true } + return (userPrivilegesResponse.applications as EsApplication[]).filter( + app => + app.application === application || + app.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD ); - - return value; }; } diff --git a/x-pack/plugins/security/server/lib/authorization/index.ts b/x-pack/plugins/security/server/lib/authorization/index.ts index a05c898d6a539..32c05dc8a5ebc 100644 --- a/x-pack/plugins/security/server/lib/authorization/index.ts +++ b/x-pack/plugins/security/server/lib/authorization/index.ts @@ -14,4 +14,3 @@ export { PrivilegeSerializer } from './privilege_serializer'; export { registerPrivilegesWithCluster } from './register_privileges_with_cluster'; export { ResourceSerializer } from './resource_serializer'; export { validateFeaturePrivileges } from './validate_feature_privileges'; -export { transformKibanaApplicationsFromEs } from './transform_applications_from_es'; diff --git a/x-pack/plugins/security/server/lib/authorization/transform_applications_from_es.ts b/x-pack/plugins/security/server/lib/authorization/transform_applications_from_es.ts deleted file mode 100644 index 670a38dff16c5..0000000000000 --- a/x-pack/plugins/security/server/lib/authorization/transform_applications_from_es.ts +++ /dev/null @@ -1,231 +0,0 @@ -/* - * 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 _ from 'lodash'; -import { RoleKibanaPrivilege } from '../../../common/model'; -import { - RESERVED_PRIVILEGES_APPLICATION_WILDCARD, - GLOBAL_RESOURCE, -} from '../../../common/constants'; -import { PrivilegeSerializer, ResourceSerializer } from '.'; - -interface EsApplication { - application: string; - privileges: string[]; - resources: string[]; -} - -interface TransformResult { - success: boolean; - value?: RoleKibanaPrivilege[]; -} - -export const transformKibanaApplicationsFromEs = ( - application: string, - esApplications: EsApplication[], - transformOptions: { allowDuplicateResources: boolean } = { allowDuplicateResources: false } -): TransformResult => { - const roleKibanaApplications = esApplications.filter( - roleApplication => - roleApplication.application === application || - roleApplication.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD - ); - - // if any application entry contains an empty resource, we throw an error - if (roleKibanaApplications.some(entry => entry.resources.length === 0)) { - throw new Error(`ES returned an application entry without resources, can't process this`); - } - - // if there is an entry with the reserved privileges application wildcard - // and there are privileges which aren't reserved, we won't transform these - if ( - roleKibanaApplications.some( - entry => - entry.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD && - !entry.privileges.every(privilege => - PrivilegeSerializer.isSerializedReservedPrivilege(privilege) - ) - ) - ) { - return { - success: false, - }; - } - - // if space privilege assigned globally, we can't transform these - if ( - roleKibanaApplications.some( - entry => - entry.resources.includes(GLOBAL_RESOURCE) && - entry.privileges.some(privilege => - PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege) - ) - ) - ) { - return { - success: false, - }; - } - - // if global base or reserved privilege assigned at a space, we can't transform these - if ( - roleKibanaApplications.some( - entry => - !entry.resources.includes(GLOBAL_RESOURCE) && - entry.privileges.some( - privilege => - PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) || - PrivilegeSerializer.isSerializedReservedPrivilege(privilege) - ) - ) - ) { - return { - success: false, - }; - } - - // if reserved privilege assigned with feature or base privileges, we won't transform these - if ( - roleKibanaApplications.some( - entry => - entry.privileges.some(privilege => - PrivilegeSerializer.isSerializedReservedPrivilege(privilege) - ) && - entry.privileges.some( - privilege => !PrivilegeSerializer.isSerializedReservedPrivilege(privilege) - ) - ) - ) { - return { - success: false, - }; - } - - // if base privilege assigned with feature privileges, we won't transform these - if ( - roleKibanaApplications.some( - entry => - entry.privileges.some(privilege => - PrivilegeSerializer.isSerializedFeaturePrivilege(privilege) - ) && - (entry.privileges.some(privilege => - PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) - ) || - entry.privileges.some(privilege => - PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege) - )) - ) - ) { - return { - success: false, - }; - } - - // if any application entry contains the '*' resource in addition to another resource, we can't transform these - if ( - roleKibanaApplications.some( - entry => entry.resources.includes(GLOBAL_RESOURCE) && entry.resources.length > 1 - ) - ) { - return { - success: false, - }; - } - - const allResources = _.flatten(roleKibanaApplications.map(entry => entry.resources)); - // if we have improperly formatted resource entries, we can't transform these - if ( - allResources.some( - resource => - resource !== GLOBAL_RESOURCE && !ResourceSerializer.isSerializedSpaceResource(resource) - ) - ) { - return { - success: false, - }; - } - - // if we have resources duplicated in entries, we won't transform these - if ( - !transformOptions.allowDuplicateResources && - allResources.length !== _.uniq(allResources).length - ) { - return { - success: false, - }; - } - - return { - success: true, - value: roleKibanaApplications.map(({ resources, privileges }) => { - // if we're dealing with a global entry, which we've ensured above is only possible if it's the only item in the array - if (resources.length === 1 && resources[0] === GLOBAL_RESOURCE) { - const reservedPrivileges = privileges.filter(privilege => - PrivilegeSerializer.isSerializedReservedPrivilege(privilege) - ); - const basePrivileges = privileges.filter(privilege => - PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) - ); - const featurePrivileges = privileges.filter(privilege => - PrivilegeSerializer.isSerializedFeaturePrivilege(privilege) - ); - - return { - ...(reservedPrivileges.length - ? { - _reserved: reservedPrivileges.map(privilege => - PrivilegeSerializer.deserializeReservedPrivilege(privilege) - ), - } - : {}), - base: basePrivileges.map(privilege => - PrivilegeSerializer.serializeGlobalBasePrivilege(privilege) - ), - feature: featurePrivileges.reduce( - (acc, privilege) => { - const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); - return { - ...acc, - [featurePrivilege.featureId]: _.uniq([ - ...(acc[featurePrivilege.featureId] || []), - featurePrivilege.privilege, - ]), - }; - }, - {} as Record - ), - spaces: ['*'], - }; - } - - const basePrivileges = privileges.filter(privilege => - PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege) - ); - const featurePrivileges = privileges.filter(privilege => - PrivilegeSerializer.isSerializedFeaturePrivilege(privilege) - ); - return { - base: basePrivileges.map(privilege => - PrivilegeSerializer.deserializeSpaceBasePrivilege(privilege) - ), - feature: featurePrivileges.reduce( - (acc, privilege) => { - const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); - return { - ...acc, - [featurePrivilege.featureId]: _.uniq([ - ...(acc[featurePrivilege.featureId] || []), - featurePrivilege.privilege, - ]), - }; - }, - {} as Record - ), - spaces: resources.map(resource => ResourceSerializer.deserializeSpaceResource(resource)), - }; - }), - }; -}; diff --git a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts index d764ac59a972c..b9d07a1286987 100644 --- a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts +++ b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts @@ -6,12 +6,35 @@ import { Legacy } from 'kibana'; import { AuthorizationService } from './authorization/service'; -import { RoleKibanaPrivilege, AuthenticatedUser } from '../../common/model'; +import { AuthenticatedUser } from '../../common/model'; import { isAuthorizedKibanaUser } from './is_authorized_kibana_user'; +import { PrivilegeSerializer } from './authorization'; +import { EsApplication } from './authorization/get_privileges_with_request'; -function buildAuthorizationService(privileges: RoleKibanaPrivilege[] = []) { +function buildAuthorizationService(privileges: EsApplication[] = []) { return ({ + application: 'kibana-.kibana', getPrivilegesWithRequest: jest.fn().mockResolvedValue([...privileges]), + privileges: { + get: () => ({ + global: { + all: ['actions'], + read: ['actions'], + }, + space: { + all: ['actions'], + read: ['actions'], + }, + features: { + feature_1: { + all: ['actions'], + }, + }, + reserved: { + reserved_feature_1: ['actions'], + }, + }), + }, } as unknown) as AuthorizationService; } @@ -34,6 +57,17 @@ function buildUser(roles: string[] = []): AuthenticatedUser { } as AuthenticatedUser; } +function buildSerializedPrivilege(name: string) { + return { + [name]: { + application: 'kibana-.kibana', + actions: [], + name, + metadata: {}, + }, + }; +} + describe('isAuthorizedKibanaUser', () => { it('returns true for superusers', async () => { const request = buildRequest(['some role', 'superuser']); @@ -52,7 +86,11 @@ describe('isAuthorizedKibanaUser', () => { it('returns false for users with only reserved privileges', async () => { const request = buildRequest(['some role']); const authService = buildAuthorizationService([ - { base: [], feature: {}, spaces: [], _reserved: ['foo'] }, + { + application: 'kibana-.kibana', + privileges: [PrivilegeSerializer.serializeReservedPrivilege('foo')], + resources: ['*'], + }, ]); await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(false); @@ -60,7 +98,13 @@ describe('isAuthorizedKibanaUser', () => { it('returns true for users with a base privilege', async () => { const request = buildRequest(['some role']); - const authService = buildAuthorizationService([{ base: ['foo'], feature: {}, spaces: [] }]); + const authService = buildAuthorizationService([ + { + application: 'kibana-.kibana', + privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], + resources: ['*'], + }, + ]); await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true); }); @@ -68,7 +112,11 @@ describe('isAuthorizedKibanaUser', () => { it('returns true for users with a feature privilege', async () => { const request = buildRequest(['some role']); const authService = buildAuthorizationService([ - { base: [], feature: { feature1: ['foo'] }, spaces: [] }, + { + application: 'kibana-.kibana', + privileges: [PrivilegeSerializer.serializeFeaturePrivilege('feature_1', 'all')], + resources: ['*'], + }, ]); await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true); @@ -77,13 +125,35 @@ describe('isAuthorizedKibanaUser', () => { it('returns true for users with both reserved and non-reserved privileges', async () => { const request = buildRequest(['some role']); const authService = buildAuthorizationService([ - { base: [], feature: { feature1: ['foo'] }, spaces: ['*'] }, - { base: [], feature: {}, spaces: [], _reserved: ['foo'] }, + { + application: 'kibana-.kibana', + privileges: [ + PrivilegeSerializer.serializeFeaturePrivilege('feature_1', 'all'), + PrivilegeSerializer.serializeReservedPrivilege('foo'), + ], + resources: ['*'], + }, ]); await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true); }); + it('returns false for users with unknown privileges', async () => { + const request = buildRequest(['some role']); + const authService = buildAuthorizationService([ + { + application: 'kibana-.kibana', + privileges: [ + PrivilegeSerializer.serializeFeaturePrivilege('feature_1', 'unknown'), + PrivilegeSerializer.serializeReservedPrivilege('foo'), + ], + resources: ['*'], + }, + ]); + + await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(false); + }); + describe('with the optional user argument', () => { it('returns true for superusers', async () => { const request = buildRequest(); diff --git a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts index 6a8cf27bf75d6..a3bbd5ce4c930 100644 --- a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts +++ b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts @@ -7,6 +7,9 @@ import { Legacy } from 'kibana'; import { AuthorizationService } from './authorization/service'; import { AuthenticatedUser } from '../../common/model'; +import { RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../common/constants'; +import { serializePrivileges } from './authorization/privileges_serializer'; +import { PrivilegeSerializer } from './authorization'; export const isAuthorizedKibanaUser = async ( authorizationService: AuthorizationService, @@ -18,11 +21,23 @@ export const isAuthorizedKibanaUser = async ( return true; } - const userPrivileges = await authorizationService.getPrivilegesWithRequest(request); + const { application, privileges } = authorizationService; + + const serializedPrivileges = serializePrivileges(application, privileges.get()); // Reserved privileges on their own do not grant access to kibana.; rather, they augment existing kibana access. - // Therefore, a user is said to be an authorized Kibana user iff they have at least one privilege that isn't reserved. - return userPrivileges.some(privilege => !privilege._reserved); + // Therefore, a user is said to be an authorized Kibana user iff they have at least one known privilege that isn't reserved. + const knownUnreservedPrivileges = Object.keys(serializedPrivileges[application]).filter( + knownPriv => !PrivilegeSerializer.isSerializedReservedPrivilege(knownPriv) + ); + + const userPrivileges = await authorizationService.getPrivilegesWithRequest(request); + + return userPrivileges.some( + privilege => + privilege.application !== RESERVED_PRIVILEGES_APPLICATION_WILDCARD && + privilege.privileges.some(priv => knownUnreservedPrivileges.includes(priv)) + ); }; function getUserRoles(request: Legacy.Request, user?: AuthenticatedUser) { diff --git a/x-pack/plugins/security/server/routes/api/external/roles/get.js b/x-pack/plugins/security/server/routes/api/external/roles/get.js index 94a8082d57d47..d0594e32ba48c 100644 --- a/x-pack/plugins/security/server/routes/api/external/roles/get.js +++ b/x-pack/plugins/security/server/routes/api/external/roles/get.js @@ -5,12 +5,151 @@ */ import _ from 'lodash'; import Boom from 'boom'; -import { RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../../../../common/constants'; +import { GLOBAL_RESOURCE, RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../../../../common/constants'; import { wrapError } from '../../../../lib/errors'; -import { transformKibanaApplicationsFromEs } from '../../../../lib/authorization'; +import { PrivilegeSerializer, ResourceSerializer } from '../../../../lib/authorization'; export function initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, application) { + const transformKibanaApplicationsFromEs = (roleApplications) => { + const roleKibanaApplications = roleApplications + .filter( + roleApplication => roleApplication.application === application || + roleApplication.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD + ); + + // if any application entry contains an empty resource, we throw an error + if (roleKibanaApplications.some(entry => entry.resources.length === 0)) { + throw new Error(`ES returned an application entry without resources, can't process this`); + } + + // if there is an entry with the reserved privileges application wildcard + // and there are privileges which aren't reserved, we won't transform these + if (roleKibanaApplications.some(entry => + entry.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD && + !entry.privileges.every(privilege => PrivilegeSerializer.isSerializedReservedPrivilege(privilege))) + ) { + return { + success: false + }; + } + + // if space privilege assigned globally, we can't transform these + if (roleKibanaApplications.some(entry => + entry.resources.includes(GLOBAL_RESOURCE) && + entry.privileges.some(privilege => PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege))) + ) { + return { + success: false + }; + } + + // if global base or reserved privilege assigned at a space, we can't transform these + if (roleKibanaApplications.some(entry => + !entry.resources.includes(GLOBAL_RESOURCE) && + entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) || + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + )) + ) { + return { + success: false + }; + } + + // if reserved privilege assigned with feature or base privileges, we won't transform these + if (roleKibanaApplications.some(entry => + entry.privileges.some(privilege => PrivilegeSerializer.isSerializedReservedPrivilege(privilege)) && + entry.privileges.some(privilege => !PrivilegeSerializer.isSerializedReservedPrivilege(privilege))) + ) { + return { + success: false + }; + } + + // if base privilege assigned with feature privileges, we won't transform these + if (roleKibanaApplications.some(entry => + entry.privileges.some(privilege => PrivilegeSerializer.isSerializedFeaturePrivilege(privilege)) && + ( + entry.privileges.some(privilege => PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege)) || + entry.privileges.some(privilege => PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege)) + ) + )) { + return { + success: false + }; + } + + // if any application entry contains the '*' resource in addition to another resource, we can't transform these + if (roleKibanaApplications.some(entry => entry.resources.includes(GLOBAL_RESOURCE) && entry.resources.length > 1)) { + return { + success: false + }; + } + + const allResources = _.flatten(roleKibanaApplications.map(entry => entry.resources)); + // if we have improperly formatted resource entries, we can't transform these + if (allResources.some(resource => resource !== GLOBAL_RESOURCE && !ResourceSerializer.isSerializedSpaceResource(resource))) { + return { + success: false + }; + } + + // if we have resources duplicated in entries, we won't transform these + if (allResources.length !== _.uniq(allResources).length) { + return { + success: false + }; + } + + return { + success: true, + value: roleKibanaApplications.map(({ resources, privileges }) => { + // if we're dealing with a global entry, which we've ensured above is only possible if it's the only item in the array + if (resources.length === 1 && resources[0] === GLOBAL_RESOURCE) { + const reservedPrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedReservedPrivilege(privilege)); + const basePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege)); + const featurePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedFeaturePrivilege(privilege)); + + return { + ...reservedPrivileges.length ? { + _reserved: reservedPrivileges.map(privilege => PrivilegeSerializer.deserializeReservedPrivilege(privilege)) + } : {}, + base: basePrivileges.map(privilege => PrivilegeSerializer.serializeGlobalBasePrivilege(privilege)), + feature: featurePrivileges.reduce((acc, privilege) => { + const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); + return { + ...acc, + [featurePrivilege.featureId]: _.uniq([ + ...acc[featurePrivilege.featureId] || [], + featurePrivilege.privilege + ]) + }; + }, {}), + spaces: ['*'] + }; + } + + const basePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege)); + const featurePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedFeaturePrivilege(privilege)); + return { + base: basePrivileges.map(privilege => PrivilegeSerializer.deserializeSpaceBasePrivilege(privilege)), + feature: featurePrivileges.reduce((acc, privilege) => { + const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); + return { + ...acc, + [featurePrivilege.featureId]: _.uniq([ + ...acc[featurePrivilege.featureId] || [], + featurePrivilege.privilege + ]) + }; + }, {}), + spaces: resources.map(resource => ResourceSerializer.deserializeSpaceResource(resource)) + }; + }) + }; + }; + const transformUnrecognizedApplicationsFromEs = (roleApplications) => { return _.uniq(roleApplications .filter(roleApplication => @@ -21,7 +160,7 @@ export function initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, }; const transformRoleFromEs = (role, name) => { - const kibanaTransformResult = transformKibanaApplicationsFromEs(application, role.applications); + const kibanaTransformResult = transformKibanaApplicationsFromEs(role.applications); return { name, From 0bd4a5ca22231c34122833c2d5a999cabd02bf8e Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 24 May 2019 15:36:17 -0400 Subject: [PATCH 08/35] move logic and rendering to xpack_main --- x-pack/plugins/security/index.js | 9 -- .../public/views/unavailable/unavailable.tsx | 107 -------------- x-pack/plugins/xpack_main/index.js | 11 ++ x-pack/plugins/xpack_main/public/index.scss | 12 ++ .../xpack_main/public/views/_index.scss | 3 + .../public/views/unavailable/_index.scss | 1 + .../views/unavailable/_unavailable_page.scss | 25 ++++ .../public/views/unavailable/index.ts | 0 .../public/views/unavailable/unavailable.html | 0 .../public/views/unavailable/unavailable.tsx | 130 ++++++++++++++++++ .../lib/__tests__/__fixtures__/request.ts | 46 +++++++ .../lib/__tests__/__fixtures__/server.ts | 53 +++++++ .../server/lib/can_redirect_request.test.ts | 28 ++++ .../server/lib/can_redirect_request.ts | 27 ++++ .../server/lib/on_pre_response.ts | 0 15 files changed, 336 insertions(+), 116 deletions(-) delete mode 100644 x-pack/plugins/security/public/views/unavailable/unavailable.tsx create mode 100644 x-pack/plugins/xpack_main/public/index.scss create mode 100644 x-pack/plugins/xpack_main/public/views/_index.scss create mode 100644 x-pack/plugins/xpack_main/public/views/unavailable/_index.scss create mode 100644 x-pack/plugins/xpack_main/public/views/unavailable/_unavailable_page.scss rename x-pack/plugins/{security => xpack_main}/public/views/unavailable/index.ts (100%) rename x-pack/plugins/{security => xpack_main}/public/views/unavailable/unavailable.html (100%) create mode 100644 x-pack/plugins/xpack_main/public/views/unavailable/unavailable.tsx create mode 100644 x-pack/plugins/xpack_main/server/lib/__tests__/__fixtures__/request.ts create mode 100644 x-pack/plugins/xpack_main/server/lib/__tests__/__fixtures__/server.ts create mode 100644 x-pack/plugins/xpack_main/server/lib/can_redirect_request.test.ts create mode 100644 x-pack/plugins/xpack_main/server/lib/can_redirect_request.ts rename x-pack/plugins/{security => xpack_main}/server/lib/on_pre_response.ts (100%) diff --git a/x-pack/plugins/security/index.js b/x-pack/plugins/security/index.js index 7f1a45063966f..96ed90e13de4c 100644 --- a/x-pack/plugins/security/index.js +++ b/x-pack/plugins/security/index.js @@ -6,7 +6,6 @@ import { resolve } from 'path'; import { getUserProvider } from './server/lib/get_user'; -import { initOnPreResponseHandler } from './server/lib/on_pre_response'; import { initAuthenticateApi } from './server/routes/api/v1/authenticate'; import { initUsersApi } from './server/routes/api/v1/users'; import { initExternalRolesApi } from './server/routes/api/external/roles'; @@ -110,11 +109,6 @@ export const security = (kibana) => new kibana.Plugin({ title: 'Logged out', main: 'plugins/security/views/logged_out', hidden: true - }, { - id: 'unavailable', - title: 'Unavailable', - main: 'plugins/security/views/unavailable', - hidden: true }], hacks: [ 'plugins/security/hacks/on_session_timeout', @@ -128,7 +122,6 @@ export const security = (kibana) => new kibana.Plugin({ secureCookies: config.get('xpack.security.secureCookies'), sessionTimeout: config.get('xpack.security.sessionTimeout'), enableSpaceAwarePrivileges: config.get('xpack.spaces.enabled'), - canAccessKibana: true, }; }, replaceInjectedVars: async function (injectedVars, request, server) { @@ -224,8 +217,6 @@ export const security = (kibana) => new kibana.Plugin({ getUserProvider(server); - initOnPreResponseHandler(server); - await initAuthenticator(server); initAuthenticateApi(server); initAPIAuthorization(server, authorization); diff --git a/x-pack/plugins/security/public/views/unavailable/unavailable.tsx b/x-pack/plugins/security/public/views/unavailable/unavailable.tsx deleted file mode 100644 index 4862bfc5af071..0000000000000 --- a/x-pack/plugins/security/public/views/unavailable/unavailable.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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 { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; -import { AuthenticationStatePage } from 'plugins/security/components/authentication_state_page'; -// @ts-ignore -import template from 'plugins/security/views/unavailable/unavailable.html'; -import React from 'react'; -import { render } from 'react-dom'; -import 'ui/autoload/styles'; -import chrome from 'ui/chrome'; -import { I18nContext } from 'ui/i18n'; - -chrome - .setVisible(false) - .setRootTemplate(template) - .setRootController('logout', ($scope: any, canAccessKibana: boolean) => { - $scope.$$postDigest(() => { - const domNode = document.getElementById('reactUnavailableRoot'); - const { title, message, help } = getMessaging(canAccessKibana); - - render( - - - -

{message}

-

{help}

-
- - - - - {canAccessKibana && ( - - - - - - )} - - - - - - -
-
, - domNode - ); - }); - }); - -function getMessaging(canAccessKibana: boolean) { - if (canAccessKibana) { - return { - title: ( - - ), - message: ( - - ), - help: ( - - ), - }; - } - - return { - title: ( - - ), - message: ( - - ), - help: ( - - ), - }; -} diff --git a/x-pack/plugins/xpack_main/index.js b/x-pack/plugins/xpack_main/index.js index 4f83683102395..4dad380627ec6 100644 --- a/x-pack/plugins/xpack_main/index.js +++ b/x-pack/plugins/xpack_main/index.js @@ -31,6 +31,7 @@ import { i18n } from '@kbn/i18n'; export { callClusterFactory } from './server/lib/call_cluster_factory'; import { registerOssFeatures } from './server/lib/register_oss_features'; +import { initOnPreResponseHandler } from './server/lib/on_pre_response'; /** * Determine if Telemetry is enabled. @@ -71,6 +72,13 @@ export const xpackMain = (kibana) => { uiExports: { managementSections: ['plugins/xpack_main/views/management'], + styleSheetPaths: resolve(__dirname, 'public/index.scss'), + apps: [{ + id: 'unavailable', + title: 'Unavailable', + main: 'plugins/xpack_main/views/unavailable', + hidden: true + }], uiSettingDefaults: { [CONFIG_TELEMETRY]: { name: i18n.translate('xpack.main.telemetry.telemetryConfigTitle', { @@ -107,6 +115,7 @@ export const xpackMain = (kibana) => { telemetryOptedIn: null, activeSpace: null, spacesEnabled: config.get('xpack.spaces.enabled'), + canAccessKibana: true, }; }, hacks: [ @@ -134,6 +143,8 @@ export const xpackMain = (kibana) => { const { types: savedObjectTypes } = server.savedObjects; registerOssFeatures(server.plugins.xpack_main.registerFeature, savedObjectTypes); + initOnPreResponseHandler(server); + // register routes xpackInfoRoute(server); telemetryRoute(server); diff --git a/x-pack/plugins/xpack_main/public/index.scss b/x-pack/plugins/xpack_main/public/index.scss new file mode 100644 index 0000000000000..904adf1b955e8 --- /dev/null +++ b/x-pack/plugins/xpack_main/public/index.scss @@ -0,0 +1,12 @@ +@import 'src/legacy/ui/public/styles/styling_constants'; + +// Prefix all styles with "xpk" to avoid conflicts. +// Examples +// xpkChart +// xpkChart__legend +// xpkChart__legend--small +// xpkChart__legend-isLoading + +// Public views +@import './views/index'; + diff --git a/x-pack/plugins/xpack_main/public/views/_index.scss b/x-pack/plugins/xpack_main/public/views/_index.scss new file mode 100644 index 0000000000000..b0501209a8b6e --- /dev/null +++ b/x-pack/plugins/xpack_main/public/views/_index.scss @@ -0,0 +1,3 @@ +// Unavailable page styles +@import './unavailable/index'; + diff --git a/x-pack/plugins/xpack_main/public/views/unavailable/_index.scss b/x-pack/plugins/xpack_main/public/views/unavailable/_index.scss new file mode 100644 index 0000000000000..d98170852e3e6 --- /dev/null +++ b/x-pack/plugins/xpack_main/public/views/unavailable/_index.scss @@ -0,0 +1 @@ +@import './unavailable_page'; diff --git a/x-pack/plugins/xpack_main/public/views/unavailable/_unavailable_page.scss b/x-pack/plugins/xpack_main/public/views/unavailable/_unavailable_page.scss new file mode 100644 index 0000000000000..c5c8105ac26eb --- /dev/null +++ b/x-pack/plugins/xpack_main/public/views/unavailable/_unavailable_page.scss @@ -0,0 +1,25 @@ +.xpkUnavailablePage { + @include kibanaFullScreenGraphics; +} + +.xpkUnavailablePage__header { + position: relative; + padding: $euiSizeXL; + z-index: 10; +} + +.xpkUnavailablePage__logo { + @include kibanaCircleLogo; + @include euiBottomShadowMedium; + + margin-bottom: $euiSizeXL; +} + +.xpkUnavailablePage__content { + position: relative; + margin: auto; + max-width: 460px; + padding-left: $euiSizeXL; + padding-right: $euiSizeXL; + z-index: 10; +} diff --git a/x-pack/plugins/security/public/views/unavailable/index.ts b/x-pack/plugins/xpack_main/public/views/unavailable/index.ts similarity index 100% rename from x-pack/plugins/security/public/views/unavailable/index.ts rename to x-pack/plugins/xpack_main/public/views/unavailable/index.ts diff --git a/x-pack/plugins/security/public/views/unavailable/unavailable.html b/x-pack/plugins/xpack_main/public/views/unavailable/unavailable.html similarity index 100% rename from x-pack/plugins/security/public/views/unavailable/unavailable.html rename to x-pack/plugins/xpack_main/public/views/unavailable/unavailable.html diff --git a/x-pack/plugins/xpack_main/public/views/unavailable/unavailable.tsx b/x-pack/plugins/xpack_main/public/views/unavailable/unavailable.tsx new file mode 100644 index 0000000000000..9df0f65fb4b80 --- /dev/null +++ b/x-pack/plugins/xpack_main/public/views/unavailable/unavailable.tsx @@ -0,0 +1,130 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, + EuiIcon, + EuiTitle, +} from '@elastic/eui'; +// @ts-ignore +import template from 'plugins/xpack_main/views/unavailable/unavailable.html'; +import React from 'react'; +import { render } from 'react-dom'; +import 'ui/autoload/styles'; +import chrome from 'ui/chrome'; +import { I18nContext } from 'ui/i18n'; + +chrome + .setVisible(false) + .setRootTemplate(template) + .setRootController('logout', ($scope: any, canAccessKibana: boolean) => { + $scope.$$postDigest(() => { + const domNode = document.getElementById('reactUnavailableRoot'); + const { title, message, help } = getMessaging(canAccessKibana); + + const dataTestSubj = `unavailable-${canAccessKibana ? 'notFound' : 'unauthorized'}`; + + render( + +
+
+
+ + + + + +

{title}

+
+ +
+
+
+ +

{message}

+

{help}

+
+ + + + + {canAccessKibana && ( + + + + + + )} + + + + + + +
+
+
, + domNode + ); + }); + }); + +function getMessaging(canAccessKibana: boolean) { + if (canAccessKibana) { + return { + title: ( + + ), + message: ( + + ), + help: ( + + ), + }; + } + + return { + title: ( + + ), + message: ( + + ), + help: ( + + ), + }; +} diff --git a/x-pack/plugins/xpack_main/server/lib/__tests__/__fixtures__/request.ts b/x-pack/plugins/xpack_main/server/lib/__tests__/__fixtures__/request.ts new file mode 100644 index 0000000000000..48bfaec17db95 --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/__tests__/__fixtures__/request.ts @@ -0,0 +1,46 @@ +/* + * 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 { Request } from 'hapi'; +import { stub } from 'sinon'; +import url from 'url'; + +interface RequestFixtureOptions { + headers?: Record; + auth?: string; + params?: Record; + path?: string; + basePath?: string; + search?: string; + payload?: unknown; +} + +export function requestFixture({ + headers = { accept: 'something/html' }, + auth, + params, + path = '/wat', + basePath = '', + search = '', + payload, +}: RequestFixtureOptions = {}) { + const cookieAuth = { clear: stub(), set: stub() }; + return ({ + raw: { req: { headers } }, + auth, + headers, + params, + url: { path, search }, + cookieAuth, + getBasePath: () => basePath, + query: search ? url.parse(search, true /* parseQueryString */).query : {}, + payload, + state: { user: 'these are the contents of the user client cookie' }, + } as any) as Request & { + cookieAuth: typeof cookieAuth; + getBasePath: () => string; + }; +} diff --git a/x-pack/plugins/xpack_main/server/lib/__tests__/__fixtures__/server.ts b/x-pack/plugins/xpack_main/server/lib/__tests__/__fixtures__/server.ts new file mode 100644 index 0000000000000..82f91483dc60d --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/__tests__/__fixtures__/server.ts @@ -0,0 +1,53 @@ +/* + * 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 { stub } from 'sinon'; + +export function serverFixture() { + return { + config: stub(), + register: stub(), + expose: stub(), + log: stub(), + route: stub(), + decorate: stub(), + + info: { + protocol: 'protocol', + }, + + auth: { + strategy: stub(), + test: stub(), + }, + + plugins: { + elasticsearch: { + createCluster: stub(), + }, + + kibana: { + systemApi: { isSystemApiRequest: stub() }, + }, + + security: { + getUser: stub(), + authenticate: stub(), + deauthenticate: stub(), + }, + + xpack_main: { + info: { + isAvailable: stub(), + feature: stub(), + license: { + isOneOf: stub(), + }, + }, + }, + }, + }; +} diff --git a/x-pack/plugins/xpack_main/server/lib/can_redirect_request.test.ts b/x-pack/plugins/xpack_main/server/lib/can_redirect_request.test.ts new file mode 100644 index 0000000000000..b132b39c7ae7e --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/can_redirect_request.test.ts @@ -0,0 +1,28 @@ +/* + * 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 { requestFixture } from './__tests__/__fixtures__/request'; +import { canRedirectRequest } from './can_redirect_request'; + +describe('lib/can_redirect_request', () => { + it('returns true if request does not have either a kbn-version or kbn-xsrf header', () => { + expect(canRedirectRequest(requestFixture())).toBe(true); + }); + + it('returns false if request has a kbn-version header', () => { + const request = requestFixture(); + request.raw.req.headers['kbn-version'] = 'something'; + + expect(canRedirectRequest(request)).toBe(false); + }); + + it('returns false if request has a kbn-xsrf header', () => { + const request = requestFixture(); + request.raw.req.headers['kbn-xsrf'] = 'something'; + + expect(canRedirectRequest(request)).toBe(false); + }); +}); diff --git a/x-pack/plugins/xpack_main/server/lib/can_redirect_request.ts b/x-pack/plugins/xpack_main/server/lib/can_redirect_request.ts new file mode 100644 index 0000000000000..c87d31f8ff0c2 --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/can_redirect_request.ts @@ -0,0 +1,27 @@ +/* + * 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 { Request } from 'hapi'; +import { contains, get, has } from 'lodash'; + +const ROUTE_TAG_API = 'api'; +const KIBANA_XSRF_HEADER = 'kbn-xsrf'; +const KIBANA_VERSION_HEADER = 'kbn-version'; + +/** + * Checks whether we can reply to the request with redirect response. We can do that + * only for non-AJAX and non-API requests. + * @param request HapiJS request instance to check redirection possibility for. + */ +export function canRedirectRequest(request: Request) { + const hasVersionHeader = has(request.raw.req.headers, KIBANA_VERSION_HEADER); + const hasXsrfHeader = has(request.raw.req.headers, KIBANA_XSRF_HEADER); + + const isApiRoute = contains(get(request, 'route.settings.tags'), ROUTE_TAG_API); + const isAjaxRequest = hasVersionHeader || hasXsrfHeader; + + return !isApiRoute && !isAjaxRequest; +} diff --git a/x-pack/plugins/security/server/lib/on_pre_response.ts b/x-pack/plugins/xpack_main/server/lib/on_pre_response.ts similarity index 100% rename from x-pack/plugins/security/server/lib/on_pre_response.ts rename to x-pack/plugins/xpack_main/server/lib/on_pre_response.ts From 61d22357992f54ddf9282959a995a546307e316b Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 24 May 2019 15:36:42 -0400 Subject: [PATCH 09/35] updating functional tests --- test/functional/page_objects/error_page.js | 30 ++++++++----------- .../feature_controls/canvas_security.ts | 20 ++----------- .../canvas/feature_controls/canvas_spaces.ts | 20 ++----------- .../maps/feature_controls/maps_security.ts | 11 ++----- .../apps/maps/feature_controls/maps_spaces.ts | 11 ++----- .../feature_controls/timelion_security.ts | 18 +++++------ .../feature_controls/timelion_spaces.ts | 29 +++--------------- .../functional/page_objects/security_page.js | 13 ++++---- 8 files changed, 42 insertions(+), 110 deletions(-) diff --git a/test/functional/page_objects/error_page.js b/test/functional/page_objects/error_page.js index dd4966ca690fd..a06f92cfc2bd9 100644 --- a/test/functional/page_objects/error_page.js +++ b/test/functional/page_objects/error_page.js @@ -18,29 +18,23 @@ */ import expect from '@kbn/expect'; -export function ErrorPageProvider({ getPageObjects }) { - const PageObjects = getPageObjects(['common']); +export function ErrorPageProvider({ getService }) { + const testSubjects = getService('testSubjects'); class ErrorPage { async expectForbidden() { - const messageText = await PageObjects.common.getBodyText(); - expect(messageText).to.eql( - JSON.stringify({ - statusCode: 403, - error: 'Forbidden', - message: 'Forbidden' - }) - ); + const title = await testSubjects.getVisibleText('unavailable-unauthorized-title'); + const message = await testSubjects.getVisibleText('unavailable-unauthorized-message'); + + expect(title).to.eql('No access to Kibana'); + expect(message).to.eql('Your account does not have access to Kibana.'); } async expectNotFound() { - const messageText = await PageObjects.common.getBodyText(); - expect(messageText).to.eql( - JSON.stringify({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', - }) - ); + const title = await testSubjects.getVisibleText('unavailable-notFound-title'); + const message = await testSubjects.getVisibleText('unavailable-notFound-message'); + + expect(title).to.eql('Not found'); + expect(message).to.eql('Sorry, the requested resource was not found.'); } } diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts index d830d19c3bc4c..3c09649212dbd 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts @@ -10,7 +10,7 @@ import { KibanaFunctionalTestDefaultProviders } from '../../../../types/provider export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { const esArchiver = getService('esArchiver'); const security = getService('security'); - const PageObjects = getPageObjects(['common', 'canvas', 'security', 'spaceSelector']); + const PageObjects = getPageObjects(['common', 'canvas', 'error', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); const globalNav = getService('globalNav'); @@ -226,14 +226,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); - const messageText = await PageObjects.common.getBodyText(); - expect(messageText).to.eql( - JSON.stringify({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', - }) - ); + await PageObjects.error.expectNotFound(); }); it(`create new workpad returns a 404`, async () => { @@ -241,14 +234,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); - const messageText = await PageObjects.common.getBodyText(); - expect(messageText).to.eql( - JSON.stringify({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', - }) - ); + await PageObjects.error.expectNotFound(); }); }); }); diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts index 63959b545dc36..0d56fe94b45a1 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts @@ -11,7 +11,7 @@ import { KibanaFunctionalTestDefaultProviders } from '../../../../types/provider export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { const esArchiver = getService('esArchiver'); const spacesService: SpacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'canvas', 'security', 'spaceSelector']); + const PageObjects = getPageObjects(['common', 'canvas', 'error', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); describe('spaces feature controls', () => { @@ -112,14 +112,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa shouldLoginIfPrompted: false, }); - const messageText = await PageObjects.common.getBodyText(); - expect(messageText).to.eql( - JSON.stringify({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', - }) - ); + await PageObjects.error.expectNotFound(); }); it(`edit workpad returns a 404`, async () => { @@ -132,14 +125,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa shouldLoginIfPrompted: false, } ); - const messageText = await PageObjects.common.getBodyText(); - expect(messageText).to.eql( - JSON.stringify({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', - }) - ); + await PageObjects.error.expectNotFound(); }); }); }); diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts index 65532c0f77861..235b324a8a00a 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts @@ -10,7 +10,7 @@ import { KibanaFunctionalTestDefaultProviders } from '../../../../types/provider export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { const esArchiver = getService('esArchiver'); const security = getService('security'); - const PageObjects = getPageObjects(['common', 'settings', 'security', 'maps']); + const PageObjects = getPageObjects(['common', 'error', 'settings', 'security', 'maps']); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); const globalNav = getService('globalNav'); @@ -204,14 +204,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); - const messageText = await PageObjects.common.getBodyText(); - expect(messageText).to.eql( - JSON.stringify({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', - }) - ); + await PageObjects.error.expectNotFound(); }); }); }); diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts index 6a0f7cb4c2060..db0ee38aef89d 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts @@ -11,7 +11,7 @@ import { KibanaFunctionalTestDefaultProviders } from '../../../../types/provider export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { const esArchiver = getService('esArchiver'); const spacesService: SpacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'maps', 'security']); + const PageObjects = getPageObjects(['common', 'error', 'maps', 'security']); const appsMenu = getService('appsMenu'); describe('spaces feature controls', () => { @@ -87,14 +87,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); - const messageText = await PageObjects.common.getBodyText(); - expect(messageText).to.eql( - JSON.stringify({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', - }) - ); + await PageObjects.error.expectNotFound(); }); }); }); diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts index 77d50037ce98b..530d7462fcdd9 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts @@ -10,7 +10,14 @@ import { KibanaFunctionalTestDefaultProviders } from '../../../../types/provider export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { const esArchiver = getService('esArchiver'); const security = getService('security'); - const PageObjects = getPageObjects(['common', 'timelion', 'header', 'security', 'spaceSelector']); + const PageObjects = getPageObjects([ + 'common', + 'error', + 'timelion', + 'header', + 'security', + 'spaceSelector', + ]); const appsMenu = getService('appsMenu'); const globalNav = getService('globalNav'); @@ -174,14 +181,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); - const messageText = await PageObjects.common.getBodyText(); - expect(messageText).to.eql( - JSON.stringify({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', - }) - ); + await PageObjects.error.expectNotFound(); }); }); }); diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts index 3871a7d5142d7..8cc570a327c6e 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts @@ -11,7 +11,7 @@ import { KibanaFunctionalTestDefaultProviders } from '../../../../types/provider export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { const esArchiver = getService('esArchiver'); const spacesService: SpacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'timelion', 'security', 'spaceSelector']); + const PageObjects = getPageObjects(['common', 'error', 'timelion', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); describe('timelion', () => { @@ -86,14 +86,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa shouldLoginIfPrompted: false, }); - const messageText = await PageObjects.common.getBodyText(); - expect(messageText).to.eql( - JSON.stringify({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', - }) - ); + await PageObjects.error.expectNotFound(); }); it(`edit timelion sheet which doesn't exist returns a 404`, async () => { @@ -103,14 +96,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa shouldLoginIfPrompted: false, }); - const messageText = await PageObjects.common.getBodyText(); - expect(messageText).to.eql( - JSON.stringify({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', - }) - ); + await PageObjects.error.expectNotFound(); }); it(`edit timelion sheet which exists returns a 404`, async () => { @@ -120,14 +106,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa shouldLoginIfPrompted: false, }); - const messageText = await PageObjects.common.getBodyText(); - expect(messageText).to.eql( - JSON.stringify({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', - }) - ); + await PageObjects.error.expectNotFound(); }); }); }); diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.js index 4d2289284b47a..0dc00cb7cab96 100644 --- a/x-pack/test/functional/page_objects/security_page.js +++ b/x-pack/test/functional/page_objects/security_page.js @@ -28,7 +28,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { const expectSpaceSelector = options.expectSpaceSelector || false; const expectSuccess = options.expectSuccess; const expectForbidden = options.expectForbidden || false; - const rawDataTabLocator = 'a[id=rawdata-tab]'; + const expectNotFound = options.expectNotFound || false; await PageObjects.common.navigateToApp('login'); await testSubjects.setValue('loginUsername', username); @@ -40,14 +40,15 @@ export function SecurityPageProvider({ getService, getPageObjects }) { await retry.try(() => testSubjects.find('kibanaSpaceSelector')); log.debug(`Finished login process, landed on space selector. currentUrl = ${await browser.getCurrentUrl()}`); } else if (expectForbidden) { - if (await find.existsByCssSelector(rawDataTabLocator)) { - // Firefox has 3 tabs and requires navigation to see Raw output - await find.clickByCssSelector(rawDataTabLocator); - } await retry.try(async () => { await PageObjects.error.expectForbidden(); }); log.debug(`Finished login process, found forbidden message. currentUrl = ${await browser.getCurrentUrl()}`); + } else if (expectNotFound) { + await retry.try(async () => { + await PageObjects.error.expectNotFound(); + }); + log.debug(`Finished login process, found forbidden message. currentUrl = ${await browser.getCurrentUrl()}`); } else if (expectSuccess) { await find.byCssSelector('[data-test-subj="kibanaChrome"] nav:not(.ng-hide) ', 20000); log.debug(`Finished login process currentUrl = ${await browser.getCurrentUrl()}`); @@ -84,7 +85,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { async login(username, password, options = {}) { await this.loginPage.login(username, password, options); - if (options.expectSpaceSelector || options.expectForbidden) { + if (options.expectSpaceSelector || options.expectForbidden || options.expectNotFound) { return; } From 72d376daaab2ede8c9a30c5c0aebb0ec10db826f Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 24 May 2019 15:46:35 -0400 Subject: [PATCH 10/35] cleanup --- .../check_privileges_dynamically.test.ts | 4 ++-- .../server/lib/authorization/service.test.mocks.ts | 5 +++++ .../security/server/lib/authorization/service.test.ts | 5 +++++ .../server/lib/is_authorized_kibana_user.test.ts | 11 ----------- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts b/x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts index 282fb99ca330a..e68351713db0c 100644 --- a/x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts +++ b/x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts @@ -15,10 +15,10 @@ test(`checkPrivileges.atSpace when spaces is enabled`, async () => { atSpace: jest.fn().mockReturnValue(expectedResult), }; const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockSpaces = { + const mockSpaces = ({ isEnabled: true, getSpaceId: jest.fn().mockReturnValue(spaceId), - } as OptionalPlugin; + } as unknown) as OptionalPlugin; const request = Symbol(); const privilegeOrPrivileges = ['foo', 'bar']; const checkPrivilegesDynamically = checkPrivilegesDynamicallyWithRequestFactory( diff --git a/x-pack/plugins/security/server/lib/authorization/service.test.mocks.ts b/x-pack/plugins/security/server/lib/authorization/service.test.mocks.ts index 4679552f337e6..fcb63ec57ab0a 100644 --- a/x-pack/plugins/security/server/lib/authorization/service.test.mocks.ts +++ b/x-pack/plugins/security/server/lib/authorization/service.test.mocks.ts @@ -14,6 +14,11 @@ jest.mock('./check_privileges_dynamically', () => ({ checkPrivilegesDynamicallyWithRequestFactory: mockCheckPrivilegesDynamicallyWithRequestFactory, })); +export const mockGetPrivilegesWithRequestFactory = jest.fn(); +jest.mock('./get_privileges_with_request', () => ({ + getPrivilegesWithRequestFactory: mockGetPrivilegesWithRequestFactory, +})); + export const mockGetClient = jest.fn(); jest.mock('../../../../../server/lib/get_client_shield', () => ({ getClient: mockGetClient, diff --git a/x-pack/plugins/security/server/lib/authorization/service.test.ts b/x-pack/plugins/security/server/lib/authorization/service.test.ts index e5772e6eb8f6e..0287818ff5e70 100644 --- a/x-pack/plugins/security/server/lib/authorization/service.test.ts +++ b/x-pack/plugins/security/server/lib/authorization/service.test.ts @@ -9,6 +9,7 @@ import { mockAuthorizationModeFactory, mockCheckPrivilegesDynamicallyWithRequestFactory, mockCheckPrivilegesWithRequestFactory, + mockGetPrivilegesWithRequestFactory, mockGetClient, mockPrivilegesFactory, } from './service.test.mocks'; @@ -64,6 +65,9 @@ test(`returns exposed services`, () => { mockAuthorizationModeFactory.mockReturnValue(mockAuthorizationMode); const mockSpaces = Symbol(); + const mockGetPrivilegesWithRequest = Symbol(); + mockGetPrivilegesWithRequestFactory.mockReturnValue(mockGetPrivilegesWithRequest); + const authorization = createAuthorizationService( mockServer as any, mockXpackInfoFeature as any, @@ -91,6 +95,7 @@ test(`returns exposed services`, () => { application, checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, + getPrivilegesWithRequest: mockGetPrivilegesWithRequest, mode: mockAuthorizationMode, privileges: mockPrivilegesService, }); diff --git a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts index b9d07a1286987..c1bb65e11df6a 100644 --- a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts +++ b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts @@ -57,17 +57,6 @@ function buildUser(roles: string[] = []): AuthenticatedUser { } as AuthenticatedUser; } -function buildSerializedPrivilege(name: string) { - return { - [name]: { - application: 'kibana-.kibana', - actions: [], - name, - metadata: {}, - }, - }; -} - describe('isAuthorizedKibanaUser', () => { it('returns true for superusers', async () => { const request = buildRequest(['some role', 'superuser']); From bab5b79fbc8d0caaed1acd84264e19f1764ac16b Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 24 May 2019 16:26:29 -0400 Subject: [PATCH 11/35] fix i18n message ids --- .../public/views/unavailable/unavailable.tsx | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/xpack_main/public/views/unavailable/unavailable.tsx b/x-pack/plugins/xpack_main/public/views/unavailable/unavailable.tsx index 9df0f65fb4b80..9b88320cf0959 100644 --- a/x-pack/plugins/xpack_main/public/views/unavailable/unavailable.tsx +++ b/x-pack/plugins/xpack_main/public/views/unavailable/unavailable.tsx @@ -60,7 +60,7 @@ chrome @@ -68,10 +68,7 @@ chrome )} - + @@ -87,20 +84,17 @@ function getMessaging(canAccessKibana: boolean) { if (canAccessKibana) { return { title: ( - + ), message: ( ), help: ( ), @@ -110,19 +104,19 @@ function getMessaging(canAccessKibana: boolean) { return { title: ( ), message: ( ), help: ( ), From d08f8c8d5b8bb111747862a042c9cda1466d507f Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 24 May 2019 16:43:02 -0400 Subject: [PATCH 12/35] update mock user --- .../security/server/routes/api/v1/__tests__/authenticate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security/server/routes/api/v1/__tests__/authenticate.js b/x-pack/plugins/security/server/routes/api/v1/__tests__/authenticate.js index 49bbdee39b917..c328124eab555 100644 --- a/x-pack/plugins/security/server/routes/api/v1/__tests__/authenticate.js +++ b/x-pack/plugins/security/server/routes/api/v1/__tests__/authenticate.js @@ -117,7 +117,7 @@ describe('Authentication routes', () => { describe('authentication succeeds', () => { it(`returns user data`, async () => { - const user = { username: 'user' }; + const user = { username: 'user', roles: ['superuser'] }; authenticateStub.returns( Promise.resolve(AuthenticationResult.succeeded(user)) ); From 690500ced9004566af1714db6b240b30b7222269 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 24 May 2019 16:46:48 -0400 Subject: [PATCH 13/35] update route matching --- .../xpack_main/server/lib/__tests__/__fixtures__/request.ts | 1 + x-pack/plugins/xpack_main/server/lib/can_redirect_request.ts | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/xpack_main/server/lib/__tests__/__fixtures__/request.ts b/x-pack/plugins/xpack_main/server/lib/__tests__/__fixtures__/request.ts index 48bfaec17db95..6c53d8635de11 100644 --- a/x-pack/plugins/xpack_main/server/lib/__tests__/__fixtures__/request.ts +++ b/x-pack/plugins/xpack_main/server/lib/__tests__/__fixtures__/request.ts @@ -33,6 +33,7 @@ export function requestFixture({ auth, headers, params, + path, url: { path, search }, cookieAuth, getBasePath: () => basePath, diff --git a/x-pack/plugins/xpack_main/server/lib/can_redirect_request.ts b/x-pack/plugins/xpack_main/server/lib/can_redirect_request.ts index c87d31f8ff0c2..c286c90506108 100644 --- a/x-pack/plugins/xpack_main/server/lib/can_redirect_request.ts +++ b/x-pack/plugins/xpack_main/server/lib/can_redirect_request.ts @@ -5,9 +5,8 @@ */ import { Request } from 'hapi'; -import { contains, get, has } from 'lodash'; +import { has } from 'lodash'; -const ROUTE_TAG_API = 'api'; const KIBANA_XSRF_HEADER = 'kbn-xsrf'; const KIBANA_VERSION_HEADER = 'kbn-version'; @@ -20,7 +19,7 @@ export function canRedirectRequest(request: Request) { const hasVersionHeader = has(request.raw.req.headers, KIBANA_VERSION_HEADER); const hasXsrfHeader = has(request.raw.req.headers, KIBANA_XSRF_HEADER); - const isApiRoute = contains(get(request, 'route.settings.tags'), ROUTE_TAG_API); + const isApiRoute = request.path.startsWith('/api'); const isAjaxRequest = hasVersionHeader || hasXsrfHeader; return !isApiRoute && !isAjaxRequest; From 2313680397aeb0a0d973bcbdf7c4ec89f356596f Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 24 May 2019 16:58:08 -0400 Subject: [PATCH 14/35] update expectForbidden assertion --- .../unauthorized_login_form/unauthorized_login_form.tsx | 2 +- x-pack/test/functional/page_objects/security_page.js | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx b/x-pack/plugins/security/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx index 63697914ec9d1..2da600e3c252f 100644 --- a/x-pack/plugins/security/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx +++ b/x-pack/plugins/security/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx @@ -19,7 +19,7 @@ export const UnauthorizedLoginForm = injectI18n((props: Props) => { } return ( - + testSubjects.find('kibanaSpaceSelector')); log.debug(`Finished login process, landed on space selector. currentUrl = ${await browser.getCurrentUrl()}`); } else if (expectForbidden) { - await retry.try(async () => { - await PageObjects.error.expectForbidden(); - }); + await testSubjects.existOrFail('unauthorized-login-form'); log.debug(`Finished login process, found forbidden message. currentUrl = ${await browser.getCurrentUrl()}`); } else if (expectNotFound) { await retry.try(async () => { From 1586ae5a3a9a500e79d7117b31e44bf2448ec02f Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 29 May 2019 09:50:25 -0400 Subject: [PATCH 15/35] attempt to reduce test flakyness --- x-pack/test/functional/config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 6b8dc8a2fc106..a6a520f6d9941 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -196,6 +196,7 @@ export default async function ({ readConfigFile }) { '--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', // server restarts should not invalidate active sessions '--xpack.code.security.enableGitCertCheck=false', // Disable git certificate check '--timelion.ui.enabled=true', + '--uiSettings.overrides.accessibility:disableAnimations=true' ], }, uiSettings: { From b6dfb56369756313fe00475bbe652070a00557d2 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 29 May 2019 11:55:03 -0400 Subject: [PATCH 16/35] handle missing credentials --- .../security/server/lib/is_authorized_kibana_user.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts index a3bbd5ce4c930..09eb45fc23170 100644 --- a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts +++ b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts @@ -41,11 +41,17 @@ export const isAuthorizedKibanaUser = async ( }; function getUserRoles(request: Legacy.Request, user?: AuthenticatedUser) { - if (user) { + if (user && user.roles) { return user.roles; } - if (request.auth && request.auth.credentials) { - return (request.auth.credentials as AuthenticatedUser).roles; + + const authUser: AuthenticatedUser | null = + request.auth && request.auth.credentials + ? (request.auth.credentials as AuthenticatedUser) + : null; + + if (authUser && authUser.roles) { + return authUser.roles; } return []; } From 50433ab6476352746ad362235fbc16182881bd27 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 30 May 2019 07:43:04 -0400 Subject: [PATCH 17/35] guard call to isAuthorizedKibanaUser --- x-pack/plugins/security/index.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security/index.js b/x-pack/plugins/security/index.js index 96ed90e13de4c..6e5a2459396db 100644 --- a/x-pack/plugins/security/index.js +++ b/x-pack/plugins/security/index.js @@ -125,10 +125,13 @@ export const security = (kibana) => new kibana.Plugin({ }; }, replaceInjectedVars: async function (injectedVars, request, server) { - if (request.auth && request.auth.credentials) { + const { security } = server.plugins; + const hasCredentials = request.auth && request.auth.credentials; + + if (hasCredentials && security.authorization.mode.useRbacForRequest(request)) { return { ...injectedVars, - canAccessKibana: await isAuthorizedKibanaUser(server.plugins.security.authorization, request), + canAccessKibana: await isAuthorizedKibanaUser(security.authorization, request), }; } return injectedVars; From 1654c9da52d8339ec25e38d87ab6527483b9cafa Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 7 Jun 2019 16:42:38 -0400 Subject: [PATCH 18/35] don't disable animations twice --- x-pack/test/functional/config.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 103863cea5f41..b81b5097c493d 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -201,11 +201,6 @@ export default async function ({ readConfigFile }) { '--uiSettings.overrides.accessibility:disableAnimations=true' ], }, - uiSettings: { - defaults: { - 'accessibility:disableAnimations': true, - }, - }, // the apps section defines the urls that // `PageObjects.common.navigateTo(appKey)` will use. // Merge urls for your plugin with the urls defined in From 20243cf4766e37fbc7e7063ded95efb353b6b33f Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 7 Jun 2019 17:26:40 -0400 Subject: [PATCH 19/35] Revert "don't disable animations twice" This reverts commit 1654c9da52d8339ec25e38d87ab6527483b9cafa. --- x-pack/test/functional/config.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index b81b5097c493d..103863cea5f41 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -201,6 +201,11 @@ export default async function ({ readConfigFile }) { '--uiSettings.overrides.accessibility:disableAnimations=true' ], }, + uiSettings: { + defaults: { + 'accessibility:disableAnimations': true, + }, + }, // the apps section defines the urls that // `PageObjects.common.navigateTo(appKey)` will use. // Merge urls for your plugin with the urls defined in From 0d34e2b4548b1badac0de058f79e2826de79b334 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 7 Jun 2019 17:27:23 -0400 Subject: [PATCH 20/35] undo forcing disable animations --- x-pack/test/functional/config.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 103863cea5f41..263d1491f8362 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -197,8 +197,7 @@ export default async function ({ readConfigFile }) { '--stats.maximumWaitTimeForAllCollectorsInS=0', '--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', // server restarts should not invalidate active sessions '--xpack.code.security.enableGitCertCheck=false', // Disable git certificate check - '--timelion.ui.enabled=true', - '--uiSettings.overrides.accessibility:disableAnimations=true' + '--timelion.ui.enabled=true' ], }, uiSettings: { From cdd1d0e2aba638b6da163f71a617c7a4980afc46 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 7 Jun 2019 17:27:55 -0400 Subject: [PATCH 21/35] wait for animations to complete before asserting page contents --- test/functional/page_objects/error_page.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/test/functional/page_objects/error_page.js b/test/functional/page_objects/error_page.js index a06f92cfc2bd9..d07d5fadb7af8 100644 --- a/test/functional/page_objects/error_page.js +++ b/test/functional/page_objects/error_page.js @@ -20,21 +20,26 @@ import expect from '@kbn/expect'; export function ErrorPageProvider({ getService }) { const testSubjects = getService('testSubjects'); + const retry = getService('retry'); class ErrorPage { async expectForbidden() { - const title = await testSubjects.getVisibleText('unavailable-unauthorized-title'); - const message = await testSubjects.getVisibleText('unavailable-unauthorized-message'); + await retry.try(async () => { + const title = await testSubjects.getVisibleText('unavailable-unauthorized-title'); + const message = await testSubjects.getVisibleText('unavailable-unauthorized-message'); - expect(title).to.eql('No access to Kibana'); - expect(message).to.eql('Your account does not have access to Kibana.'); + expect(title).to.eql('No access to Kibana'); + expect(message).to.eql('Your account does not have access to Kibana.'); + }); } async expectNotFound() { - const title = await testSubjects.getVisibleText('unavailable-notFound-title'); - const message = await testSubjects.getVisibleText('unavailable-notFound-message'); + await retry.try(async () => { + const title = await testSubjects.getVisibleText('unavailable-notFound-title'); + const message = await testSubjects.getVisibleText('unavailable-notFound-message'); - expect(title).to.eql('Not found'); - expect(message).to.eql('Sorry, the requested resource was not found.'); + expect(title).to.eql('Not found'); + expect(message).to.eql('Sorry, the requested resource was not found.'); + }); } } From 8effcf9a65d89b5db2b841f415575a0cafd221c7 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 10 Jun 2019 07:28:36 -0400 Subject: [PATCH 22/35] consolidate auth logic --- x-pack/plugins/security/index.js | 12 ++++-------- .../security/server/lib/is_authorized_kibana_user.ts | 9 ++++++++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security/index.js b/x-pack/plugins/security/index.js index 6e5a2459396db..142ad76b9967a 100644 --- a/x-pack/plugins/security/index.js +++ b/x-pack/plugins/security/index.js @@ -126,15 +126,11 @@ export const security = (kibana) => new kibana.Plugin({ }, replaceInjectedVars: async function (injectedVars, request, server) { const { security } = server.plugins; - const hasCredentials = request.auth && request.auth.credentials; - if (hasCredentials && security.authorization.mode.useRbacForRequest(request)) { - return { - ...injectedVars, - canAccessKibana: await isAuthorizedKibanaUser(security.authorization, request), - }; - } - return injectedVars; + return { + ...injectedVars, + canAccessKibana: await isAuthorizedKibanaUser(security.authorization, request), + }; } }, diff --git a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts index 09eb45fc23170..fc8216b6a92eb 100644 --- a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts +++ b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts @@ -16,6 +16,13 @@ export const isAuthorizedKibanaUser = async ( request: Legacy.Request, user?: AuthenticatedUser ) => { + const hasCredentials = request.auth && request.auth.credentials; + const useRbac = authorizationService.mode.useRbacForRequest(request); + + if (!hasCredentials || !useRbac) { + return true; + } + const roles = getUserRoles(request, user); if (roles.includes('superuser')) { return true; @@ -25,7 +32,7 @@ export const isAuthorizedKibanaUser = async ( const serializedPrivileges = serializePrivileges(application, privileges.get()); - // Reserved privileges on their own do not grant access to kibana.; rather, they augment existing kibana access. + // Reserved privileges on their own do not grant access to kibana; rather, they augment existing kibana access. // Therefore, a user is said to be an authorized Kibana user iff they have at least one known privilege that isn't reserved. const knownUnreservedPrivileges = Object.keys(serializedPrivileges[application]).filter( knownPriv => !PrivilegeSerializer.isSerializedReservedPrivilege(knownPriv) From 3f845e96154cacb28bd7a5d0150d36a4818d14be Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 10 Jun 2019 16:31:56 -0400 Subject: [PATCH 23/35] update test fixture --- .../security/server/lib/__tests__/__fixtures__/server.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugins/security/server/lib/__tests__/__fixtures__/server.ts b/x-pack/plugins/security/server/lib/__tests__/__fixtures__/server.ts index 82f91483dc60d..b48760a6f55ba 100644 --- a/x-pack/plugins/security/server/lib/__tests__/__fixtures__/server.ts +++ b/x-pack/plugins/security/server/lib/__tests__/__fixtures__/server.ts @@ -37,6 +37,9 @@ export function serverFixture() { getUser: stub(), authenticate: stub(), deauthenticate: stub(), + authorization: { + mode: { useRbacForRequest: () => true }, + }, }, xpack_main: { From 66e68eaf06e694e5259ea3b1224dc1d64fe20402 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 11 Jun 2019 08:05:18 -0400 Subject: [PATCH 24/35] fix mock --- .../security/server/lib/is_authorized_kibana_user.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts index c1bb65e11df6a..e82235b588530 100644 --- a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts +++ b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts @@ -15,6 +15,9 @@ function buildAuthorizationService(privileges: EsApplication[] = []) { return ({ application: 'kibana-.kibana', getPrivilegesWithRequest: jest.fn().mockResolvedValue([...privileges]), + mode: { + useRbacForRequest: jest.fn().mockReturnValue(true), + }, privileges: { get: () => ({ global: { From 0f8b28772ad8535f4ebf151c94a296e82743a675 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 12 Jun 2019 08:33:36 -0400 Subject: [PATCH 25/35] remove optional user parameter --- x-pack/plugins/security/index.js | 5 +- .../lib/is_authorized_kibana_user.test.ts | 51 +++++-------------- .../server/lib/is_authorized_kibana_user.ts | 22 +------- .../server/routes/api/v1/authenticate.js | 3 +- 4 files changed, 20 insertions(+), 61 deletions(-) diff --git a/x-pack/plugins/security/index.js b/x-pack/plugins/security/index.js index 142ad76b9967a..a68611a74ca64 100644 --- a/x-pack/plugins/security/index.js +++ b/x-pack/plugins/security/index.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; import { resolve } from 'path'; import { getUserProvider } from './server/lib/get_user'; import { initAuthenticateApi } from './server/routes/api/v1/authenticate'; @@ -127,9 +128,11 @@ export const security = (kibana) => new kibana.Plugin({ replaceInjectedVars: async function (injectedVars, request, server) { const { security } = server.plugins; + const userRoles = _.get(request, 'auth.credentials.roles', []); + return { ...injectedVars, - canAccessKibana: await isAuthorizedKibanaUser(security.authorization, request), + canAccessKibana: await isAuthorizedKibanaUser(security.authorization, request, userRoles), }; } }, diff --git a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts index e82235b588530..8c85502105010 100644 --- a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts +++ b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts @@ -41,42 +41,33 @@ function buildAuthorizationService(privileges: EsApplication[] = []) { } as unknown) as AuthorizationService; } -function buildRequest(roles: string[] = []): Legacy.Request { +function buildRequest(): Legacy.Request { const request: Legacy.Request = ({ - auth: { - credentials: { - roles, - }, - }, + auth: { credentials: { username: 'foo' } }, } as unknown) as Legacy.Request; return request; } -function buildUser(roles: string[] = []): AuthenticatedUser { - return { - username: 'test user', - roles, - } as AuthenticatedUser; -} - describe('isAuthorizedKibanaUser', () => { it('returns true for superusers', async () => { - const request = buildRequest(['some role', 'superuser']); + const request = buildRequest(); const authService = buildAuthorizationService(); - await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true); + await expect(isAuthorizedKibanaUser(authService, request, ['superuser'])).resolves.toEqual( + true + ); }); it('returns false for users with no privileges', async () => { - const request = buildRequest(['some role']); + const request = buildRequest(); const authService = buildAuthorizationService(); await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(false); }); it('returns false for users with only reserved privileges', async () => { - const request = buildRequest(['some role']); + const request = buildRequest(); const authService = buildAuthorizationService([ { application: 'kibana-.kibana', @@ -89,7 +80,7 @@ describe('isAuthorizedKibanaUser', () => { }); it('returns true for users with a base privilege', async () => { - const request = buildRequest(['some role']); + const request = buildRequest(); const authService = buildAuthorizationService([ { application: 'kibana-.kibana', @@ -102,7 +93,7 @@ describe('isAuthorizedKibanaUser', () => { }); it('returns true for users with a feature privilege', async () => { - const request = buildRequest(['some role']); + const request = buildRequest(); const authService = buildAuthorizationService([ { application: 'kibana-.kibana', @@ -115,7 +106,7 @@ describe('isAuthorizedKibanaUser', () => { }); it('returns true for users with both reserved and non-reserved privileges', async () => { - const request = buildRequest(['some role']); + const request = buildRequest(); const authService = buildAuthorizationService([ { application: 'kibana-.kibana', @@ -131,7 +122,7 @@ describe('isAuthorizedKibanaUser', () => { }); it('returns false for users with unknown privileges', async () => { - const request = buildRequest(['some role']); + const request = buildRequest(); const authService = buildAuthorizationService([ { application: 'kibana-.kibana', @@ -145,22 +136,4 @@ describe('isAuthorizedKibanaUser', () => { await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(false); }); - - describe('with the optional user argument', () => { - it('returns true for superusers', async () => { - const request = buildRequest(); - const user = buildUser(['some role', 'superuser']); - const authService = buildAuthorizationService(); - - await expect(isAuthorizedKibanaUser(authService, request, user)).resolves.toEqual(true); - }); - - it('returns false for users with no privileges', async () => { - const request = buildRequest(); - const user = buildUser(['some role']); - const authService = buildAuthorizationService(); - - await expect(isAuthorizedKibanaUser(authService, request, user)).resolves.toEqual(false); - }); - }); }); diff --git a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts index fc8216b6a92eb..60775805d9705 100644 --- a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts +++ b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts @@ -6,7 +6,6 @@ import { Legacy } from 'kibana'; import { AuthorizationService } from './authorization/service'; -import { AuthenticatedUser } from '../../common/model'; import { RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../common/constants'; import { serializePrivileges } from './authorization/privileges_serializer'; import { PrivilegeSerializer } from './authorization'; @@ -14,7 +13,7 @@ import { PrivilegeSerializer } from './authorization'; export const isAuthorizedKibanaUser = async ( authorizationService: AuthorizationService, request: Legacy.Request, - user?: AuthenticatedUser + userRoles: string[] = [] ) => { const hasCredentials = request.auth && request.auth.credentials; const useRbac = authorizationService.mode.useRbacForRequest(request); @@ -23,8 +22,7 @@ export const isAuthorizedKibanaUser = async ( return true; } - const roles = getUserRoles(request, user); - if (roles.includes('superuser')) { + if (userRoles.includes('superuser')) { return true; } @@ -46,19 +44,3 @@ export const isAuthorizedKibanaUser = async ( privilege.privileges.some(priv => knownUnreservedPrivileges.includes(priv)) ); }; - -function getUserRoles(request: Legacy.Request, user?: AuthenticatedUser) { - if (user && user.roles) { - return user.roles; - } - - const authUser: AuthenticatedUser | null = - request.auth && request.auth.credentials - ? (request.auth.credentials as AuthenticatedUser) - : null; - - if (authUser && authUser.roles) { - return authUser.roles; - } - return []; -} diff --git a/x-pack/plugins/security/server/routes/api/v1/authenticate.js b/x-pack/plugins/security/server/routes/api/v1/authenticate.js index 8782b63dda5c7..a59de3b2bf671 100644 --- a/x-pack/plugins/security/server/routes/api/v1/authenticate.js +++ b/x-pack/plugins/security/server/routes/api/v1/authenticate.js @@ -40,7 +40,8 @@ export function initAuthenticateApi(server) { throw Boom.unauthorized(authenticationResult.error); } - const isAllowed = await isAuthorizedKibanaUser(authorization, request, authenticationResult.user); + const userRoles = authenticationResult.user.roles; + const isAllowed = await isAuthorizedKibanaUser(authorization, request, userRoles); if (!isAllowed) { throw Boom.forbidden('unauthorized for Kibana'); } From e9df3f7709aae748b6f47470011d8f96703f332d Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 13 Jun 2019 09:22:08 -0400 Subject: [PATCH 26/35] remove duplicate import --- x-pack/plugins/security/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/security/index.js b/x-pack/plugins/security/index.js index 479e83121806e..ae4fbb235127c 100644 --- a/x-pack/plugins/security/index.js +++ b/x-pack/plugins/security/index.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import { resolve } from 'path'; import { get, has } from 'lodash'; import { getUserProvider } from './server/lib/get_user'; @@ -143,7 +142,7 @@ export const security = (kibana) => new kibana.Plugin({ replaceInjectedVars: async function (injectedVars, request, server) { const { security } = server.plugins; - const userRoles = _.get(request, 'auth.credentials.roles', []); + const userRoles = get(request, 'auth.credentials.roles', []); return { ...injectedVars, From 456823d24c65f923e376f8235b09552674941e2e Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 13 Jun 2019 09:22:32 -0400 Subject: [PATCH 27/35] simplify check for credentials --- .../security/server/lib/is_authorized_kibana_user.test.ts | 3 +-- .../plugins/security/server/lib/is_authorized_kibana_user.ts | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts index 8c85502105010..c9c7d90c01035 100644 --- a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts +++ b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts @@ -6,7 +6,6 @@ import { Legacy } from 'kibana'; import { AuthorizationService } from './authorization/service'; -import { AuthenticatedUser } from '../../common/model'; import { isAuthorizedKibanaUser } from './is_authorized_kibana_user'; import { PrivilegeSerializer } from './authorization'; import { EsApplication } from './authorization/get_privileges_with_request'; @@ -43,7 +42,7 @@ function buildAuthorizationService(privileges: EsApplication[] = []) { function buildRequest(): Legacy.Request { const request: Legacy.Request = ({ - auth: { credentials: { username: 'foo' } }, + headers: { authorization: 'Basic: somegarbage' }, } as unknown) as Legacy.Request; return request; diff --git a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts index 60775805d9705..a1b6ac66a2262 100644 --- a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts +++ b/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts @@ -15,7 +15,10 @@ export const isAuthorizedKibanaUser = async ( request: Legacy.Request, userRoles: string[] = [] ) => { - const hasCredentials = request.auth && request.auth.credentials; + // While `request.auth.credentials` is the cononical way to check for credentials on Hapi requests, + // it is _not_ populated on the `/api/security/v1/authenticate` route, where the user's session is first established. + // `request.headers.authorization` is present both on the authentiate route, and on subsequent requests by the nature of our authentication provider. + const hasCredentials = request.headers.authorization; const useRbac = authorizationService.mode.useRbacForRequest(request); if (!hasCredentials || !useRbac) { From 640bd1b885ccebc8109b4ab4ef047b12ec17133f Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 13 Jun 2019 11:02:25 -0400 Subject: [PATCH 28/35] extract es application privilege parsing for re-use --- x-pack/plugins/security/index.js | 4 +- .../get_privileges_with_request.test.ts | 3 +- .../get_privileges_with_request.ts | 21 +- .../server/lib/authorization/index.ts | 2 + .../is_authorized_kibana_user.test.ts | 6 +- .../is_authorized_kibana_user.ts | 34 +-- ...nsform_kibana_applications_from_es.test.ts | 271 ++++++++++++++++++ .../transform_kibana_applications_from_es.ts | 209 ++++++++++++++ .../server/lib/authorization/types.ts | 20 ++ .../server/routes/api/external/roles/get.js | 143 +-------- .../server/routes/api/v1/authenticate.js | 2 +- 11 files changed, 544 insertions(+), 171 deletions(-) rename x-pack/plugins/security/server/lib/{ => authorization}/is_authorized_kibana_user.test.ts (95%) rename x-pack/plugins/security/server/lib/{ => authorization}/is_authorized_kibana_user.ts (52%) create mode 100644 x-pack/plugins/security/server/lib/authorization/transform_kibana_applications_from_es.test.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/transform_kibana_applications_from_es.ts diff --git a/x-pack/plugins/security/index.js b/x-pack/plugins/security/index.js index ae4fbb235127c..f8e8e4f4b06ea 100644 --- a/x-pack/plugins/security/index.js +++ b/x-pack/plugins/security/index.js @@ -28,13 +28,13 @@ import { initAPIAuthorization, initAppAuthorization, registerPrivilegesWithCluster, - validateFeaturePrivileges + validateFeaturePrivileges, + isAuthorizedKibanaUser } from './server/lib/authorization'; import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize'; import { SecureSavedObjectsClientWrapper } from './server/lib/saved_objects_client/secure_saved_objects_client_wrapper'; import { deepFreeze } from './server/lib/deep_freeze'; import { createOptionalPlugin } from './server/lib/optional_plugin'; -import { isAuthorizedKibanaUser } from './server/lib/is_authorized_kibana_user'; export const security = (kibana) => new kibana.Plugin({ id: 'security', diff --git a/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.test.ts b/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.test.ts index e8f7f2a3a0bba..14ba8dfde95b2 100644 --- a/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.test.ts +++ b/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.test.ts @@ -5,8 +5,9 @@ */ import { RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../../common/constants'; -import { EsApplication, getPrivilegesWithRequestFactory } from './get_privileges_with_request'; +import { getPrivilegesWithRequestFactory } from './get_privileges_with_request'; import { Legacy } from 'kibana'; +import { EsApplication } from './types'; const application = 'kibana-our_application'; diff --git a/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.ts b/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.ts index ed84bd6b6ebef..6d837f50c72aa 100644 --- a/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.ts +++ b/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.ts @@ -5,14 +5,12 @@ */ import { Legacy } from 'kibana'; -import { RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../../common/constants'; +import { transformKibanaApplicationsFromEs } from './transform_kibana_applications_from_es'; +import { TransformApplicationsFromEsResponse } from './types'; -export type GetPrivilegesWithRequest = (request: Legacy.Request) => Promise; -export interface EsApplication { - application: string; - privileges: string[]; - resources: string[]; -} +export type GetPrivilegesWithRequest = ( + request: Legacy.Request +) => Promise; export function getPrivilegesWithRequestFactory( application: string, @@ -22,12 +20,9 @@ export function getPrivilegesWithRequestFactory( return async function getPrivilegesWithRequest( request: Legacy.Request - ): Promise { + ): Promise { const userPrivilegesResponse = await callWithRequest(request, 'shield.userPrivileges'); - return (userPrivilegesResponse.applications as EsApplication[]).filter( - app => - app.application === application || - app.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD - ); + + return transformKibanaApplicationsFromEs(application, userPrivilegesResponse.applications); }; } diff --git a/x-pack/plugins/security/server/lib/authorization/index.ts b/x-pack/plugins/security/server/lib/authorization/index.ts index 32c05dc8a5ebc..aa3b9d39dc312 100644 --- a/x-pack/plugins/security/server/lib/authorization/index.ts +++ b/x-pack/plugins/security/server/lib/authorization/index.ts @@ -14,3 +14,5 @@ export { PrivilegeSerializer } from './privilege_serializer'; export { registerPrivilegesWithCluster } from './register_privileges_with_cluster'; export { ResourceSerializer } from './resource_serializer'; export { validateFeaturePrivileges } from './validate_feature_privileges'; +export { transformKibanaApplicationsFromEs } from './transform_kibana_applications_from_es'; +export { isAuthorizedKibanaUser } from './is_authorized_kibana_user'; diff --git a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts b/x-pack/plugins/security/server/lib/authorization/is_authorized_kibana_user.test.ts similarity index 95% rename from x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts rename to x-pack/plugins/security/server/lib/authorization/is_authorized_kibana_user.test.ts index c9c7d90c01035..cf73ccce1de08 100644 --- a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.test.ts +++ b/x-pack/plugins/security/server/lib/authorization/is_authorized_kibana_user.test.ts @@ -5,10 +5,10 @@ */ import { Legacy } from 'kibana'; -import { AuthorizationService } from './authorization/service'; +import { AuthorizationService } from './service'; import { isAuthorizedKibanaUser } from './is_authorized_kibana_user'; -import { PrivilegeSerializer } from './authorization'; -import { EsApplication } from './authorization/get_privileges_with_request'; +import { PrivilegeSerializer } from './privilege_serializer'; +import { EsApplication } from './types'; function buildAuthorizationService(privileges: EsApplication[] = []) { return ({ diff --git a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts b/x-pack/plugins/security/server/lib/authorization/is_authorized_kibana_user.ts similarity index 52% rename from x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts rename to x-pack/plugins/security/server/lib/authorization/is_authorized_kibana_user.ts index a1b6ac66a2262..bf9f147357a5d 100644 --- a/x-pack/plugins/security/server/lib/is_authorized_kibana_user.ts +++ b/x-pack/plugins/security/server/lib/authorization/is_authorized_kibana_user.ts @@ -5,10 +5,8 @@ */ import { Legacy } from 'kibana'; -import { AuthorizationService } from './authorization/service'; -import { RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../common/constants'; -import { serializePrivileges } from './authorization/privileges_serializer'; -import { PrivilegeSerializer } from './authorization'; +import { AuthorizationService } from './service'; +import { KibanaApplication } from './types'; export const isAuthorizedKibanaUser = async ( authorizationService: AuthorizationService, @@ -29,21 +27,19 @@ export const isAuthorizedKibanaUser = async ( return true; } - const { application, privileges } = authorizationService; - - const serializedPrivileges = serializePrivileges(application, privileges.get()); - - // Reserved privileges on their own do not grant access to kibana; rather, they augment existing kibana access. - // Therefore, a user is said to be an authorized Kibana user iff they have at least one known privilege that isn't reserved. - const knownUnreservedPrivileges = Object.keys(serializedPrivileges[application]).filter( - knownPriv => !PrivilegeSerializer.isSerializedReservedPrivilege(knownPriv) - ); - const userPrivileges = await authorizationService.getPrivilegesWithRequest(request); + if (!userPrivileges.success) { + // TODO: what to do? + return true; + } + + const userKibanaPrivileges = userPrivileges.value as KibanaApplication[]; - return userPrivileges.some( - privilege => - privilege.application !== RESERVED_PRIVILEGES_APPLICATION_WILDCARD && - privilege.privileges.some(priv => knownUnreservedPrivileges.includes(priv)) - ); + return userKibanaPrivileges.some(privilege => { + const hasBasePrivileges = privilege.base.length > 0; + const hasFeaturePrivileges = Object.values(privilege.feature).some( + featurePrivileges => featurePrivileges.length > 0 + ); + return hasBasePrivileges || hasFeaturePrivileges; + }); }; diff --git a/x-pack/plugins/security/server/lib/authorization/transform_kibana_applications_from_es.test.ts b/x-pack/plugins/security/server/lib/authorization/transform_kibana_applications_from_es.test.ts new file mode 100644 index 0000000000000..017ca69d9662f --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/transform_kibana_applications_from_es.test.ts @@ -0,0 +1,271 @@ +/* + * 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 { EsApplication } from './types'; +import { PrivilegeSerializer } from './privilege_serializer'; +import { ResourceSerializer } from './resource_serializer'; +import { transformKibanaApplicationsFromEs } from './transform_kibana_applications_from_es'; +import { RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../../common/constants'; + +const application = 'kibana-.kibana'; + +describe('transformKibanaApplicationsFromEs', () => { + it('throws for empty resources', () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], + resources: [], + }, + ]; + + expect(() => + transformKibanaApplicationsFromEs(application, esApplications) + ).toThrowErrorMatchingInlineSnapshot( + `"ES returned an application entry without resources, can't process this"` + ); + }); + + it('does not transform unknown applications', () => { + const esApplications: EsApplication[] = [ + { + application: 'kibana-.unknown', + privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], + resources: [ResourceSerializer.serializeSpaceResource('my-space')], + }, + { + application: 'other-.unknown', + privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], + resources: [ResourceSerializer.serializeSpaceResource('my-space')], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: true, + value: [], + }); + }); + + it('transforms well-formed entries', () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], + resources: ['*'], + }, + { + application, + privileges: [ + PrivilegeSerializer.serializeFeaturePrivilege('feature1', 'all'), + PrivilegeSerializer.serializeFeaturePrivilege('feature2', 'read'), + ], + resources: [ResourceSerializer.serializeSpaceResource('my-space')], + }, + { + application, + privileges: [PrivilegeSerializer.serializeSpaceBasePrivilege('all')], + resources: [ResourceSerializer.serializeSpaceResource('my-space')], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: true, + value: [ + { + base: ['all'], + feature: {}, + spaces: ['*'], + }, + { + base: [], + feature: { + feature1: ['all'], + feature2: ['read'], + }, + spaces: ['my-space'], + }, + { + base: ['all'], + feature: {}, + spaces: ['my-space'], + }, + ], + }); + }); + + it(`returns 'success: false' when global base privileges are assigned at a space`, () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], + resources: [ResourceSerializer.serializeSpaceResource('my-space')], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + + it(`returns 'success: false' when reserved privileges are assigned at a space`, () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [PrivilegeSerializer.serializeReservedPrivilege('all')], + resources: [ResourceSerializer.serializeSpaceResource('my-space')], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + + it(`returns 'success: false' when reserved privileges are assigned with other privileges`, () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [ + PrivilegeSerializer.serializeReservedPrivilege('all'), + PrivilegeSerializer.serializeGlobalBasePrivilege('all'), + ], + resources: ['*'], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + + it(`returns 'success: false' when both base and feature privileges are assigned globally in the same application entry`, () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [ + PrivilegeSerializer.serializeGlobalBasePrivilege('all'), + PrivilegeSerializer.serializeFeaturePrivilege('feature-1', 'all'), + ], + resources: ['*'], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + + it(`returns 'success: false' when both base and feature privileges are assigned at a space in the same application entry`, () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [ + PrivilegeSerializer.serializeSpaceBasePrivilege('all'), + PrivilegeSerializer.serializeFeaturePrivilege('feature-1', 'all'), + ], + resources: [ResourceSerializer.serializeSpaceResource('my-space')], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + + it(`returns 'success: false' when space base privileges are assigned globally`, () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [PrivilegeSerializer.serializeSpaceBasePrivilege('all')], + resources: ['*'], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + + it(`returns 'success: false' when privileges are assigned at both the global and other resources`, () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [PrivilegeSerializer.serializeFeaturePrivilege('feature-1', 'all')], + resources: ['*', ResourceSerializer.serializeSpaceResource('my-space')], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + + it(`returns 'success: false' for malformed resources`, () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [PrivilegeSerializer.serializeFeaturePrivilege('feature-1', 'all')], + resources: ['i-dont-know-what-i-am'], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + + describe('reserved privileges application wildcard', () => { + it('transforms reserved privileges', () => { + const esApplications: EsApplication[] = [ + { + application: RESERVED_PRIVILEGES_APPLICATION_WILDCARD, + privileges: [PrivilegeSerializer.serializeReservedPrivilege('all')], + resources: ['*'], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: true, + value: [ + { + base: [], + feature: {}, + _reserved: ['all'], + spaces: ['*'], + }, + ], + }); + }); + + it(`returns 'success: false' for non-reserved privileges`, () => { + const esApplications: EsApplication[] = [ + { + application: RESERVED_PRIVILEGES_APPLICATION_WILDCARD, + privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], + resources: ['*'], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + + it(`returns 'success: false' for reserved privileges at a specific resource`, () => { + const esApplications: EsApplication[] = [ + { + application: RESERVED_PRIVILEGES_APPLICATION_WILDCARD, + privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], + resources: [ResourceSerializer.serializeSpaceResource('some-space')], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + }); +}); diff --git a/x-pack/plugins/security/server/lib/authorization/transform_kibana_applications_from_es.ts b/x-pack/plugins/security/server/lib/authorization/transform_kibana_applications_from_es.ts new file mode 100644 index 0000000000000..a8419543a8780 --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/transform_kibana_applications_from_es.ts @@ -0,0 +1,209 @@ +/* + * 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 _ from 'lodash'; +import { + RESERVED_PRIVILEGES_APPLICATION_WILDCARD, + GLOBAL_RESOURCE, +} from '../../../common/constants'; +import { PrivilegeSerializer, ResourceSerializer } from '.'; +import { EsApplication, TransformApplicationsFromEsResponse } from './types'; + +export const transformKibanaApplicationsFromEs = ( + application: string, + esApplications: EsApplication[] +): TransformApplicationsFromEsResponse => { + const kibanaApplications = esApplications.filter( + roleApplication => + roleApplication.application === application || + roleApplication.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD + ); + + // if any application entry contains an empty resource, we throw an error + if (kibanaApplications.some(entry => entry.resources.length === 0)) { + throw new Error(`ES returned an application entry without resources, can't process this`); + } + + // if there is an entry with the reserved privileges application wildcard + // and there are privileges which aren't reserved, we won't transform these + if ( + kibanaApplications.some( + entry => + entry.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD && + !entry.privileges.every(privilege => + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ) + ) + ) { + return { + success: false, + }; + } + + // if space privilege assigned globally, we can't transform these + if ( + kibanaApplications.some( + entry => + entry.resources.includes(GLOBAL_RESOURCE) && + entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege) + ) + ) + ) { + return { + success: false, + }; + } + + // if global base or reserved privilege assigned at a space, we can't transform these + if ( + kibanaApplications.some( + entry => + !entry.resources.includes(GLOBAL_RESOURCE) && + entry.privileges.some( + privilege => + PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) || + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ) + ) + ) { + return { + success: false, + }; + } + + // if reserved privilege assigned with feature or base privileges, we won't transform these + if ( + kibanaApplications.some( + entry => + entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ) && + entry.privileges.some( + privilege => !PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ) + ) + ) { + return { + success: false, + }; + } + + // if base privilege assigned with feature privileges, we won't transform these + if ( + kibanaApplications.some( + entry => + entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedFeaturePrivilege(privilege) + ) && + (entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) + ) || + entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege) + )) + ) + ) { + return { + success: false, + }; + } + + // if any application entry contains the '*' resource in addition to another resource, we can't transform these + if ( + kibanaApplications.some( + entry => entry.resources.includes(GLOBAL_RESOURCE) && entry.resources.length > 1 + ) + ) { + return { + success: false, + }; + } + + const allResources = _.flatten(kibanaApplications.map(entry => entry.resources)); + // if we have improperly formatted resource entries, we can't transform these + if ( + allResources.some( + resource => + resource !== GLOBAL_RESOURCE && !ResourceSerializer.isSerializedSpaceResource(resource) + ) + ) { + return { + success: false, + }; + } + + return { + success: true, + value: kibanaApplications.map(({ resources, privileges }) => { + // if we're dealing with a global entry, which we've ensured above is only possible if it's the only item in the array + if (resources.length === 1 && resources[0] === GLOBAL_RESOURCE) { + const reservedPrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ); + const basePrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) + ); + const featurePrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedFeaturePrivilege(privilege) + ); + + return { + ...(reservedPrivileges.length + ? { + _reserved: reservedPrivileges.map(privilege => + PrivilegeSerializer.deserializeReservedPrivilege(privilege) + ), + } + : {}), + base: basePrivileges.map(privilege => + PrivilegeSerializer.serializeGlobalBasePrivilege(privilege) + ), + feature: featurePrivileges.reduce( + (acc, privilege) => { + const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); + return { + ...acc, + [featurePrivilege.featureId]: _.uniq([ + ...(acc[featurePrivilege.featureId] || []), + featurePrivilege.privilege, + ]), + }; + }, + {} as Record + ), + spaces: ['*'], + }; + } + + const basePrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege) + ); + const featurePrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedFeaturePrivilege(privilege) + ); + return { + base: basePrivileges.map(privilege => + PrivilegeSerializer.deserializeSpaceBasePrivilege(privilege) + ), + feature: featurePrivileges.reduce( + (acc, privilege) => { + const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); + return { + ...acc, + [featurePrivilege.featureId]: _.uniq([ + ...(acc[featurePrivilege.featureId] || []), + featurePrivilege.privilege, + ]), + }; + }, + {} as Record + ), + spaces: resources.map(resource => ResourceSerializer.deserializeSpaceResource(resource)), + }; + }), + }; +}; diff --git a/x-pack/plugins/security/server/lib/authorization/types.ts b/x-pack/plugins/security/server/lib/authorization/types.ts index 75188d1191b1a..3bf7639238ac3 100644 --- a/x-pack/plugins/security/server/lib/authorization/types.ts +++ b/x-pack/plugins/security/server/lib/authorization/types.ts @@ -17,3 +17,23 @@ export interface HasPrivilegesResponse { [applicationName: string]: HasPrivilegesResponseApplication; }; } + +export interface EsApplication { + application: string; + privileges: string[]; + resources: string[]; +} + +export interface KibanaApplication { + base: string[]; + feature: { + [featureId: string]: string[]; + }; + _reserved?: string[]; + spaces: string[]; +} + +export interface TransformApplicationsFromEsResponse { + success: boolean; + value?: KibanaApplication[]; +} diff --git a/x-pack/plugins/security/server/routes/api/external/roles/get.js b/x-pack/plugins/security/server/routes/api/external/roles/get.js index d0594e32ba48c..dc3cf64333bc2 100644 --- a/x-pack/plugins/security/server/routes/api/external/roles/get.js +++ b/x-pack/plugins/security/server/routes/api/external/roles/get.js @@ -5,149 +5,28 @@ */ import _ from 'lodash'; import Boom from 'boom'; -import { GLOBAL_RESOURCE, RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../../../../common/constants'; +import { RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../../../../common/constants'; import { wrapError } from '../../../../lib/errors'; -import { PrivilegeSerializer, ResourceSerializer } from '../../../../lib/authorization'; +import { transformKibanaApplicationsFromEs } from '../../../../lib/authorization'; export function initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, application) { - const transformKibanaApplicationsFromEs = (roleApplications) => { - const roleKibanaApplications = roleApplications - .filter( - roleApplication => roleApplication.application === application || - roleApplication.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD - ); + const transformRoleApplicationsFromEs = (roleApplications) => { + const { success, value } = transformKibanaApplicationsFromEs(application, roleApplications); - // if any application entry contains an empty resource, we throw an error - if (roleKibanaApplications.some(entry => entry.resources.length === 0)) { - throw new Error(`ES returned an application entry without resources, can't process this`); + if (!success) { + return { success, value }; } - // if there is an entry with the reserved privileges application wildcard - // and there are privileges which aren't reserved, we won't transform these - if (roleKibanaApplications.some(entry => - entry.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD && - !entry.privileges.every(privilege => PrivilegeSerializer.isSerializedReservedPrivilege(privilege))) - ) { + const allSpaces = _.flatten(value.map(entry => entry.spaces)); + // if we have spaces duplicated in entries, we won't transform these + if (allSpaces.length !== _.uniq(allSpaces).length) { return { success: false }; } - // if space privilege assigned globally, we can't transform these - if (roleKibanaApplications.some(entry => - entry.resources.includes(GLOBAL_RESOURCE) && - entry.privileges.some(privilege => PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege))) - ) { - return { - success: false - }; - } - - // if global base or reserved privilege assigned at a space, we can't transform these - if (roleKibanaApplications.some(entry => - !entry.resources.includes(GLOBAL_RESOURCE) && - entry.privileges.some(privilege => - PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) || - PrivilegeSerializer.isSerializedReservedPrivilege(privilege) - )) - ) { - return { - success: false - }; - } - - // if reserved privilege assigned with feature or base privileges, we won't transform these - if (roleKibanaApplications.some(entry => - entry.privileges.some(privilege => PrivilegeSerializer.isSerializedReservedPrivilege(privilege)) && - entry.privileges.some(privilege => !PrivilegeSerializer.isSerializedReservedPrivilege(privilege))) - ) { - return { - success: false - }; - } - - // if base privilege assigned with feature privileges, we won't transform these - if (roleKibanaApplications.some(entry => - entry.privileges.some(privilege => PrivilegeSerializer.isSerializedFeaturePrivilege(privilege)) && - ( - entry.privileges.some(privilege => PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege)) || - entry.privileges.some(privilege => PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege)) - ) - )) { - return { - success: false - }; - } - - // if any application entry contains the '*' resource in addition to another resource, we can't transform these - if (roleKibanaApplications.some(entry => entry.resources.includes(GLOBAL_RESOURCE) && entry.resources.length > 1)) { - return { - success: false - }; - } - - const allResources = _.flatten(roleKibanaApplications.map(entry => entry.resources)); - // if we have improperly formatted resource entries, we can't transform these - if (allResources.some(resource => resource !== GLOBAL_RESOURCE && !ResourceSerializer.isSerializedSpaceResource(resource))) { - return { - success: false - }; - } - - // if we have resources duplicated in entries, we won't transform these - if (allResources.length !== _.uniq(allResources).length) { - return { - success: false - }; - } - - return { - success: true, - value: roleKibanaApplications.map(({ resources, privileges }) => { - // if we're dealing with a global entry, which we've ensured above is only possible if it's the only item in the array - if (resources.length === 1 && resources[0] === GLOBAL_RESOURCE) { - const reservedPrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedReservedPrivilege(privilege)); - const basePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege)); - const featurePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedFeaturePrivilege(privilege)); - - return { - ...reservedPrivileges.length ? { - _reserved: reservedPrivileges.map(privilege => PrivilegeSerializer.deserializeReservedPrivilege(privilege)) - } : {}, - base: basePrivileges.map(privilege => PrivilegeSerializer.serializeGlobalBasePrivilege(privilege)), - feature: featurePrivileges.reduce((acc, privilege) => { - const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); - return { - ...acc, - [featurePrivilege.featureId]: _.uniq([ - ...acc[featurePrivilege.featureId] || [], - featurePrivilege.privilege - ]) - }; - }, {}), - spaces: ['*'] - }; - } - - const basePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege)); - const featurePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedFeaturePrivilege(privilege)); - return { - base: basePrivileges.map(privilege => PrivilegeSerializer.deserializeSpaceBasePrivilege(privilege)), - feature: featurePrivileges.reduce((acc, privilege) => { - const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); - return { - ...acc, - [featurePrivilege.featureId]: _.uniq([ - ...acc[featurePrivilege.featureId] || [], - featurePrivilege.privilege - ]) - }; - }, {}), - spaces: resources.map(resource => ResourceSerializer.deserializeSpaceResource(resource)) - }; - }) - }; + return { success, value }; }; const transformUnrecognizedApplicationsFromEs = (roleApplications) => { @@ -160,7 +39,7 @@ export function initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, }; const transformRoleFromEs = (role, name) => { - const kibanaTransformResult = transformKibanaApplicationsFromEs(role.applications); + const kibanaTransformResult = transformRoleApplicationsFromEs(role.applications); return { name, diff --git a/x-pack/plugins/security/server/routes/api/v1/authenticate.js b/x-pack/plugins/security/server/routes/api/v1/authenticate.js index a59de3b2bf671..9e1bd0dbb073e 100644 --- a/x-pack/plugins/security/server/routes/api/v1/authenticate.js +++ b/x-pack/plugins/security/server/routes/api/v1/authenticate.js @@ -7,7 +7,7 @@ import Boom from 'boom'; import Joi from 'joi'; import { wrapError } from '../../../lib/errors'; -import { isAuthorizedKibanaUser } from '../../../lib/is_authorized_kibana_user'; +import { isAuthorizedKibanaUser } from '../../../lib/authorization'; import { canRedirectRequest } from '../../../lib/can_redirect_request'; export function initAuthenticateApi(server) { From ad4c461d83a67adfc73211ab669068e50e210262 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 14 Jun 2019 09:15:20 -0400 Subject: [PATCH 29/35] update authorization check --- .../get_privileges_with_request.test.ts | 133 ------------------ .../is_authorized_kibana_user.test.ts | 110 +++++++++------ .../is_authorized_kibana_user.ts | 20 ++- 3 files changed, 81 insertions(+), 182 deletions(-) delete mode 100644 x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.test.ts diff --git a/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.test.ts b/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.test.ts deleted file mode 100644 index 14ba8dfde95b2..0000000000000 --- a/x-pack/plugins/security/server/lib/authorization/get_privileges_with_request.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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 { RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../../common/constants'; -import { getPrivilegesWithRequestFactory } from './get_privileges_with_request'; -import { Legacy } from 'kibana'; -import { EsApplication } from './types'; - -const application = 'kibana-our_application'; - -const createMockShieldClient = (response: any) => { - const mockCallWithRequest = jest.fn(); - - mockCallWithRequest.mockImplementationOnce(async () => response); - - return { - callWithRequest: mockCallWithRequest, - }; -}; - -describe('#getPrivilegesWithRequest', () => { - const getPrivilegesWithRequestTest = ( - description: string, - options: { - esPrivilegesResponse: { applications: EsApplication[] }; - expectedResult?: any; - expectErrorThrown?: any; - } - ) => { - test(description, async () => { - const mockShieldClient = createMockShieldClient(options.esPrivilegesResponse); - - const getPrivilegesWithRequest = getPrivilegesWithRequestFactory( - application, - mockShieldClient - ); - const request = { foo: Symbol() }; - - let actualResult; - let errorThrown = null; - try { - actualResult = await getPrivilegesWithRequest((request as unknown) as Legacy.Request); - } catch (err) { - errorThrown = err; - } - - expect(mockShieldClient.callWithRequest).toHaveBeenCalledWith( - request, - 'shield.userPrivileges' - ); - - if (options.expectedResult) { - expect(errorThrown).toBeNull(); - expect(actualResult).toEqual(options.expectedResult); - } - }); - }; - - getPrivilegesWithRequestTest('returns ES Applications for this kibana instance', { - esPrivilegesResponse: { - applications: [ - { - application, - privileges: ['all', 'read'], - resources: ['*'], - }, - ], - }, - expectedResult: [ - { - application, - privileges: ['all', 'read'], - resources: ['*'], - }, - ], - }); - - getPrivilegesWithRequestTest('inclues ES Applications for the reserved privileges wildcard', { - esPrivilegesResponse: { - applications: [ - { - application, - privileges: ['all', 'read'], - resources: ['*'], - }, - { - application: RESERVED_PRIVILEGES_APPLICATION_WILDCARD, - privileges: ['reserved_foo'], - resources: ['*'], - }, - ], - }, - expectedResult: [ - { - application, - privileges: ['all', 'read'], - resources: ['*'], - }, - { - application: RESERVED_PRIVILEGES_APPLICATION_WILDCARD, - privileges: ['reserved_foo'], - resources: ['*'], - }, - ], - }); - - getPrivilegesWithRequestTest('excludes unknown ES Applications', { - esPrivilegesResponse: { - applications: [ - { - application, - privileges: ['all', 'read'], - resources: ['*'], - }, - { - application: 'kibana-.unknownApp', - privileges: ['reserved_foo'], - resources: ['*'], - }, - ], - }, - expectedResult: [ - { - application, - privileges: ['all', 'read'], - resources: ['*'], - }, - ], - }); -}); diff --git a/x-pack/plugins/security/server/lib/authorization/is_authorized_kibana_user.test.ts b/x-pack/plugins/security/server/lib/authorization/is_authorized_kibana_user.test.ts index cf73ccce1de08..36686b2214e05 100644 --- a/x-pack/plugins/security/server/lib/authorization/is_authorized_kibana_user.test.ts +++ b/x-pack/plugins/security/server/lib/authorization/is_authorized_kibana_user.test.ts @@ -7,13 +7,14 @@ import { Legacy } from 'kibana'; import { AuthorizationService } from './service'; import { isAuthorizedKibanaUser } from './is_authorized_kibana_user'; -import { PrivilegeSerializer } from './privilege_serializer'; -import { EsApplication } from './types'; +import { TransformApplicationsFromEsResponse } from './types'; -function buildAuthorizationService(privileges: EsApplication[] = []) { +function buildAuthorizationService( + privilegesResponse: TransformApplicationsFromEsResponse = { success: true, value: [] } +) { return ({ application: 'kibana-.kibana', - getPrivilegesWithRequest: jest.fn().mockResolvedValue([...privileges]), + getPrivilegesWithRequest: jest.fn().mockResolvedValue(privilegesResponse), mode: { useRbacForRequest: jest.fn().mockReturnValue(true), }, @@ -28,7 +29,7 @@ function buildAuthorizationService(privileges: EsApplication[] = []) { read: ['actions'], }, features: { - feature_1: { + feature1: { all: ['actions'], }, }, @@ -67,71 +68,88 @@ describe('isAuthorizedKibanaUser', () => { it('returns false for users with only reserved privileges', async () => { const request = buildRequest(); - const authService = buildAuthorizationService([ - { - application: 'kibana-.kibana', - privileges: [PrivilegeSerializer.serializeReservedPrivilege('foo')], - resources: ['*'], - }, - ]); + const authService = buildAuthorizationService({ + success: true, + value: [ + { + base: [], + feature: {}, + _reserved: ['foo'], + spaces: ['*'], + }, + ], + }); await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(false); }); it('returns true for users with a base privilege', async () => { const request = buildRequest(); - const authService = buildAuthorizationService([ - { - application: 'kibana-.kibana', - privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], - resources: ['*'], - }, - ]); + const authService = buildAuthorizationService({ + success: true, + value: [ + { + base: ['all'], + feature: {}, + spaces: ['*'], + }, + ], + }); await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true); }); it('returns true for users with a feature privilege', async () => { const request = buildRequest(); - const authService = buildAuthorizationService([ - { - application: 'kibana-.kibana', - privileges: [PrivilegeSerializer.serializeFeaturePrivilege('feature_1', 'all')], - resources: ['*'], - }, - ]); + const authService = buildAuthorizationService({ + success: true, + value: [ + { + base: [], + feature: { + feature1: ['all'], + }, + spaces: ['*'], + }, + ], + }); await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true); }); it('returns true for users with both reserved and non-reserved privileges', async () => { const request = buildRequest(); - const authService = buildAuthorizationService([ - { - application: 'kibana-.kibana', - privileges: [ - PrivilegeSerializer.serializeFeaturePrivilege('feature_1', 'all'), - PrivilegeSerializer.serializeReservedPrivilege('foo'), - ], - resources: ['*'], - }, - ]); + const authService = buildAuthorizationService({ + success: true, + value: [ + { + base: [], + feature: { + feature1: ['all'], + }, + _reserved: ['foo'], + spaces: ['*'], + }, + ], + }); await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true); }); it('returns false for users with unknown privileges', async () => { const request = buildRequest(); - const authService = buildAuthorizationService([ - { - application: 'kibana-.kibana', - privileges: [ - PrivilegeSerializer.serializeFeaturePrivilege('feature_1', 'unknown'), - PrivilegeSerializer.serializeReservedPrivilege('foo'), - ], - resources: ['*'], - }, - ]); + const authService = buildAuthorizationService({ + success: true, + value: [ + { + base: [], + feature: { + feature1: ['unknown'], + }, + spaces: ['*'], + }, + ], + }); await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(false); }); diff --git a/x-pack/plugins/security/server/lib/authorization/is_authorized_kibana_user.ts b/x-pack/plugins/security/server/lib/authorization/is_authorized_kibana_user.ts index bf9f147357a5d..73832950b2be9 100644 --- a/x-pack/plugins/security/server/lib/authorization/is_authorized_kibana_user.ts +++ b/x-pack/plugins/security/server/lib/authorization/is_authorized_kibana_user.ts @@ -35,11 +35,25 @@ export const isAuthorizedKibanaUser = async ( const userKibanaPrivileges = userPrivileges.value as KibanaApplication[]; + const knownPrivileges = authorizationService.privileges.get(); + return userKibanaPrivileges.some(privilege => { - const hasBasePrivileges = privilege.base.length > 0; - const hasFeaturePrivileges = Object.values(privilege.feature).some( - featurePrivileges => featurePrivileges.length > 0 + const hasBasePrivileges = privilege.base.some(basePrivilege => + Object.keys(knownPrivileges.global).includes(basePrivilege) + ); + + const hasFeaturePrivileges = Object.entries(privilege.feature).some( + ([featureId, privileges]) => { + const knownFeaturePrivilege = knownPrivileges.features[featureId]; + if (knownFeaturePrivilege) { + return Object.keys(knownFeaturePrivilege).some(knownPrivilege => + privileges.includes(knownPrivilege) + ); + } + return false; + } ); + return hasBasePrivileges || hasFeaturePrivileges; }); }; From 260c2ef6083d9aeb461df91d0765a2f9cfd94fc8 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 20 Jun 2019 15:34:26 -0400 Subject: [PATCH 30/35] fix merge from master --- .../plugins/xpack_main/public/views/_index.scss | 0 .../xpack_main/public/views/unavailable/_index.scss | 0 .../public/views/unavailable/_unavailable_page.scss | 0 .../xpack_main/public/views/unavailable/index.ts | 0 .../public/views/unavailable/unavailable.html | 0 .../public/views/unavailable/unavailable.tsx | 0 .../server/lib/__tests__/__fixtures__/request.ts | 0 .../server/lib/__tests__/__fixtures__/server.ts | 0 .../server/lib/can_redirect_request.test.ts | 0 .../xpack_main/server/lib/can_redirect_request.ts | 0 .../plugins/xpack_main/server/lib/on_pre_response.ts | 0 x-pack/plugins/xpack_main/public/index.scss | 12 ------------ 12 files changed, 12 deletions(-) rename x-pack/{ => legacy}/plugins/xpack_main/public/views/_index.scss (100%) rename x-pack/{ => legacy}/plugins/xpack_main/public/views/unavailable/_index.scss (100%) rename x-pack/{ => legacy}/plugins/xpack_main/public/views/unavailable/_unavailable_page.scss (100%) rename x-pack/{ => legacy}/plugins/xpack_main/public/views/unavailable/index.ts (100%) rename x-pack/{ => legacy}/plugins/xpack_main/public/views/unavailable/unavailable.html (100%) rename x-pack/{ => legacy}/plugins/xpack_main/public/views/unavailable/unavailable.tsx (100%) rename x-pack/{ => legacy}/plugins/xpack_main/server/lib/__tests__/__fixtures__/request.ts (100%) rename x-pack/{ => legacy}/plugins/xpack_main/server/lib/__tests__/__fixtures__/server.ts (100%) rename x-pack/{ => legacy}/plugins/xpack_main/server/lib/can_redirect_request.test.ts (100%) rename x-pack/{ => legacy}/plugins/xpack_main/server/lib/can_redirect_request.ts (100%) rename x-pack/{ => legacy}/plugins/xpack_main/server/lib/on_pre_response.ts (100%) delete mode 100644 x-pack/plugins/xpack_main/public/index.scss diff --git a/x-pack/plugins/xpack_main/public/views/_index.scss b/x-pack/legacy/plugins/xpack_main/public/views/_index.scss similarity index 100% rename from x-pack/plugins/xpack_main/public/views/_index.scss rename to x-pack/legacy/plugins/xpack_main/public/views/_index.scss diff --git a/x-pack/plugins/xpack_main/public/views/unavailable/_index.scss b/x-pack/legacy/plugins/xpack_main/public/views/unavailable/_index.scss similarity index 100% rename from x-pack/plugins/xpack_main/public/views/unavailable/_index.scss rename to x-pack/legacy/plugins/xpack_main/public/views/unavailable/_index.scss diff --git a/x-pack/plugins/xpack_main/public/views/unavailable/_unavailable_page.scss b/x-pack/legacy/plugins/xpack_main/public/views/unavailable/_unavailable_page.scss similarity index 100% rename from x-pack/plugins/xpack_main/public/views/unavailable/_unavailable_page.scss rename to x-pack/legacy/plugins/xpack_main/public/views/unavailable/_unavailable_page.scss diff --git a/x-pack/plugins/xpack_main/public/views/unavailable/index.ts b/x-pack/legacy/plugins/xpack_main/public/views/unavailable/index.ts similarity index 100% rename from x-pack/plugins/xpack_main/public/views/unavailable/index.ts rename to x-pack/legacy/plugins/xpack_main/public/views/unavailable/index.ts diff --git a/x-pack/plugins/xpack_main/public/views/unavailable/unavailable.html b/x-pack/legacy/plugins/xpack_main/public/views/unavailable/unavailable.html similarity index 100% rename from x-pack/plugins/xpack_main/public/views/unavailable/unavailable.html rename to x-pack/legacy/plugins/xpack_main/public/views/unavailable/unavailable.html diff --git a/x-pack/plugins/xpack_main/public/views/unavailable/unavailable.tsx b/x-pack/legacy/plugins/xpack_main/public/views/unavailable/unavailable.tsx similarity index 100% rename from x-pack/plugins/xpack_main/public/views/unavailable/unavailable.tsx rename to x-pack/legacy/plugins/xpack_main/public/views/unavailable/unavailable.tsx diff --git a/x-pack/plugins/xpack_main/server/lib/__tests__/__fixtures__/request.ts b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/__fixtures__/request.ts similarity index 100% rename from x-pack/plugins/xpack_main/server/lib/__tests__/__fixtures__/request.ts rename to x-pack/legacy/plugins/xpack_main/server/lib/__tests__/__fixtures__/request.ts diff --git a/x-pack/plugins/xpack_main/server/lib/__tests__/__fixtures__/server.ts b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/__fixtures__/server.ts similarity index 100% rename from x-pack/plugins/xpack_main/server/lib/__tests__/__fixtures__/server.ts rename to x-pack/legacy/plugins/xpack_main/server/lib/__tests__/__fixtures__/server.ts diff --git a/x-pack/plugins/xpack_main/server/lib/can_redirect_request.test.ts b/x-pack/legacy/plugins/xpack_main/server/lib/can_redirect_request.test.ts similarity index 100% rename from x-pack/plugins/xpack_main/server/lib/can_redirect_request.test.ts rename to x-pack/legacy/plugins/xpack_main/server/lib/can_redirect_request.test.ts diff --git a/x-pack/plugins/xpack_main/server/lib/can_redirect_request.ts b/x-pack/legacy/plugins/xpack_main/server/lib/can_redirect_request.ts similarity index 100% rename from x-pack/plugins/xpack_main/server/lib/can_redirect_request.ts rename to x-pack/legacy/plugins/xpack_main/server/lib/can_redirect_request.ts diff --git a/x-pack/plugins/xpack_main/server/lib/on_pre_response.ts b/x-pack/legacy/plugins/xpack_main/server/lib/on_pre_response.ts similarity index 100% rename from x-pack/plugins/xpack_main/server/lib/on_pre_response.ts rename to x-pack/legacy/plugins/xpack_main/server/lib/on_pre_response.ts diff --git a/x-pack/plugins/xpack_main/public/index.scss b/x-pack/plugins/xpack_main/public/index.scss deleted file mode 100644 index 904adf1b955e8..0000000000000 --- a/x-pack/plugins/xpack_main/public/index.scss +++ /dev/null @@ -1,12 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -// Prefix all styles with "xpk" to avoid conflicts. -// Examples -// xpkChart -// xpkChart__legend -// xpkChart__legend--small -// xpkChart__legend-isLoading - -// Public views -@import './views/index'; - From b5400e5e19215a0f69dc7d93df97d8aa04aa25a4 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 20 Jun 2019 16:36:13 -0400 Subject: [PATCH 31/35] add missing files --- .../unauthorized_login_form/index.ts | 7 + .../unauthorized_login_form.tsx | 49 ++++ .../get_privileges_with_request.ts | 28 ++ .../is_authorized_kibana_user.test.ts | 156 ++++++++++ .../is_authorized_kibana_user.ts | 59 ++++ ...nsform_kibana_applications_from_es.test.ts | 271 ++++++++++++++++++ .../transform_kibana_applications_from_es.ts | 209 ++++++++++++++ 7 files changed, 779 insertions(+) create mode 100644 x-pack/legacy/plugins/security/public/views/login/components/unauthorized_login_form/index.ts create mode 100644 x-pack/legacy/plugins/security/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx create mode 100644 x-pack/legacy/plugins/security/server/lib/authorization/get_privileges_with_request.ts create mode 100644 x-pack/legacy/plugins/security/server/lib/authorization/is_authorized_kibana_user.test.ts create mode 100644 x-pack/legacy/plugins/security/server/lib/authorization/is_authorized_kibana_user.ts create mode 100644 x-pack/legacy/plugins/security/server/lib/authorization/transform_kibana_applications_from_es.test.ts create mode 100644 x-pack/legacy/plugins/security/server/lib/authorization/transform_kibana_applications_from_es.ts diff --git a/x-pack/legacy/plugins/security/public/views/login/components/unauthorized_login_form/index.ts b/x-pack/legacy/plugins/security/public/views/login/components/unauthorized_login_form/index.ts new file mode 100644 index 0000000000000..dcbd92150159c --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/login/components/unauthorized_login_form/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { UnauthorizedLoginForm } from './unauthorized_login_form'; diff --git a/x-pack/legacy/plugins/security/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx b/x-pack/legacy/plugins/security/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx new file mode 100644 index 0000000000000..2da600e3c252f --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx @@ -0,0 +1,49 @@ +/* + * 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 chrome from 'ui/chrome'; +import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; +import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import React from 'react'; + +interface Props { + window: any; + intl: InjectedIntl; +} + +export const UnauthorizedLoginForm = injectI18n((props: Props) => { + function handleLogoutClick() { + props.window.location = chrome.addBasePath('/logout'); + } + + return ( + + + + + } + body={ +

+ +

+ } + actions={ + + Logout + + } + /> +
+ ); +}); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/get_privileges_with_request.ts b/x-pack/legacy/plugins/security/server/lib/authorization/get_privileges_with_request.ts new file mode 100644 index 0000000000000..6d837f50c72aa --- /dev/null +++ b/x-pack/legacy/plugins/security/server/lib/authorization/get_privileges_with_request.ts @@ -0,0 +1,28 @@ +/* + * 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 { Legacy } from 'kibana'; +import { transformKibanaApplicationsFromEs } from './transform_kibana_applications_from_es'; +import { TransformApplicationsFromEsResponse } from './types'; + +export type GetPrivilegesWithRequest = ( + request: Legacy.Request +) => Promise; + +export function getPrivilegesWithRequestFactory( + application: string, + shieldClient: any +): GetPrivilegesWithRequest { + const { callWithRequest } = shieldClient; + + return async function getPrivilegesWithRequest( + request: Legacy.Request + ): Promise { + const userPrivilegesResponse = await callWithRequest(request, 'shield.userPrivileges'); + + return transformKibanaApplicationsFromEs(application, userPrivilegesResponse.applications); + }; +} diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/is_authorized_kibana_user.test.ts b/x-pack/legacy/plugins/security/server/lib/authorization/is_authorized_kibana_user.test.ts new file mode 100644 index 0000000000000..36686b2214e05 --- /dev/null +++ b/x-pack/legacy/plugins/security/server/lib/authorization/is_authorized_kibana_user.test.ts @@ -0,0 +1,156 @@ +/* + * 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 { Legacy } from 'kibana'; +import { AuthorizationService } from './service'; +import { isAuthorizedKibanaUser } from './is_authorized_kibana_user'; +import { TransformApplicationsFromEsResponse } from './types'; + +function buildAuthorizationService( + privilegesResponse: TransformApplicationsFromEsResponse = { success: true, value: [] } +) { + return ({ + application: 'kibana-.kibana', + getPrivilegesWithRequest: jest.fn().mockResolvedValue(privilegesResponse), + mode: { + useRbacForRequest: jest.fn().mockReturnValue(true), + }, + privileges: { + get: () => ({ + global: { + all: ['actions'], + read: ['actions'], + }, + space: { + all: ['actions'], + read: ['actions'], + }, + features: { + feature1: { + all: ['actions'], + }, + }, + reserved: { + reserved_feature_1: ['actions'], + }, + }), + }, + } as unknown) as AuthorizationService; +} + +function buildRequest(): Legacy.Request { + const request: Legacy.Request = ({ + headers: { authorization: 'Basic: somegarbage' }, + } as unknown) as Legacy.Request; + + return request; +} + +describe('isAuthorizedKibanaUser', () => { + it('returns true for superusers', async () => { + const request = buildRequest(); + const authService = buildAuthorizationService(); + + await expect(isAuthorizedKibanaUser(authService, request, ['superuser'])).resolves.toEqual( + true + ); + }); + + it('returns false for users with no privileges', async () => { + const request = buildRequest(); + const authService = buildAuthorizationService(); + + await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(false); + }); + + it('returns false for users with only reserved privileges', async () => { + const request = buildRequest(); + const authService = buildAuthorizationService({ + success: true, + value: [ + { + base: [], + feature: {}, + _reserved: ['foo'], + spaces: ['*'], + }, + ], + }); + + await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(false); + }); + + it('returns true for users with a base privilege', async () => { + const request = buildRequest(); + const authService = buildAuthorizationService({ + success: true, + value: [ + { + base: ['all'], + feature: {}, + spaces: ['*'], + }, + ], + }); + + await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true); + }); + + it('returns true for users with a feature privilege', async () => { + const request = buildRequest(); + const authService = buildAuthorizationService({ + success: true, + value: [ + { + base: [], + feature: { + feature1: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true); + }); + + it('returns true for users with both reserved and non-reserved privileges', async () => { + const request = buildRequest(); + const authService = buildAuthorizationService({ + success: true, + value: [ + { + base: [], + feature: { + feature1: ['all'], + }, + _reserved: ['foo'], + spaces: ['*'], + }, + ], + }); + + await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true); + }); + + it('returns false for users with unknown privileges', async () => { + const request = buildRequest(); + const authService = buildAuthorizationService({ + success: true, + value: [ + { + base: [], + feature: { + feature1: ['unknown'], + }, + spaces: ['*'], + }, + ], + }); + + await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(false); + }); +}); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/is_authorized_kibana_user.ts b/x-pack/legacy/plugins/security/server/lib/authorization/is_authorized_kibana_user.ts new file mode 100644 index 0000000000000..73832950b2be9 --- /dev/null +++ b/x-pack/legacy/plugins/security/server/lib/authorization/is_authorized_kibana_user.ts @@ -0,0 +1,59 @@ +/* + * 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 { Legacy } from 'kibana'; +import { AuthorizationService } from './service'; +import { KibanaApplication } from './types'; + +export const isAuthorizedKibanaUser = async ( + authorizationService: AuthorizationService, + request: Legacy.Request, + userRoles: string[] = [] +) => { + // While `request.auth.credentials` is the cononical way to check for credentials on Hapi requests, + // it is _not_ populated on the `/api/security/v1/authenticate` route, where the user's session is first established. + // `request.headers.authorization` is present both on the authentiate route, and on subsequent requests by the nature of our authentication provider. + const hasCredentials = request.headers.authorization; + const useRbac = authorizationService.mode.useRbacForRequest(request); + + if (!hasCredentials || !useRbac) { + return true; + } + + if (userRoles.includes('superuser')) { + return true; + } + + const userPrivileges = await authorizationService.getPrivilegesWithRequest(request); + if (!userPrivileges.success) { + // TODO: what to do? + return true; + } + + const userKibanaPrivileges = userPrivileges.value as KibanaApplication[]; + + const knownPrivileges = authorizationService.privileges.get(); + + return userKibanaPrivileges.some(privilege => { + const hasBasePrivileges = privilege.base.some(basePrivilege => + Object.keys(knownPrivileges.global).includes(basePrivilege) + ); + + const hasFeaturePrivileges = Object.entries(privilege.feature).some( + ([featureId, privileges]) => { + const knownFeaturePrivilege = knownPrivileges.features[featureId]; + if (knownFeaturePrivilege) { + return Object.keys(knownFeaturePrivilege).some(knownPrivilege => + privileges.includes(knownPrivilege) + ); + } + return false; + } + ); + + return hasBasePrivileges || hasFeaturePrivileges; + }); +}; diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/transform_kibana_applications_from_es.test.ts b/x-pack/legacy/plugins/security/server/lib/authorization/transform_kibana_applications_from_es.test.ts new file mode 100644 index 0000000000000..017ca69d9662f --- /dev/null +++ b/x-pack/legacy/plugins/security/server/lib/authorization/transform_kibana_applications_from_es.test.ts @@ -0,0 +1,271 @@ +/* + * 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 { EsApplication } from './types'; +import { PrivilegeSerializer } from './privilege_serializer'; +import { ResourceSerializer } from './resource_serializer'; +import { transformKibanaApplicationsFromEs } from './transform_kibana_applications_from_es'; +import { RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../../common/constants'; + +const application = 'kibana-.kibana'; + +describe('transformKibanaApplicationsFromEs', () => { + it('throws for empty resources', () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], + resources: [], + }, + ]; + + expect(() => + transformKibanaApplicationsFromEs(application, esApplications) + ).toThrowErrorMatchingInlineSnapshot( + `"ES returned an application entry without resources, can't process this"` + ); + }); + + it('does not transform unknown applications', () => { + const esApplications: EsApplication[] = [ + { + application: 'kibana-.unknown', + privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], + resources: [ResourceSerializer.serializeSpaceResource('my-space')], + }, + { + application: 'other-.unknown', + privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], + resources: [ResourceSerializer.serializeSpaceResource('my-space')], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: true, + value: [], + }); + }); + + it('transforms well-formed entries', () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], + resources: ['*'], + }, + { + application, + privileges: [ + PrivilegeSerializer.serializeFeaturePrivilege('feature1', 'all'), + PrivilegeSerializer.serializeFeaturePrivilege('feature2', 'read'), + ], + resources: [ResourceSerializer.serializeSpaceResource('my-space')], + }, + { + application, + privileges: [PrivilegeSerializer.serializeSpaceBasePrivilege('all')], + resources: [ResourceSerializer.serializeSpaceResource('my-space')], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: true, + value: [ + { + base: ['all'], + feature: {}, + spaces: ['*'], + }, + { + base: [], + feature: { + feature1: ['all'], + feature2: ['read'], + }, + spaces: ['my-space'], + }, + { + base: ['all'], + feature: {}, + spaces: ['my-space'], + }, + ], + }); + }); + + it(`returns 'success: false' when global base privileges are assigned at a space`, () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], + resources: [ResourceSerializer.serializeSpaceResource('my-space')], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + + it(`returns 'success: false' when reserved privileges are assigned at a space`, () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [PrivilegeSerializer.serializeReservedPrivilege('all')], + resources: [ResourceSerializer.serializeSpaceResource('my-space')], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + + it(`returns 'success: false' when reserved privileges are assigned with other privileges`, () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [ + PrivilegeSerializer.serializeReservedPrivilege('all'), + PrivilegeSerializer.serializeGlobalBasePrivilege('all'), + ], + resources: ['*'], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + + it(`returns 'success: false' when both base and feature privileges are assigned globally in the same application entry`, () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [ + PrivilegeSerializer.serializeGlobalBasePrivilege('all'), + PrivilegeSerializer.serializeFeaturePrivilege('feature-1', 'all'), + ], + resources: ['*'], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + + it(`returns 'success: false' when both base and feature privileges are assigned at a space in the same application entry`, () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [ + PrivilegeSerializer.serializeSpaceBasePrivilege('all'), + PrivilegeSerializer.serializeFeaturePrivilege('feature-1', 'all'), + ], + resources: [ResourceSerializer.serializeSpaceResource('my-space')], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + + it(`returns 'success: false' when space base privileges are assigned globally`, () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [PrivilegeSerializer.serializeSpaceBasePrivilege('all')], + resources: ['*'], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + + it(`returns 'success: false' when privileges are assigned at both the global and other resources`, () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [PrivilegeSerializer.serializeFeaturePrivilege('feature-1', 'all')], + resources: ['*', ResourceSerializer.serializeSpaceResource('my-space')], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + + it(`returns 'success: false' for malformed resources`, () => { + const esApplications: EsApplication[] = [ + { + application, + privileges: [PrivilegeSerializer.serializeFeaturePrivilege('feature-1', 'all')], + resources: ['i-dont-know-what-i-am'], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + + describe('reserved privileges application wildcard', () => { + it('transforms reserved privileges', () => { + const esApplications: EsApplication[] = [ + { + application: RESERVED_PRIVILEGES_APPLICATION_WILDCARD, + privileges: [PrivilegeSerializer.serializeReservedPrivilege('all')], + resources: ['*'], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: true, + value: [ + { + base: [], + feature: {}, + _reserved: ['all'], + spaces: ['*'], + }, + ], + }); + }); + + it(`returns 'success: false' for non-reserved privileges`, () => { + const esApplications: EsApplication[] = [ + { + application: RESERVED_PRIVILEGES_APPLICATION_WILDCARD, + privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], + resources: ['*'], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + + it(`returns 'success: false' for reserved privileges at a specific resource`, () => { + const esApplications: EsApplication[] = [ + { + application: RESERVED_PRIVILEGES_APPLICATION_WILDCARD, + privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], + resources: [ResourceSerializer.serializeSpaceResource('some-space')], + }, + ]; + + expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ + success: false, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/transform_kibana_applications_from_es.ts b/x-pack/legacy/plugins/security/server/lib/authorization/transform_kibana_applications_from_es.ts new file mode 100644 index 0000000000000..a8419543a8780 --- /dev/null +++ b/x-pack/legacy/plugins/security/server/lib/authorization/transform_kibana_applications_from_es.ts @@ -0,0 +1,209 @@ +/* + * 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 _ from 'lodash'; +import { + RESERVED_PRIVILEGES_APPLICATION_WILDCARD, + GLOBAL_RESOURCE, +} from '../../../common/constants'; +import { PrivilegeSerializer, ResourceSerializer } from '.'; +import { EsApplication, TransformApplicationsFromEsResponse } from './types'; + +export const transformKibanaApplicationsFromEs = ( + application: string, + esApplications: EsApplication[] +): TransformApplicationsFromEsResponse => { + const kibanaApplications = esApplications.filter( + roleApplication => + roleApplication.application === application || + roleApplication.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD + ); + + // if any application entry contains an empty resource, we throw an error + if (kibanaApplications.some(entry => entry.resources.length === 0)) { + throw new Error(`ES returned an application entry without resources, can't process this`); + } + + // if there is an entry with the reserved privileges application wildcard + // and there are privileges which aren't reserved, we won't transform these + if ( + kibanaApplications.some( + entry => + entry.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD && + !entry.privileges.every(privilege => + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ) + ) + ) { + return { + success: false, + }; + } + + // if space privilege assigned globally, we can't transform these + if ( + kibanaApplications.some( + entry => + entry.resources.includes(GLOBAL_RESOURCE) && + entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege) + ) + ) + ) { + return { + success: false, + }; + } + + // if global base or reserved privilege assigned at a space, we can't transform these + if ( + kibanaApplications.some( + entry => + !entry.resources.includes(GLOBAL_RESOURCE) && + entry.privileges.some( + privilege => + PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) || + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ) + ) + ) { + return { + success: false, + }; + } + + // if reserved privilege assigned with feature or base privileges, we won't transform these + if ( + kibanaApplications.some( + entry => + entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ) && + entry.privileges.some( + privilege => !PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ) + ) + ) { + return { + success: false, + }; + } + + // if base privilege assigned with feature privileges, we won't transform these + if ( + kibanaApplications.some( + entry => + entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedFeaturePrivilege(privilege) + ) && + (entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) + ) || + entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege) + )) + ) + ) { + return { + success: false, + }; + } + + // if any application entry contains the '*' resource in addition to another resource, we can't transform these + if ( + kibanaApplications.some( + entry => entry.resources.includes(GLOBAL_RESOURCE) && entry.resources.length > 1 + ) + ) { + return { + success: false, + }; + } + + const allResources = _.flatten(kibanaApplications.map(entry => entry.resources)); + // if we have improperly formatted resource entries, we can't transform these + if ( + allResources.some( + resource => + resource !== GLOBAL_RESOURCE && !ResourceSerializer.isSerializedSpaceResource(resource) + ) + ) { + return { + success: false, + }; + } + + return { + success: true, + value: kibanaApplications.map(({ resources, privileges }) => { + // if we're dealing with a global entry, which we've ensured above is only possible if it's the only item in the array + if (resources.length === 1 && resources[0] === GLOBAL_RESOURCE) { + const reservedPrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ); + const basePrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) + ); + const featurePrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedFeaturePrivilege(privilege) + ); + + return { + ...(reservedPrivileges.length + ? { + _reserved: reservedPrivileges.map(privilege => + PrivilegeSerializer.deserializeReservedPrivilege(privilege) + ), + } + : {}), + base: basePrivileges.map(privilege => + PrivilegeSerializer.serializeGlobalBasePrivilege(privilege) + ), + feature: featurePrivileges.reduce( + (acc, privilege) => { + const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); + return { + ...acc, + [featurePrivilege.featureId]: _.uniq([ + ...(acc[featurePrivilege.featureId] || []), + featurePrivilege.privilege, + ]), + }; + }, + {} as Record + ), + spaces: ['*'], + }; + } + + const basePrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege) + ); + const featurePrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedFeaturePrivilege(privilege) + ); + return { + base: basePrivileges.map(privilege => + PrivilegeSerializer.deserializeSpaceBasePrivilege(privilege) + ), + feature: featurePrivileges.reduce( + (acc, privilege) => { + const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); + return { + ...acc, + [featurePrivilege.featureId]: _.uniq([ + ...(acc[featurePrivilege.featureId] || []), + featurePrivilege.privilege, + ]), + }; + }, + {} as Record + ), + spaces: resources.map(resource => ResourceSerializer.deserializeSpaceResource(resource)), + }; + }), + }; +}; From 5fcf289c269f173c934836d495f8b8a479dd541a Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 20 Jun 2019 16:41:20 -0400 Subject: [PATCH 32/35] remove incorrectly migrated files --- .../get_privileges_with_request.ts | 28 -- .../is_authorized_kibana_user.test.ts | 156 ---------- .../is_authorized_kibana_user.ts | 59 ---- ...nsform_kibana_applications_from_es.test.ts | 271 ------------------ .../transform_kibana_applications_from_es.ts | 209 -------------- 5 files changed, 723 deletions(-) delete mode 100644 x-pack/legacy/server/lib/authorization/get_privileges_with_request.ts delete mode 100644 x-pack/legacy/server/lib/authorization/is_authorized_kibana_user.test.ts delete mode 100644 x-pack/legacy/server/lib/authorization/is_authorized_kibana_user.ts delete mode 100644 x-pack/legacy/server/lib/authorization/transform_kibana_applications_from_es.test.ts delete mode 100644 x-pack/legacy/server/lib/authorization/transform_kibana_applications_from_es.ts diff --git a/x-pack/legacy/server/lib/authorization/get_privileges_with_request.ts b/x-pack/legacy/server/lib/authorization/get_privileges_with_request.ts deleted file mode 100644 index 6d837f50c72aa..0000000000000 --- a/x-pack/legacy/server/lib/authorization/get_privileges_with_request.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { Legacy } from 'kibana'; -import { transformKibanaApplicationsFromEs } from './transform_kibana_applications_from_es'; -import { TransformApplicationsFromEsResponse } from './types'; - -export type GetPrivilegesWithRequest = ( - request: Legacy.Request -) => Promise; - -export function getPrivilegesWithRequestFactory( - application: string, - shieldClient: any -): GetPrivilegesWithRequest { - const { callWithRequest } = shieldClient; - - return async function getPrivilegesWithRequest( - request: Legacy.Request - ): Promise { - const userPrivilegesResponse = await callWithRequest(request, 'shield.userPrivileges'); - - return transformKibanaApplicationsFromEs(application, userPrivilegesResponse.applications); - }; -} diff --git a/x-pack/legacy/server/lib/authorization/is_authorized_kibana_user.test.ts b/x-pack/legacy/server/lib/authorization/is_authorized_kibana_user.test.ts deleted file mode 100644 index 36686b2214e05..0000000000000 --- a/x-pack/legacy/server/lib/authorization/is_authorized_kibana_user.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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 { Legacy } from 'kibana'; -import { AuthorizationService } from './service'; -import { isAuthorizedKibanaUser } from './is_authorized_kibana_user'; -import { TransformApplicationsFromEsResponse } from './types'; - -function buildAuthorizationService( - privilegesResponse: TransformApplicationsFromEsResponse = { success: true, value: [] } -) { - return ({ - application: 'kibana-.kibana', - getPrivilegesWithRequest: jest.fn().mockResolvedValue(privilegesResponse), - mode: { - useRbacForRequest: jest.fn().mockReturnValue(true), - }, - privileges: { - get: () => ({ - global: { - all: ['actions'], - read: ['actions'], - }, - space: { - all: ['actions'], - read: ['actions'], - }, - features: { - feature1: { - all: ['actions'], - }, - }, - reserved: { - reserved_feature_1: ['actions'], - }, - }), - }, - } as unknown) as AuthorizationService; -} - -function buildRequest(): Legacy.Request { - const request: Legacy.Request = ({ - headers: { authorization: 'Basic: somegarbage' }, - } as unknown) as Legacy.Request; - - return request; -} - -describe('isAuthorizedKibanaUser', () => { - it('returns true for superusers', async () => { - const request = buildRequest(); - const authService = buildAuthorizationService(); - - await expect(isAuthorizedKibanaUser(authService, request, ['superuser'])).resolves.toEqual( - true - ); - }); - - it('returns false for users with no privileges', async () => { - const request = buildRequest(); - const authService = buildAuthorizationService(); - - await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(false); - }); - - it('returns false for users with only reserved privileges', async () => { - const request = buildRequest(); - const authService = buildAuthorizationService({ - success: true, - value: [ - { - base: [], - feature: {}, - _reserved: ['foo'], - spaces: ['*'], - }, - ], - }); - - await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(false); - }); - - it('returns true for users with a base privilege', async () => { - const request = buildRequest(); - const authService = buildAuthorizationService({ - success: true, - value: [ - { - base: ['all'], - feature: {}, - spaces: ['*'], - }, - ], - }); - - await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true); - }); - - it('returns true for users with a feature privilege', async () => { - const request = buildRequest(); - const authService = buildAuthorizationService({ - success: true, - value: [ - { - base: [], - feature: { - feature1: ['all'], - }, - spaces: ['*'], - }, - ], - }); - - await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true); - }); - - it('returns true for users with both reserved and non-reserved privileges', async () => { - const request = buildRequest(); - const authService = buildAuthorizationService({ - success: true, - value: [ - { - base: [], - feature: { - feature1: ['all'], - }, - _reserved: ['foo'], - spaces: ['*'], - }, - ], - }); - - await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true); - }); - - it('returns false for users with unknown privileges', async () => { - const request = buildRequest(); - const authService = buildAuthorizationService({ - success: true, - value: [ - { - base: [], - feature: { - feature1: ['unknown'], - }, - spaces: ['*'], - }, - ], - }); - - await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(false); - }); -}); diff --git a/x-pack/legacy/server/lib/authorization/is_authorized_kibana_user.ts b/x-pack/legacy/server/lib/authorization/is_authorized_kibana_user.ts deleted file mode 100644 index 73832950b2be9..0000000000000 --- a/x-pack/legacy/server/lib/authorization/is_authorized_kibana_user.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 { Legacy } from 'kibana'; -import { AuthorizationService } from './service'; -import { KibanaApplication } from './types'; - -export const isAuthorizedKibanaUser = async ( - authorizationService: AuthorizationService, - request: Legacy.Request, - userRoles: string[] = [] -) => { - // While `request.auth.credentials` is the cononical way to check for credentials on Hapi requests, - // it is _not_ populated on the `/api/security/v1/authenticate` route, where the user's session is first established. - // `request.headers.authorization` is present both on the authentiate route, and on subsequent requests by the nature of our authentication provider. - const hasCredentials = request.headers.authorization; - const useRbac = authorizationService.mode.useRbacForRequest(request); - - if (!hasCredentials || !useRbac) { - return true; - } - - if (userRoles.includes('superuser')) { - return true; - } - - const userPrivileges = await authorizationService.getPrivilegesWithRequest(request); - if (!userPrivileges.success) { - // TODO: what to do? - return true; - } - - const userKibanaPrivileges = userPrivileges.value as KibanaApplication[]; - - const knownPrivileges = authorizationService.privileges.get(); - - return userKibanaPrivileges.some(privilege => { - const hasBasePrivileges = privilege.base.some(basePrivilege => - Object.keys(knownPrivileges.global).includes(basePrivilege) - ); - - const hasFeaturePrivileges = Object.entries(privilege.feature).some( - ([featureId, privileges]) => { - const knownFeaturePrivilege = knownPrivileges.features[featureId]; - if (knownFeaturePrivilege) { - return Object.keys(knownFeaturePrivilege).some(knownPrivilege => - privileges.includes(knownPrivilege) - ); - } - return false; - } - ); - - return hasBasePrivileges || hasFeaturePrivileges; - }); -}; diff --git a/x-pack/legacy/server/lib/authorization/transform_kibana_applications_from_es.test.ts b/x-pack/legacy/server/lib/authorization/transform_kibana_applications_from_es.test.ts deleted file mode 100644 index 017ca69d9662f..0000000000000 --- a/x-pack/legacy/server/lib/authorization/transform_kibana_applications_from_es.test.ts +++ /dev/null @@ -1,271 +0,0 @@ -/* - * 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 { EsApplication } from './types'; -import { PrivilegeSerializer } from './privilege_serializer'; -import { ResourceSerializer } from './resource_serializer'; -import { transformKibanaApplicationsFromEs } from './transform_kibana_applications_from_es'; -import { RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../../common/constants'; - -const application = 'kibana-.kibana'; - -describe('transformKibanaApplicationsFromEs', () => { - it('throws for empty resources', () => { - const esApplications: EsApplication[] = [ - { - application, - privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], - resources: [], - }, - ]; - - expect(() => - transformKibanaApplicationsFromEs(application, esApplications) - ).toThrowErrorMatchingInlineSnapshot( - `"ES returned an application entry without resources, can't process this"` - ); - }); - - it('does not transform unknown applications', () => { - const esApplications: EsApplication[] = [ - { - application: 'kibana-.unknown', - privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], - resources: [ResourceSerializer.serializeSpaceResource('my-space')], - }, - { - application: 'other-.unknown', - privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], - resources: [ResourceSerializer.serializeSpaceResource('my-space')], - }, - ]; - - expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ - success: true, - value: [], - }); - }); - - it('transforms well-formed entries', () => { - const esApplications: EsApplication[] = [ - { - application, - privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], - resources: ['*'], - }, - { - application, - privileges: [ - PrivilegeSerializer.serializeFeaturePrivilege('feature1', 'all'), - PrivilegeSerializer.serializeFeaturePrivilege('feature2', 'read'), - ], - resources: [ResourceSerializer.serializeSpaceResource('my-space')], - }, - { - application, - privileges: [PrivilegeSerializer.serializeSpaceBasePrivilege('all')], - resources: [ResourceSerializer.serializeSpaceResource('my-space')], - }, - ]; - - expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ - success: true, - value: [ - { - base: ['all'], - feature: {}, - spaces: ['*'], - }, - { - base: [], - feature: { - feature1: ['all'], - feature2: ['read'], - }, - spaces: ['my-space'], - }, - { - base: ['all'], - feature: {}, - spaces: ['my-space'], - }, - ], - }); - }); - - it(`returns 'success: false' when global base privileges are assigned at a space`, () => { - const esApplications: EsApplication[] = [ - { - application, - privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], - resources: [ResourceSerializer.serializeSpaceResource('my-space')], - }, - ]; - - expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ - success: false, - }); - }); - - it(`returns 'success: false' when reserved privileges are assigned at a space`, () => { - const esApplications: EsApplication[] = [ - { - application, - privileges: [PrivilegeSerializer.serializeReservedPrivilege('all')], - resources: [ResourceSerializer.serializeSpaceResource('my-space')], - }, - ]; - - expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ - success: false, - }); - }); - - it(`returns 'success: false' when reserved privileges are assigned with other privileges`, () => { - const esApplications: EsApplication[] = [ - { - application, - privileges: [ - PrivilegeSerializer.serializeReservedPrivilege('all'), - PrivilegeSerializer.serializeGlobalBasePrivilege('all'), - ], - resources: ['*'], - }, - ]; - - expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ - success: false, - }); - }); - - it(`returns 'success: false' when both base and feature privileges are assigned globally in the same application entry`, () => { - const esApplications: EsApplication[] = [ - { - application, - privileges: [ - PrivilegeSerializer.serializeGlobalBasePrivilege('all'), - PrivilegeSerializer.serializeFeaturePrivilege('feature-1', 'all'), - ], - resources: ['*'], - }, - ]; - - expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ - success: false, - }); - }); - - it(`returns 'success: false' when both base and feature privileges are assigned at a space in the same application entry`, () => { - const esApplications: EsApplication[] = [ - { - application, - privileges: [ - PrivilegeSerializer.serializeSpaceBasePrivilege('all'), - PrivilegeSerializer.serializeFeaturePrivilege('feature-1', 'all'), - ], - resources: [ResourceSerializer.serializeSpaceResource('my-space')], - }, - ]; - - expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ - success: false, - }); - }); - - it(`returns 'success: false' when space base privileges are assigned globally`, () => { - const esApplications: EsApplication[] = [ - { - application, - privileges: [PrivilegeSerializer.serializeSpaceBasePrivilege('all')], - resources: ['*'], - }, - ]; - - expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ - success: false, - }); - }); - - it(`returns 'success: false' when privileges are assigned at both the global and other resources`, () => { - const esApplications: EsApplication[] = [ - { - application, - privileges: [PrivilegeSerializer.serializeFeaturePrivilege('feature-1', 'all')], - resources: ['*', ResourceSerializer.serializeSpaceResource('my-space')], - }, - ]; - - expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ - success: false, - }); - }); - - it(`returns 'success: false' for malformed resources`, () => { - const esApplications: EsApplication[] = [ - { - application, - privileges: [PrivilegeSerializer.serializeFeaturePrivilege('feature-1', 'all')], - resources: ['i-dont-know-what-i-am'], - }, - ]; - - expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ - success: false, - }); - }); - - describe('reserved privileges application wildcard', () => { - it('transforms reserved privileges', () => { - const esApplications: EsApplication[] = [ - { - application: RESERVED_PRIVILEGES_APPLICATION_WILDCARD, - privileges: [PrivilegeSerializer.serializeReservedPrivilege('all')], - resources: ['*'], - }, - ]; - - expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ - success: true, - value: [ - { - base: [], - feature: {}, - _reserved: ['all'], - spaces: ['*'], - }, - ], - }); - }); - - it(`returns 'success: false' for non-reserved privileges`, () => { - const esApplications: EsApplication[] = [ - { - application: RESERVED_PRIVILEGES_APPLICATION_WILDCARD, - privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], - resources: ['*'], - }, - ]; - - expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ - success: false, - }); - }); - - it(`returns 'success: false' for reserved privileges at a specific resource`, () => { - const esApplications: EsApplication[] = [ - { - application: RESERVED_PRIVILEGES_APPLICATION_WILDCARD, - privileges: [PrivilegeSerializer.serializeGlobalBasePrivilege('all')], - resources: [ResourceSerializer.serializeSpaceResource('some-space')], - }, - ]; - - expect(transformKibanaApplicationsFromEs(application, esApplications)).toEqual({ - success: false, - }); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/authorization/transform_kibana_applications_from_es.ts b/x-pack/legacy/server/lib/authorization/transform_kibana_applications_from_es.ts deleted file mode 100644 index a8419543a8780..0000000000000 --- a/x-pack/legacy/server/lib/authorization/transform_kibana_applications_from_es.ts +++ /dev/null @@ -1,209 +0,0 @@ -/* - * 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 _ from 'lodash'; -import { - RESERVED_PRIVILEGES_APPLICATION_WILDCARD, - GLOBAL_RESOURCE, -} from '../../../common/constants'; -import { PrivilegeSerializer, ResourceSerializer } from '.'; -import { EsApplication, TransformApplicationsFromEsResponse } from './types'; - -export const transformKibanaApplicationsFromEs = ( - application: string, - esApplications: EsApplication[] -): TransformApplicationsFromEsResponse => { - const kibanaApplications = esApplications.filter( - roleApplication => - roleApplication.application === application || - roleApplication.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD - ); - - // if any application entry contains an empty resource, we throw an error - if (kibanaApplications.some(entry => entry.resources.length === 0)) { - throw new Error(`ES returned an application entry without resources, can't process this`); - } - - // if there is an entry with the reserved privileges application wildcard - // and there are privileges which aren't reserved, we won't transform these - if ( - kibanaApplications.some( - entry => - entry.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD && - !entry.privileges.every(privilege => - PrivilegeSerializer.isSerializedReservedPrivilege(privilege) - ) - ) - ) { - return { - success: false, - }; - } - - // if space privilege assigned globally, we can't transform these - if ( - kibanaApplications.some( - entry => - entry.resources.includes(GLOBAL_RESOURCE) && - entry.privileges.some(privilege => - PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege) - ) - ) - ) { - return { - success: false, - }; - } - - // if global base or reserved privilege assigned at a space, we can't transform these - if ( - kibanaApplications.some( - entry => - !entry.resources.includes(GLOBAL_RESOURCE) && - entry.privileges.some( - privilege => - PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) || - PrivilegeSerializer.isSerializedReservedPrivilege(privilege) - ) - ) - ) { - return { - success: false, - }; - } - - // if reserved privilege assigned with feature or base privileges, we won't transform these - if ( - kibanaApplications.some( - entry => - entry.privileges.some(privilege => - PrivilegeSerializer.isSerializedReservedPrivilege(privilege) - ) && - entry.privileges.some( - privilege => !PrivilegeSerializer.isSerializedReservedPrivilege(privilege) - ) - ) - ) { - return { - success: false, - }; - } - - // if base privilege assigned with feature privileges, we won't transform these - if ( - kibanaApplications.some( - entry => - entry.privileges.some(privilege => - PrivilegeSerializer.isSerializedFeaturePrivilege(privilege) - ) && - (entry.privileges.some(privilege => - PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) - ) || - entry.privileges.some(privilege => - PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege) - )) - ) - ) { - return { - success: false, - }; - } - - // if any application entry contains the '*' resource in addition to another resource, we can't transform these - if ( - kibanaApplications.some( - entry => entry.resources.includes(GLOBAL_RESOURCE) && entry.resources.length > 1 - ) - ) { - return { - success: false, - }; - } - - const allResources = _.flatten(kibanaApplications.map(entry => entry.resources)); - // if we have improperly formatted resource entries, we can't transform these - if ( - allResources.some( - resource => - resource !== GLOBAL_RESOURCE && !ResourceSerializer.isSerializedSpaceResource(resource) - ) - ) { - return { - success: false, - }; - } - - return { - success: true, - value: kibanaApplications.map(({ resources, privileges }) => { - // if we're dealing with a global entry, which we've ensured above is only possible if it's the only item in the array - if (resources.length === 1 && resources[0] === GLOBAL_RESOURCE) { - const reservedPrivileges = privileges.filter(privilege => - PrivilegeSerializer.isSerializedReservedPrivilege(privilege) - ); - const basePrivileges = privileges.filter(privilege => - PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) - ); - const featurePrivileges = privileges.filter(privilege => - PrivilegeSerializer.isSerializedFeaturePrivilege(privilege) - ); - - return { - ...(reservedPrivileges.length - ? { - _reserved: reservedPrivileges.map(privilege => - PrivilegeSerializer.deserializeReservedPrivilege(privilege) - ), - } - : {}), - base: basePrivileges.map(privilege => - PrivilegeSerializer.serializeGlobalBasePrivilege(privilege) - ), - feature: featurePrivileges.reduce( - (acc, privilege) => { - const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); - return { - ...acc, - [featurePrivilege.featureId]: _.uniq([ - ...(acc[featurePrivilege.featureId] || []), - featurePrivilege.privilege, - ]), - }; - }, - {} as Record - ), - spaces: ['*'], - }; - } - - const basePrivileges = privileges.filter(privilege => - PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege) - ); - const featurePrivileges = privileges.filter(privilege => - PrivilegeSerializer.isSerializedFeaturePrivilege(privilege) - ); - return { - base: basePrivileges.map(privilege => - PrivilegeSerializer.deserializeSpaceBasePrivilege(privilege) - ), - feature: featurePrivileges.reduce( - (acc, privilege) => { - const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); - return { - ...acc, - [featurePrivilege.featureId]: _.uniq([ - ...(acc[featurePrivilege.featureId] || []), - featurePrivilege.privilege, - ]), - }; - }, - {} as Record - ), - spaces: resources.map(resource => ResourceSerializer.deserializeSpaceResource(resource)), - }; - }), - }; -}; From 0eda5296eeae691001a70e4d1494c66914bb5ecf Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 3 Jul 2019 12:47:43 -0400 Subject: [PATCH 33/35] fix merge --- x-pack/legacy/plugins/security/index.js | 1 + x-pack/legacy/plugins/xpack_main/public/index.scss | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 x-pack/legacy/plugins/xpack_main/public/index.scss diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index c8b91fdad59a2..76f12539c76ae 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -34,6 +34,7 @@ import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status import { SecureSavedObjectsClientWrapper } from './server/lib/saved_objects_client/secure_saved_objects_client_wrapper'; import { deepFreeze } from './server/lib/deep_freeze'; import { createOptionalPlugin } from '../../server/lib/optional_plugin'; +import { get } from 'lodash'; export const security = (kibana) => new kibana.Plugin({ id: 'security', diff --git a/x-pack/legacy/plugins/xpack_main/public/index.scss b/x-pack/legacy/plugins/xpack_main/public/index.scss new file mode 100644 index 0000000000000..ca150b3181e1f --- /dev/null +++ b/x-pack/legacy/plugins/xpack_main/public/index.scss @@ -0,0 +1,13 @@ +// Import the EUI global scope so we can use EUI constants +@import 'src/legacy/ui/public/styles/_styling_constants'; + +/* xpack main plugin styles */ + +// Prefix all styles with "xpk" to avoid conflicts. +// Examples +// xpkChart +// xpkChart__legend +// xpkChart__legend--small +// xpkChart__legend-isLoading + +@import './views/index'; From 175b4d22f51edc250930fbf8d8f33f152d728ba5 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 3 Jul 2019 13:26:18 -0400 Subject: [PATCH 34/35] remove stray files --- .../unauthorized_login_form/index.ts | 7 --- .../unauthorized_login_form.tsx | 49 ------------------- 2 files changed, 56 deletions(-) delete mode 100644 x-pack/legacy/public/views/login/components/unauthorized_login_form/index.ts delete mode 100644 x-pack/legacy/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx diff --git a/x-pack/legacy/public/views/login/components/unauthorized_login_form/index.ts b/x-pack/legacy/public/views/login/components/unauthorized_login_form/index.ts deleted file mode 100644 index dcbd92150159c..0000000000000 --- a/x-pack/legacy/public/views/login/components/unauthorized_login_form/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * 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 { UnauthorizedLoginForm } from './unauthorized_login_form'; diff --git a/x-pack/legacy/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx b/x-pack/legacy/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx deleted file mode 100644 index 2da600e3c252f..0000000000000 --- a/x-pack/legacy/public/views/login/components/unauthorized_login_form/unauthorized_login_form.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 chrome from 'ui/chrome'; -import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import React from 'react'; - -interface Props { - window: any; - intl: InjectedIntl; -} - -export const UnauthorizedLoginForm = injectI18n((props: Props) => { - function handleLogoutClick() { - props.window.location = chrome.addBasePath('/logout'); - } - - return ( - - - - - } - body={ -

- -

- } - actions={ - - Logout - - } - /> -
- ); -}); From 3b01d82ebfa965b8090392e6baddf248a9a64350 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 27 Aug 2019 14:41:37 -0400 Subject: [PATCH 35/35] fix merge --- .../lib/authorization/check_privileges_dynamically.test.ts | 2 +- .../spaces/server/lib/spaces_client/spaces_client.test.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts b/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts index 72a4e52361600..188fa4b0061c0 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts +++ b/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts @@ -15,7 +15,7 @@ test(`checkPrivileges.atSpace when spaces is enabled`, async () => { atSpace: jest.fn().mockReturnValue(expectedResult), }; const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockSpaces = ({ + const mockSpaces = { isEnabled: true, getSpaceId: jest.fn().mockReturnValue(spaceId), spaceIdToNamespace: jest.fn(), diff --git a/x-pack/legacy/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts b/x-pack/legacy/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts index 78ad10bbd9164..5409ffd6819ca 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts @@ -46,6 +46,9 @@ const createMockAuthorization = () => { 'checkSavedObjectsPrivilegesWithRequest should not be called from this test suite' ); }), + getPrivilegesWithRequest: jest.fn().mockImplementation(() => { + throw new Error('getPrivilegesWithRequest should not be called from this test suite'); + }), privileges: { get: jest.fn().mockImplementation(() => { throw new Error('privileges.get() should not be called from this test suite');