Skip to content

Commit e7b6386

Browse files
[APM] Use ES Permission API to check if a user has permissions to read 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
1 parent 077879a commit e7b6386

File tree

9 files changed

+233
-70
lines changed

9 files changed

+233
-70
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
import React from 'react';
7+
import { render, fireEvent } from '@testing-library/react';
8+
import { shallow } from 'enzyme';
9+
import { APMIndicesPermission } from '../';
10+
11+
import * as hooks from '../../../../hooks/useFetcher';
12+
import {
13+
expectTextsInDocument,
14+
MockApmPluginContextWrapper,
15+
expectTextsNotInDocument
16+
} from '../../../../utils/testHelpers';
17+
18+
describe('APMIndicesPermission', () => {
19+
it('returns empty component when api status is loading', () => {
20+
spyOn(hooks, 'useFetcher').and.returnValue({
21+
status: hooks.FETCH_STATUS.LOADING
22+
});
23+
const component = shallow(<APMIndicesPermission />);
24+
expect(component.isEmptyRender()).toBeTruthy();
25+
});
26+
it('returns empty component when api status is pending', () => {
27+
spyOn(hooks, 'useFetcher').and.returnValue({
28+
status: hooks.FETCH_STATUS.PENDING
29+
});
30+
const component = shallow(<APMIndicesPermission />);
31+
expect(component.isEmptyRender()).toBeTruthy();
32+
});
33+
it('renders missing permission page', () => {
34+
spyOn(hooks, 'useFetcher').and.returnValue({
35+
status: hooks.FETCH_STATUS.SUCCESS,
36+
data: {
37+
'apm-*': { read: false }
38+
}
39+
});
40+
const component = render(
41+
<MockApmPluginContextWrapper>
42+
<APMIndicesPermission />
43+
</MockApmPluginContextWrapper>
44+
);
45+
expectTextsInDocument(component, [
46+
'Missing permissions to access APM',
47+
'Dismiss',
48+
'apm-*'
49+
]);
50+
});
51+
it('shows escape hatch button when at least one indice has read privileges', () => {
52+
spyOn(hooks, 'useFetcher').and.returnValue({
53+
status: hooks.FETCH_STATUS.SUCCESS,
54+
data: {
55+
'apm-7.5.1-error-*': { read: false },
56+
'apm-7.5.1-metric-*': { read: false },
57+
'apm-7.5.1-transaction-*': { read: false },
58+
'apm-7.5.1-span-*': { read: true }
59+
}
60+
});
61+
const component = render(
62+
<MockApmPluginContextWrapper>
63+
<APMIndicesPermission />
64+
</MockApmPluginContextWrapper>
65+
);
66+
expectTextsInDocument(component, [
67+
'Missing permissions to access APM',
68+
'apm-7.5.1-error-*',
69+
'apm-7.5.1-metric-*',
70+
'apm-7.5.1-transaction-*',
71+
'Dismiss'
72+
]);
73+
expectTextsNotInDocument(component, ['apm-7.5.1-span-*']);
74+
});
75+
76+
it('shows children component when indices have read privileges', () => {
77+
spyOn(hooks, 'useFetcher').and.returnValue({
78+
status: hooks.FETCH_STATUS.SUCCESS,
79+
data: {
80+
'apm-7.5.1-error-*': { read: true },
81+
'apm-7.5.1-metric-*': { read: true },
82+
'apm-7.5.1-transaction-*': { read: true },
83+
'apm-7.5.1-span-*': { read: true }
84+
}
85+
});
86+
const component = render(
87+
<MockApmPluginContextWrapper>
88+
<APMIndicesPermission>
89+
<p>My amazing component</p>
90+
</APMIndicesPermission>
91+
</MockApmPluginContextWrapper>
92+
);
93+
expectTextsNotInDocument(component, [
94+
'Missing permissions to access APM',
95+
'apm-7.5.1-error-*',
96+
'apm-7.5.1-metric-*',
97+
'apm-7.5.1-transaction-*',
98+
'apm-7.5.1-span-*'
99+
]);
100+
expectTextsInDocument(component, ['My amazing component']);
101+
});
102+
103+
it('dismesses the warning by clicking on the escape hatch', () => {
104+
spyOn(hooks, 'useFetcher').and.returnValue({
105+
status: hooks.FETCH_STATUS.SUCCESS,
106+
data: {
107+
'apm-7.5.1-error-*': { read: false },
108+
'apm-7.5.1-metric-*': { read: false },
109+
'apm-7.5.1-transaction-*': { read: false },
110+
'apm-7.5.1-span-*': { read: true }
111+
}
112+
});
113+
const component = render(
114+
<MockApmPluginContextWrapper>
115+
<APMIndicesPermission>
116+
<p>My amazing component</p>
117+
</APMIndicesPermission>
118+
</MockApmPluginContextWrapper>
119+
);
120+
expectTextsInDocument(component, ['Dismiss']);
121+
fireEvent.click(component.getByText('Dismiss'));
122+
expectTextsInDocument(component, ['My amazing component']);
123+
});
124+
});

