Skip to content

Commit

Permalink
[APM] Use ES Permission API to check if a user has permissions to rea…
Browse files Browse the repository at this point in the history
…d from APM indices (#57311)

* get indices privileges from has_privileges api

* changing to ES privileges api

* changing missing permission page

* always show dimiss button

* always show dimiss button

* changing message and unit test

* fixing react warning message
  • Loading branch information
cauemarcondes committed Feb 19, 2020
1 parent 077879a commit e7b6386
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* 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 { render, fireEvent } from '@testing-library/react';
import { shallow } from 'enzyme';
import { APMIndicesPermission } from '../';

import * as hooks from '../../../../hooks/useFetcher';
import {
expectTextsInDocument,
MockApmPluginContextWrapper,
expectTextsNotInDocument
} from '../../../../utils/testHelpers';

describe('APMIndicesPermission', () => {
it('returns empty component when api status is loading', () => {
spyOn(hooks, 'useFetcher').and.returnValue({
status: hooks.FETCH_STATUS.LOADING
});
const component = shallow(<APMIndicesPermission />);
expect(component.isEmptyRender()).toBeTruthy();
});
it('returns empty component when api status is pending', () => {
spyOn(hooks, 'useFetcher').and.returnValue({
status: hooks.FETCH_STATUS.PENDING
});
const component = shallow(<APMIndicesPermission />);
expect(component.isEmptyRender()).toBeTruthy();
});
it('renders missing permission page', () => {
spyOn(hooks, 'useFetcher').and.returnValue({
status: hooks.FETCH_STATUS.SUCCESS,
data: {
'apm-*': { read: false }
}
});
const component = render(
<MockApmPluginContextWrapper>
<APMIndicesPermission />
</MockApmPluginContextWrapper>
);
expectTextsInDocument(component, [
'Missing permissions to access APM',
'Dismiss',
'apm-*'
]);
});
it('shows escape hatch button when at least one indice has read privileges', () => {
spyOn(hooks, 'useFetcher').and.returnValue({
status: hooks.FETCH_STATUS.SUCCESS,
data: {
'apm-7.5.1-error-*': { read: false },
'apm-7.5.1-metric-*': { read: false },
'apm-7.5.1-transaction-*': { read: false },
'apm-7.5.1-span-*': { read: true }
}
});
const component = render(
<MockApmPluginContextWrapper>
<APMIndicesPermission />
</MockApmPluginContextWrapper>
);
expectTextsInDocument(component, [
'Missing permissions to access APM',
'apm-7.5.1-error-*',
'apm-7.5.1-metric-*',
'apm-7.5.1-transaction-*',
'Dismiss'
]);
expectTextsNotInDocument(component, ['apm-7.5.1-span-*']);
});

it('shows children component when indices have read privileges', () => {
spyOn(hooks, 'useFetcher').and.returnValue({
status: hooks.FETCH_STATUS.SUCCESS,
data: {
'apm-7.5.1-error-*': { read: true },
'apm-7.5.1-metric-*': { read: true },
'apm-7.5.1-transaction-*': { read: true },
'apm-7.5.1-span-*': { read: true }
}
});
const component = render(
<MockApmPluginContextWrapper>
<APMIndicesPermission>
<p>My amazing component</p>
</APMIndicesPermission>
</MockApmPluginContextWrapper>
);
expectTextsNotInDocument(component, [
'Missing permissions to access APM',
'apm-7.5.1-error-*',
'apm-7.5.1-metric-*',
'apm-7.5.1-transaction-*',
'apm-7.5.1-span-*'
]);
expectTextsInDocument(component, ['My amazing component']);
});

it('dismesses the warning by clicking on the escape hatch', () => {
spyOn(hooks, 'useFetcher').and.returnValue({
status: hooks.FETCH_STATUS.SUCCESS,
data: {
'apm-7.5.1-error-*': { read: false },
'apm-7.5.1-metric-*': { read: false },
'apm-7.5.1-transaction-*': { read: false },
'apm-7.5.1-span-*': { read: true }
}
});
const component = render(
<MockApmPluginContextWrapper>
<APMIndicesPermission>
<p>My amazing component</p>
</APMIndicesPermission>
</MockApmPluginContextWrapper>
);
expectTextsInDocument(component, ['Dismiss']);
fireEvent.click(component.getByText('Dismiss'));
expectTextsInDocument(component, ['My amazing component']);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,46 @@ import {
EuiFlexItem,
EuiLink,
EuiPanel,
EuiText,
EuiTitle
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import React, { useState } from 'react';
import styled from 'styled-components';
import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher';
import { fontSize, pct, px, units } from '../../../style/variables';
import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink';

export const Permission: React.FC = ({ children }) => {
const [isPermissionPageEnabled, setIsPermissionsPageEnabled] = useState(true);
export const APMIndicesPermission: React.FC = ({ children }) => {
const [
isPermissionWarningDismissed,
setIsPermissionWarningDismissed
] = useState(false);

const { data, status } = useFetcher(callApmApi => {
const { data: indicesPrivileges = {}, status } = useFetcher(callApmApi => {
return callApmApi({
pathname: '/api/apm/security/permissions'
pathname: '/api/apm/security/indices_privileges'
});
}, []);

// Return null until receive the reponse of the api.
if (status === FETCH_STATUS.LOADING || status === FETCH_STATUS.PENDING) {
return null;
}
// When the user doesn't have the appropriate permissions and they
// did not use the escape hatch, show the missing permissions page
if (data?.hasPermission === false && isPermissionPageEnabled) {

const indicesWithoutPermission = Object.keys(indicesPrivileges).filter(
index => !indicesPrivileges[index].read
);

// Show permission warning when a user has at least one index without Read privilege,
// and he has not manually dismissed the warning
if (!isEmpty(indicesWithoutPermission) && !isPermissionWarningDismissed) {
return (
<PermissionPage
onEscapeHatchClick={() => setIsPermissionsPageEnabled(false)}
<PermissionWarning
indicesWithoutPermission={indicesWithoutPermission}
onEscapeHatchClick={() => setIsPermissionWarningDismissed(true)}
/>
);
}
Expand All @@ -62,10 +73,14 @@ const EscapeHatch = styled.div`
`;

interface Props {
indicesWithoutPermission: string[];
onEscapeHatchClick: () => void;
}

const PermissionPage = ({ onEscapeHatchClick }: Props) => {
const PermissionWarning = ({
indicesWithoutPermission,
onEscapeHatchClick
}: Props) => {
return (
<div style={{ height: pct(95) }}>
<EuiFlexGroup alignItems="center">
Expand Down Expand Up @@ -96,12 +111,21 @@ const PermissionPage = ({ onEscapeHatchClick }: Props) => {
</h2>
}
body={
<p>
{i18n.translate('xpack.apm.permission.description', {
defaultMessage:
"We've detected your current role in Kibana does not grant you access to the APM data. Please check with your Kibana administrator to get the proper privileges granted in order to start using APM."
})}
</p>
<>
<p>
{i18n.translate('xpack.apm.permission.description', {
defaultMessage:
"Your user doesn't have access to all APM indices. You can still use the APM app but some data may be missing. You must be granted access to the following indices:"
})}
</p>
<ul style={{ listStyleType: 'none' }}>
{indicesWithoutPermission.map(index => (
<li key={index} style={{ marginTop: units.half }}>
<EuiText size="s">{index}</EuiText>
</li>
))}
</ul>
</>
}
actions={
<>
Expand All @@ -117,15 +141,14 @@ const PermissionPage = ({ onEscapeHatchClick }: Props) => {
</EuiButton>
)}
</ElasticDocsLink>

<EscapeHatch>
<EuiLink
color="subdued"
onClick={onEscapeHatchClick}
style={{ fontSize }}
>
{i18n.translate('xpack.apm.permission.dismissWarning', {
defaultMessage: 'Dismiss warning'
defaultMessage: 'Dismiss'
})}
</EuiLink>
</EscapeHatch>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ interface Props extends EuiLinkAnchorProps {
export function ElasticDocsLink({ section, path, children, ...rest }: Props) {
const { version } = useApmPluginContext().packageInfo;
const href = `https://www.elastic.co/guide/en${section}/${version}${path}`;
return (
return typeof children === 'function' ? (
children(href)
) : (
<EuiLink href={href} {...rest}>
{typeof children === 'function' ? children(href) : children}
children
</EuiLink>
);
}
7 changes: 3 additions & 4 deletions x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { setHelpExtension } from './setHelpExtension';
import { toggleAppLinkInNav } from './toggleAppLinkInNav';
import { setReadonlyBadge } from './updateBadge';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import { Permission } from '../components/app/Permission';
import { APMIndicesPermission } from '../components/app/APMIndicesPermission';

export const REACT_APP_ROOT_ID = 'react-apm-root';

Expand All @@ -53,14 +53,13 @@ const App = () => {
<MainContainer data-test-subj="apmMainContainer" role="main">
<UpdateBreadcrumbs routes={routes} />
<Route component={ScrollToTopOnPathChange} />
{/* Check if user has the appropriate permissions to use the APM UI. */}
<Permission>
<APMIndicesPermission>
<Switch>
{routes.map((route, i) => (
<ApmRoute key={i} {...route} />
))}
</Switch>
</Permission>
</APMIndicesPermission>
</MainContainer>
);
};
Expand Down
39 changes: 31 additions & 8 deletions x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,38 @@

/* eslint-disable no-console */
import {
SearchParams,
IndexDocumentParams,
IndicesCreateParams,
IndicesDeleteParams,
IndicesCreateParams
SearchParams
} from 'elasticsearch';
import { merge, uniqueId } from 'lodash';
import { cloneDeep, isString } from 'lodash';
import { cloneDeep, isString, merge, uniqueId } from 'lodash';
import { KibanaRequest } from 'src/core/server';
import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames';
import {
ESSearchResponse,
ESSearchRequest
ESSearchRequest,
ESSearchResponse
} from '../../../../../../plugins/apm/typings/elasticsearch';
import { APMRequestHandlerContext } from '../../routes/typings';
import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames';
import { pickKeys } from '../../../public/utils/pickKeys';
import { APMRequestHandlerContext } from '../../routes/typings';
import { getApmIndices } from '../settings/apm_indices/get_apm_indices';

// `type` was deprecated in 7.0
export type APMIndexDocumentParams<T> = Omit<IndexDocumentParams<T>, 'type'>;

interface IndexPrivileges {
has_all_requested: boolean;
username: string;
index: Record<string, { read: boolean }>;
}

interface IndexPrivilegesParams {
index: Array<{
names: string[] | string;
privileges: string[];
}>;
}

export function isApmIndex(
apmIndices: string[],
indexParam: SearchParams['index']
Expand Down Expand Up @@ -181,6 +193,17 @@ export function getESClient(
},
indicesCreate: (params: IndicesCreateParams) => {
return withTime(() => callMethod('indices.create', params));
},
hasPrivileges: (
params: IndexPrivilegesParams
): Promise<IndexPrivileges> => {
return withTime(() =>
callMethod('transport.request', {
method: 'POST',
path: '/_security/user/_has_privileges',
body: params
})
);
}
};
}
32 changes: 0 additions & 32 deletions x-pack/legacy/plugins/apm/server/lib/security/getPermissions.ts

This file was deleted.

Loading

0 comments on commit e7b6386

Please sign in to comment.