Skip to content

Commit

Permalink
[Component templates] Privileges support (#68733)
Browse files Browse the repository at this point in the history
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
alisonelizabeth and elasticmachine committed Jun 12, 2020
1 parent ccf8def commit 1a933c2
Show file tree
Hide file tree
Showing 15 changed files with 395 additions and 4 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/index_management/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"management"
],
"optionalPlugins": [
"security",
"usageCollection"
],
"configPath": ["xpack", "index_management"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<AuthorizationProvider
privilegesEndpoint={`${apiBasePath}/component_templates/privileges`}
httpClient={httpClient}
>
{children}
</AuthorizationProvider>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<ComponentTemplatesAuthProvider>
<ComponentTemplatesWithPrivileges>
<ComponentTemplateList />
</ComponentTemplatesWithPrivileges>
</ComponentTemplatesAuthProvider>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -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 (
<SectionError
title={
<FormattedMessage
id="xpack.idxMgmt.home.componentTemplates.checkingPrivilegesErrorMessage"
defaultMessage="Error fetching user privileges from the server."
/>
}
error={apiError}
/>
);
}

return (
<WithPrivileges
privileges={APP_CLUSTER_REQUIRED_PRIVILEGES.map((privilege) => `cluster.${privilege}`)}
>
{({ isLoading, hasPrivileges, privilegesMissing }) => {
if (isLoading) {
return (
<SectionLoading>
<FormattedMessage
id="xpack.idxMgmt.home.componentTemplates.checkingPrivilegesDescription"
defaultMessage="Checking privileges…"
/>
</SectionLoading>
);
}

if (!hasPrivileges) {
return (
<NotAuthorizedSection
title={
<FormattedMessage
id="xpack.idxMgmt.home.componentTemplates.deniedPrivilegeTitle"
defaultMessage="Cluster privileges required"
/>
}
message={
<FormattedMessage
id="xpack.idxMgmt.home.componentTemplates.deniedPrivilegeDescription"
defaultMessage="To use Component Templates, you must have {privilegesCount,
plural, one {this cluster privilege} other {these cluster privileges}}: {missingPrivileges}."
values={{
missingPrivileges: privilegesMissing.cluster!.join(', '),
privilegesCount: privilegesMissing.cluster!.length,
}}
/>
}
/>
);
}

return <>{children}</>;
}}
</WithPrivileges>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ interface Props {
}

interface Context {
httpClient: HttpSetup;
apiBasePath: string;
api: ReturnType<typeof getApi>;
documentation: ReturnType<typeof getDocumentation>;
trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void;
Expand All @@ -45,7 +47,7 @@ export const ComponentTemplatesProvider = ({

return (
<ComponentTemplatesContext.Provider
value={{ api, documentation, trackMetric, toasts, appBasePath }}
value={{ api, documentation, trackMetric, toasts, appBasePath, httpClient, apiBasePath }}
>
{children}
</ComponentTemplatesContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,10 @@ export {
sendRequest,
useRequest,
SectionLoading,
WithPrivileges,
AuthorizationProvider,
SectionError,
Error,
useAuthorizationContext,
NotAuthorizedSection,
} from '../../../../../../../src/plugins/es_ui_shared/public';
5 changes: 4 additions & 1 deletion x-pack/plugins/index_management/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class IndexMgmtServerPlugin implements Plugin<IndexManagementPluginSetup,

setup(
{ http, getStartServices }: CoreSetup,
{ licensing }: Dependencies
{ licensing, security }: Dependencies
): IndexManagementPluginSetup {
const router = http.createRouter();

Expand Down Expand Up @@ -89,6 +89,9 @@ export class IndexMgmtServerPlugin implements Plugin<IndexManagementPluginSetup,
this.apiRoutes.setup({
router,
license: this.license,
config: {
isSecurityEnabled: () => security !== undefined && security.license.isEnabled(),
},
indexDataEnricher: this.indexDataEnricher,
lib: {
isEsError,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
@@ -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<any, any, any>;

beforeEach(() => {
const router = httpService.createRouter('') as jest.Mocked<IRouter>;

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<IRouter>;

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: [],
},
});
});
});
});
Loading

0 comments on commit 1a933c2

Please sign in to comment.