From 1a933c293db6c657ec38df1ab9f72a66933063a2 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Fri, 12 Jun 2020 11:44:26 -0400 Subject: [PATCH] [Component templates] Privileges support (#68733) Co-authored-by: Elastic Machine --- x-pack/plugins/index_management/kibana.json | 1 + .../component_template_list.helpers.ts | 2 +- .../component_template_list/auth_provider.tsx | 27 ++++ .../component_template_list_container.tsx | 21 +++ .../component_template_list/index.ts | 2 +- .../with_privileges.tsx | 83 ++++++++++ .../component_templates_context.tsx | 4 +- .../component_templates/constants.ts | 3 + .../component_templates/shared_imports.ts | 6 + .../plugins/index_management/server/plugin.ts | 5 +- .../routes/api/component_templates/index.ts | 2 + .../component_templates/privileges.test.ts | 153 ++++++++++++++++++ .../api/component_templates/privileges.ts | 70 ++++++++ .../plugins/index_management/server/types.ts | 5 + .../index_management/component_templates.ts | 15 ++ 15 files changed, 395 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/auth_provider.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list_container.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx create mode 100644 x-pack/plugins/index_management/server/routes/api/component_templates/privileges.test.ts create mode 100644 x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts diff --git a/x-pack/plugins/index_management/kibana.json b/x-pack/plugins/index_management/kibana.json index 7387a042988c01..2e0fa04337b400 100644 --- a/x-pack/plugins/index_management/kibana.json +++ b/x-pack/plugins/index_management/kibana.json @@ -9,6 +9,7 @@ "management" ], "optionalPlugins": [ + "security", "usageCollection" ], "configPath": ["xpack", "index_management"] diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.ts index 8fb4dcff0bcea6..cdcb1abf8ed5d8 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.ts @@ -15,7 +15,7 @@ import { nextTick, } from '../../../../../../../../../test_utils'; import { WithAppDependencies } from './setup_environment'; -import { ComponentTemplateList } from '../../../component_template_list'; +import { ComponentTemplateList } from '../../../component_template_list/component_template_list'; const testBedConfig: TestBedConfig = { memoryRouter: { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/auth_provider.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/auth_provider.tsx new file mode 100644 index 00000000000000..ed4bd10447b0b9 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/auth_provider.tsx @@ -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 React from 'react'; + +import { AuthorizationProvider } from '../shared_imports'; +import { useComponentTemplatesContext } from '../component_templates_context'; + +export const ComponentTemplatesAuthProvider: React.FunctionComponent = ({ + children, +}: { + children?: React.ReactNode; +}) => { + const { httpClient, apiBasePath } = useComponentTemplatesContext(); + + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list_container.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list_container.tsx new file mode 100644 index 00000000000000..af8ab1b94c7901 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list_container.tsx @@ -0,0 +1,21 @@ +/* + * 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 React from 'react'; + +import { ComponentTemplatesAuthProvider } from './auth_provider'; +import { ComponentTemplatesWithPrivileges } from './with_privileges'; +import { ComponentTemplateList } from './component_template_list'; + +export const ComponentTemplateListContainer: React.FunctionComponent = () => { + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/index.ts index 84ee48d14bb8c2..873e6d02298fb4 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ComponentTemplateList } from './component_template_list'; +export { ComponentTemplateListContainer as ComponentTemplateList } from './component_template_list_container'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx new file mode 100644 index 00000000000000..2886a7715ff09f --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx @@ -0,0 +1,83 @@ +/* + * 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 React, { FunctionComponent } from 'react'; + +import { + SectionError, + useAuthorizationContext, + WithPrivileges, + SectionLoading, + NotAuthorizedSection, +} from '../shared_imports'; +import { APP_CLUSTER_REQUIRED_PRIVILEGES } from '../constants'; + +export const ComponentTemplatesWithPrivileges: FunctionComponent = ({ + children, +}: { + children?: React.ReactNode; +}) => { + const { apiError } = useAuthorizationContext(); + + if (apiError) { + return ( + + } + error={apiError} + /> + ); + } + + return ( + `cluster.${privilege}`)} + > + {({ isLoading, hasPrivileges, privilegesMissing }) => { + if (isLoading) { + return ( + + + + ); + } + + if (!hasPrivileges) { + return ( + + } + message={ + + } + /> + ); + } + + return <>{children}; + }} + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx index 6f5f5bdebd6d06..55f20ce21d4177 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx @@ -21,6 +21,8 @@ interface Props { } interface Context { + httpClient: HttpSetup; + apiBasePath: string; api: ReturnType; documentation: ReturnType; trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void; @@ -45,7 +47,7 @@ export const ComponentTemplatesProvider = ({ return ( {children} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts b/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts index 3e763119fa9fbc..501acde07fc001 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts @@ -8,3 +8,6 @@ export const UIM_COMPONENT_TEMPLATE_LIST_LOAD = 'component_template_list_load'; export const UIM_COMPONENT_TEMPLATE_DELETE = 'component_template_delete'; export const UIM_COMPONENT_TEMPLATE_DELETE_MANY = 'component_template_delete_many'; + +// privileges +export const APP_CLUSTER_REQUIRED_PRIVILEGES = ['manage_index_templates']; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts index 863b00b353c491..049204f03c0c14 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts @@ -12,4 +12,10 @@ export { sendRequest, useRequest, SectionLoading, + WithPrivileges, + AuthorizationProvider, + SectionError, + Error, + useAuthorizationContext, + NotAuthorizedSection, } from '../../../../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts index f254333007c395..c1b9945c2cd1dc 100644 --- a/x-pack/plugins/index_management/server/plugin.ts +++ b/x-pack/plugins/index_management/server/plugin.ts @@ -59,7 +59,7 @@ export class IndexMgmtServerPlugin implements Plugin security !== undefined && security.license.isEnabled(), + }, indexDataEnricher: this.indexDataEnricher, lib: { isEsError, diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/index.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/index.ts index 7ecb71182e87e5..2ed123754a8b22 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/index.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/index.ts @@ -10,10 +10,12 @@ import { registerGetAllRoute } from './get'; import { registerCreateRoute } from './create'; import { registerUpdateRoute } from './update'; import { registerDeleteRoute } from './delete'; +import { registerPrivilegesRoute } from './privileges'; export function registerComponentTemplateRoutes(dependencies: RouteDependencies) { registerGetAllRoute(dependencies); registerCreateRoute(dependencies); registerUpdateRoute(dependencies); registerDeleteRoute(dependencies); + registerPrivilegesRoute(dependencies); } diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.test.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.test.ts new file mode 100644 index 00000000000000..b34ffe3e0baf5a --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.test.ts @@ -0,0 +1,153 @@ +/* + * 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 { httpServerMock, httpServiceMock } from 'src/core/server/mocks'; +import { + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, + IRouter, +} from 'src/core/server'; + +import { License } from '../../../services/license'; +import { IndexDataEnricher } from '../../../services/index_data_enricher'; + +import { registerPrivilegesRoute } from './privileges'; + +jest.mock('../../../services/index_data_enricher'); + +const httpService = httpServiceMock.createSetupContract(); + +const mockedIndexDataEnricher = new IndexDataEnricher(); + +const mockRouteContext = ({ + callAsCurrentUser, +}: { + callAsCurrentUser: any; +}): RequestHandlerContext => { + const routeContextMock = ({ + core: { + elasticsearch: { + legacy: { + client: { + callAsCurrentUser, + }, + }, + }, + }, + } as unknown) as RequestHandlerContext; + + return routeContextMock; +}; + +describe('GET privileges', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerPrivilegesRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + config: { + isSecurityEnabled: () => true, + }, + indexDataEnricher: mockedIndexDataEnricher, + lib: { + isEsError: jest.fn(), + }, + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it('should return the correct response when a user has privileges', async () => { + const privilegesResponseMock = { + username: 'elastic', + has_all_requested: true, + cluster: { manage_index_templates: true }, + index: {}, + application: {}, + }; + + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce(privilegesResponseMock), + }); + + const request = httpServerMock.createKibanaRequest(); + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + + expect(response.payload).toEqual({ + hasAllPrivileges: true, + missingPrivileges: { + cluster: [], + }, + }); + }); + + it('should return the correct response when a user does not have privileges', async () => { + const privilegesResponseMock = { + username: 'elastic', + has_all_requested: false, + cluster: { manage_index_templates: false }, + index: {}, + application: {}, + }; + + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce(privilegesResponseMock), + }); + + const request = httpServerMock.createKibanaRequest(); + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + + expect(response.payload).toEqual({ + hasAllPrivileges: false, + missingPrivileges: { + cluster: ['manage_index_templates'], + }, + }); + }); + + describe('With security disabled', () => { + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerPrivilegesRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + config: { + isSecurityEnabled: () => false, + }, + indexDataEnricher: mockedIndexDataEnricher, + lib: { + isEsError: jest.fn(), + }, + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it('should return the default privileges response', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn(), + }); + + const request = httpServerMock.createKibanaRequest(); + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + + expect(response.payload).toEqual({ + hasAllPrivileges: true, + missingPrivileges: { + cluster: [], + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts new file mode 100644 index 00000000000000..08a3c37187f2de --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts @@ -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 { Privileges } from 'src/plugins/es_ui_shared/public'; +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../index'; + +const extractMissingPrivileges = (privilegesObject: { [key: string]: boolean } = {}): string[] => + Object.keys(privilegesObject).reduce((privileges: string[], privilegeName: string): string[] => { + if (!privilegesObject[privilegeName]) { + privileges.push(privilegeName); + } + return privileges; + }, []); + +export const registerPrivilegesRoute = ({ license, router, config }: RouteDependencies) => { + router.get( + { + path: addBasePath('/component_templates/privileges'), + validate: false, + }, + license.guardApiRoute(async (ctx, req, res) => { + const privilegesResult: Privileges = { + hasAllPrivileges: true, + missingPrivileges: { + cluster: [], + }, + }; + + // Skip the privileges check if security is not enabled + if (!config.isSecurityEnabled()) { + return res.ok({ body: privilegesResult }); + } + + const { + core: { + elasticsearch: { + legacy: { client }, + }, + }, + } = ctx; + + try { + const { has_all_requested: hasAllPrivileges, cluster } = await client.callAsCurrentUser( + 'transport.request', + { + path: '/_security/user/_has_privileges', + method: 'POST', + body: { + cluster: ['manage_index_templates'], + }, + } + ); + + if (!hasAllPrivileges) { + privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster); + } + + privilegesResult.hasAllPrivileges = hasAllPrivileges; + + return res.ok({ body: privilegesResult }); + } catch (e) { + return res.internalError({ body: e }); + } + }) + ); +}; diff --git a/x-pack/plugins/index_management/server/types.ts b/x-pack/plugins/index_management/server/types.ts index b3fb546281f1ef..1482d9225c7b5f 100644 --- a/x-pack/plugins/index_management/server/types.ts +++ b/x-pack/plugins/index_management/server/types.ts @@ -5,16 +5,21 @@ */ import { ScopedClusterClient, IRouter } from 'src/core/server'; import { LicensingPluginSetup } from '../../licensing/server'; +import { SecurityPluginSetup } from '../../security/server'; import { License, IndexDataEnricher } from './services'; import { isEsError } from './lib/is_es_error'; export interface Dependencies { + security: SecurityPluginSetup; licensing: LicensingPluginSetup; } export interface RouteDependencies { router: IRouter; license: License; + config: { + isSecurityEnabled: () => boolean; + }; indexDataEnricher: IndexDataEnricher; lib: { isEsError: typeof isEsError; diff --git a/x-pack/test/api_integration/apis/management/index_management/component_templates.ts b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts index 10ab35714b1ce9..56b4ec45b42b71 100644 --- a/x-pack/test/api_integration/apis/management/index_management/component_templates.ts +++ b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts @@ -338,5 +338,20 @@ export default function ({ getService }: FtrProviderContext) { expect(body.errors[0].error.msg).to.contain('index_template_missing_exception'); }); }); + + describe('Privileges', () => { + it('should return privileges result', async () => { + const uri = `${API_BASE_PATH}/component_templates/privileges`; + + const { body } = await supertest.get(uri).set('kbn-xsrf', 'xxx').expect(200); + + expect(body).to.eql({ + hasAllPrivileges: true, + missingPrivileges: { + cluster: [], + }, + }); + }); + }); }); }