x-pack/legacy/plugins/apm/public/components/app/Permission/index.tsx renamed to x-pack/legacy/plugins/apm/public/components/app/APMIndicesPermission/index.tsx

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,46 @@ import {
1111
EuiFlexItem,
1212
EuiLink,
1313
EuiPanel,
14+
EuiText,
1415
EuiTitle
1516
} from '@elastic/eui';
1617
import { i18n } from '@kbn/i18n';
18+
import { isEmpty } from 'lodash';
1719
import React, { useState } from 'react';
1820
import styled from 'styled-components';
1921
import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher';
2022
import { fontSize, pct, px, units } from '../../../style/variables';
2123
import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
2224
import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink';
2325

24-
export const Permission: React.FC = ({ children }) => {
25-
const [isPermissionPageEnabled, setIsPermissionsPageEnabled] = useState(true);
26+
export const APMIndicesPermission: React.FC = ({ children }) => {
27+
const [
28+
isPermissionWarningDismissed,
29+
setIsPermissionWarningDismissed
30+
] = useState(false);
2631

27-
const { data, status } = useFetcher(callApmApi => {
32+
const { data: indicesPrivileges = {}, status } = useFetcher(callApmApi => {
2833
return callApmApi({
29-
pathname: '/api/apm/security/permissions'
34+
pathname: '/api/apm/security/indices_privileges'
3035
});
3136
}, []);
3237

3338
// Return null until receive the reponse of the api.
3439
if (status === FETCH_STATUS.LOADING || status === FETCH_STATUS.PENDING) {
3540
return null;
3641
}
37-
// When the user doesn't have the appropriate permissions and they
38-
// did not use the escape hatch, show the missing permissions page
39-
if (data?.hasPermission === false && isPermissionPageEnabled) {
42+
43+
const indicesWithoutPermission = Object.keys(indicesPrivileges).filter(
44+
index => !indicesPrivileges[index].read
45+
);
46+
47+
// Show permission warning when a user has at least one index without Read privilege,
48+
// and he has not manually dismissed the warning
49+
if (!isEmpty(indicesWithoutPermission) && !isPermissionWarningDismissed) {
4050
return (
41-
<PermissionPage
42-
onEscapeHatchClick={() => setIsPermissionsPageEnabled(false)}
51+
<PermissionWarning
52+
indicesWithoutPermission={indicesWithoutPermission}
53+
onEscapeHatchClick={() => setIsPermissionWarningDismissed(true)}
4354
/>
4455
);
4556
}
@@ -62,10 +73,14 @@ const EscapeHatch = styled.div`
6273
`;
6374

6475
interface Props {
76+
indicesWithoutPermission: string[];
6577
onEscapeHatchClick: () => void;
6678
}
6779

68-
const PermissionPage = ({ onEscapeHatchClick }: Props) => {
80+
const PermissionWarning = ({
81+
indicesWithoutPermission,
82+
onEscapeHatchClick
83+
}: Props) => {
6984
return (
7085
<div style={{ height: pct(95) }}>
7186
<EuiFlexGroup alignItems="center">
@@ -96,12 +111,21 @@ const PermissionPage = ({ onEscapeHatchClick }: Props) => {
96111
</h2>
97112
}
98113
body={
99-
<p>
100-
{i18n.translate('xpack.apm.permission.description', {
101-
defaultMessage:
102-
"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."
103-
})}
104-
</p>
114+
<>
115+
<p>
116+
{i18n.translate('xpack.apm.permission.description', {
117+
defaultMessage:
118+
"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:"
119+
})}
120+
</p>
121+
<ul style={{ listStyleType: 'none' }}>
122+
{indicesWithoutPermission.map(index => (
123+
<li key={index} style={{ marginTop: units.half }}>
124+
<EuiText size="s">{index}</EuiText>
125+
</li>
126+
))}
127+
</ul>
128+
</>
105129
}
106130
actions={
107131
<>
@@ -117,15 +141,14 @@ const PermissionPage = ({ onEscapeHatchClick }: Props) => {
117141
</EuiButton>
118142
)}
119143
</ElasticDocsLink>
120-
121144
<EscapeHatch>
122145
<EuiLink
123146
color="subdued"
124147
onClick={onEscapeHatchClick}
125148
style={{ fontSize }}
126149
>
127150
{i18n.translate('xpack.apm.permission.dismissWarning', {
128-
defaultMessage: 'Dismiss warning'
151+
defaultMessage: 'Dismiss'
129152
})}
130153
</EuiLink>
131154
</EscapeHatch>

x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ interface Props extends EuiLinkAnchorProps {
1919
export function ElasticDocsLink({ section, path, children, ...rest }: Props) {
2020
const { version } = useApmPluginContext().packageInfo;
2121
const href = `https://www.elastic.co/guide/en${section}/${version}${path}`;
22-
return (
22+
return typeof children === 'function' ? (
23+
children(href)
24+
) : (
2325
<EuiLink href={href} {...rest}>
24-
{typeof children === 'function' ? children(href) : children}
26+
children
2527
</EuiLink>
2628
);
2729
}

x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { setHelpExtension } from './setHelpExtension';
3838
import { toggleAppLinkInNav } from './toggleAppLinkInNav';
3939
import { setReadonlyBadge } from './updateBadge';
4040
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
41-
import { Permission } from '../components/app/Permission';
41+
import { APMIndicesPermission } from '../components/app/APMIndicesPermission';
4242

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

@@ -53,14 +53,13 @@ const App = () => {
5353
<MainContainer data-test-subj="apmMainContainer" role="main">
5454
<UpdateBreadcrumbs routes={routes} />
5555
<Route component={ScrollToTopOnPathChange} />
56-
{/* Check if user has the appropriate permissions to use the APM UI. */}
57-
<Permission>
56+
<APMIndicesPermission>
5857
<Switch>
5958
{routes.map((route, i) => (
6059
<ApmRoute key={i} {...route} />
6160
))}
6261
</Switch>
63-
</Permission>
62+
</APMIndicesPermission>
6463
</MainContainer>
6564
);
6665
};

x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,38 @@
66

77
/* eslint-disable no-console */
88
import {
9-
SearchParams,
109
IndexDocumentParams,
10+
IndicesCreateParams,
1111
IndicesDeleteParams,
12-
IndicesCreateParams
12+
SearchParams
1313
} from 'elasticsearch';
14-
import { merge, uniqueId } from 'lodash';
15-
import { cloneDeep, isString } from 'lodash';
14+
import { cloneDeep, isString, merge, uniqueId } from 'lodash';
1615
import { KibanaRequest } from 'src/core/server';
17-
import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames';
1816
import {
19-
ESSearchResponse,
20-
ESSearchRequest
17+
ESSearchRequest,
18+
ESSearchResponse
2119
} from '../../../../../../plugins/apm/typings/elasticsearch';
22-
import { APMRequestHandlerContext } from '../../routes/typings';
20+
import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames';
2321
import { pickKeys } from '../../../public/utils/pickKeys';
22+
import { APMRequestHandlerContext } from '../../routes/typings';
2423
import { getApmIndices } from '../settings/apm_indices/get_apm_indices';
2524

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

28+
interface IndexPrivileges {
29+
has_all_requested: boolean;
30+
username: string;
31+
index: Record<string, { read: boolean }>;
32+
}
33+
34+
interface IndexPrivilegesParams {
35+
index: Array<{
36+
names: string[] | string;
37+
privileges: string[];
38+
}>;
39+
}
40+
2941
export function isApmIndex(
3042
apmIndices: string[],
3143
indexParam: SearchParams['index']
@@ -181,6 +193,17 @@ export function getESClient(
181193
},
182194
indicesCreate: (params: IndicesCreateParams) => {
183195
return withTime(() => callMethod('indices.create', params));
196+
},
197+
hasPrivileges: (
198+
params: IndexPrivilegesParams
199+
): Promise<IndexPrivileges> => {
200+
return withTime(() =>
201+
callMethod('transport.request', {
202+
method: 'POST',
203+
path: '/_security/user/_has_privileges',
204+
body: params
205+
})
206+
);
184207
}
185208
};
186209
}

x-pack/legacy/plugins/apm/server/lib/security/getPermissions.ts

Lines changed: 0 additions & 32 deletions
This file was deleted.

0 commit comments

Comments
 (0)