mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[APM] Add support for read-only (viewer) user (#161840)
Currently it is not possible for `viewer` or `editor` roles (the recommended stack roles) to access the diagnostics tool. This PR catches any permission exception gracefully. User with limited access will thereby still be able to use the diagnostics tool for the areas where they have access.
This commit is contained in:
parent
e8fefc6304
commit
52d34d4be7
23 changed files with 749 additions and 291 deletions
|
@ -1,13 +1,46 @@
|
|||
{
|
||||
"created_at": "2023-06-22T12:16:54.010Z",
|
||||
"diagnosticsPrivileges": {
|
||||
"index": {
|
||||
"apm-*": {
|
||||
"read": true
|
||||
},
|
||||
"logs-apm*": {
|
||||
"read": true
|
||||
},
|
||||
"metrics-apm*": {
|
||||
"read": true
|
||||
},
|
||||
"remote_cluster:apm-*": {
|
||||
"read": true
|
||||
},
|
||||
"remote_cluster:logs-apm*": {
|
||||
"read": true
|
||||
},
|
||||
"remote_cluster:metrics-apm*": {
|
||||
"read": true
|
||||
},
|
||||
"remote_cluster:traces-apm*": {
|
||||
"read": true
|
||||
},
|
||||
"traces-apm*": {
|
||||
"read": true
|
||||
}
|
||||
},
|
||||
"cluster": {
|
||||
"read_pipeline": true,
|
||||
"manage_index_templates": true,
|
||||
"monitor": true
|
||||
},
|
||||
"hasAllPrivileges": true
|
||||
},
|
||||
"elasticsearchVersion": "8.8.0",
|
||||
"esResponses": {
|
||||
"fieldCaps": {
|
||||
"fields": {
|
||||
"service.name": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"metadata_field": false,
|
||||
"type": "keyword", "metadata_field": false,
|
||||
"searchable": true,
|
||||
"aggregatable": true
|
||||
}
|
||||
|
|
|
@ -6,119 +6,162 @@
|
|||
*/
|
||||
|
||||
describe('Diagnostics', () => {
|
||||
describe('when no data is loaded', () => {
|
||||
describe('when logging in as superuser', () => {
|
||||
beforeEach(() => {
|
||||
cy.loginAs({ username: 'elastic', password: 'changeme' });
|
||||
cy.loginAsSuperUser();
|
||||
});
|
||||
|
||||
it('can display summary tab', () => {
|
||||
cy.visitKibana('/app/apm/diagnostics');
|
||||
describe('when no data is loaded', () => {
|
||||
it('can display summary tab for superuser', () => {
|
||||
cy.visitKibana('/app/apm/diagnostics');
|
||||
|
||||
// integration package
|
||||
cy.get('[data-test-subj="integrationPackageStatus_Badge"]').should(
|
||||
'have.text',
|
||||
'OK'
|
||||
);
|
||||
// integration package
|
||||
cy.get('[data-test-subj="integrationPackageStatus_Badge"]').should(
|
||||
'have.text',
|
||||
'OK'
|
||||
);
|
||||
|
||||
// data stream
|
||||
cy.get('[data-test-subj="dataStreamsStatus_Badge"]').should(
|
||||
'have.text',
|
||||
'OK'
|
||||
);
|
||||
// data stream
|
||||
cy.get('[data-test-subj="dataStreamsStatus_Badge"]').should(
|
||||
'have.text',
|
||||
'OK'
|
||||
);
|
||||
|
||||
// Index template
|
||||
cy.get('[data-test-subj="indexTemplatesStatus_Badge"]').should(
|
||||
'have.text',
|
||||
'OK'
|
||||
);
|
||||
// Index template
|
||||
cy.get('[data-test-subj="indexTemplatesStatus_Badge"]').should(
|
||||
'have.text',
|
||||
'OK'
|
||||
);
|
||||
|
||||
// Index template
|
||||
cy.get('[data-test-subj="fieldMappingStatus_Badge"]').should(
|
||||
'have.text',
|
||||
'OK'
|
||||
);
|
||||
// Index template
|
||||
cy.get('[data-test-subj="fieldMappingStatus_Badge"]').should(
|
||||
'have.text',
|
||||
'OK'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when importing a file', () => {
|
||||
it('shows the remove button', () => {
|
||||
importBundle();
|
||||
cy.get('[data-test-subj="apmImportCardRemoveReportButton"]').should(
|
||||
'exist'
|
||||
);
|
||||
clearBundle();
|
||||
cy.get('[data-test-subj="apmImportCardRemoveReportButton"]').should(
|
||||
'not.exist'
|
||||
);
|
||||
});
|
||||
|
||||
it('can display summary tab', () => {
|
||||
importBundle();
|
||||
cy.get('[data-test-subj="summary-tab"]').click();
|
||||
|
||||
// integration package
|
||||
cy.get('[data-test-subj="integrationPackageStatus_Badge"]').should(
|
||||
'have.text',
|
||||
'OK'
|
||||
);
|
||||
|
||||
cy.get('[data-test-subj="integrationPackageStatus_Content"]').should(
|
||||
'have.text',
|
||||
'APM integration (8.8.0)'
|
||||
);
|
||||
|
||||
// data stream
|
||||
cy.get('[data-test-subj="dataStreamsStatus_Badge"]').should(
|
||||
'have.text',
|
||||
'OK'
|
||||
);
|
||||
|
||||
// Index template
|
||||
cy.get('[data-test-subj="indexTemplatesStatus_Badge"]').should(
|
||||
'have.text',
|
||||
'OK'
|
||||
);
|
||||
|
||||
// Index template
|
||||
cy.get('[data-test-subj="fieldMappingStatus_Badge"]').should(
|
||||
'have.text',
|
||||
'Warning'
|
||||
);
|
||||
});
|
||||
|
||||
it('can display index template tab', () => {
|
||||
importBundle();
|
||||
cy.get('[data-test-subj="index-templates-tab"]').click();
|
||||
cy.get('.euiTableRow').should('have.length', 19);
|
||||
});
|
||||
|
||||
it('can display data streams tab', () => {
|
||||
importBundle();
|
||||
cy.get('[data-test-subj="data-streams-tab"]').click();
|
||||
cy.get('.euiTableRow').should('have.length', 8);
|
||||
});
|
||||
|
||||
it('can display indices tab', () => {
|
||||
importBundle();
|
||||
cy.get('[data-test-subj="indices-tab"]').click();
|
||||
|
||||
cy.get('[data-test-subj="indicedWithProblems"] .euiTableRow').should(
|
||||
'have.length',
|
||||
138
|
||||
);
|
||||
|
||||
cy.get('[data-test-subj="indicedWithoutProblems"] .euiTableRow').should(
|
||||
'have.length',
|
||||
27
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when importing a file', () => {
|
||||
describe('when logging in as "viewer" user', () => {
|
||||
beforeEach(() => {
|
||||
cy.loginAs({ username: 'elastic', password: 'changeme' });
|
||||
cy.visitKibana('/app/apm/diagnostics/import-export');
|
||||
cy.get('#file-picker').selectFile(
|
||||
'./cypress/e2e/power_user/diagnostics/apm-diagnostics-8.8.0-1687436214804.json'
|
||||
);
|
||||
cy.loginAsViewerUser();
|
||||
});
|
||||
|
||||
it('shows the remove button', () => {
|
||||
cy.get('[data-test-subj="apmImportCardRemoveReportButton"]').should(
|
||||
'exist'
|
||||
);
|
||||
describe('when no data is loaded', () => {
|
||||
it('displays a warning on "summary" tab about missing privileges ', () => {
|
||||
cy.visitKibana('/app/apm/diagnostics');
|
||||
|
||||
cy.get('.euiPanel > .euiText').should(
|
||||
'contain.text',
|
||||
'Not all features are available due to missing privileges.'
|
||||
);
|
||||
});
|
||||
|
||||
it('hides the tabs that require cluster privileges', () => {
|
||||
cy.visitKibana('/app/apm/diagnostics');
|
||||
|
||||
const tabs = ['Summary', 'Documents', 'Import/Export'];
|
||||
cy.get(
|
||||
'[data-test-subj="apmDiagnosticsTemplate"] .euiTabs .euiTab'
|
||||
).each((tab, i) => cy.wrap(tab).should('have.text', tabs[i]));
|
||||
});
|
||||
});
|
||||
|
||||
it('can display summary tab', () => {
|
||||
cy.get('[data-test-subj="summary-tab"]').click();
|
||||
describe('when importing a file', () => {
|
||||
it('displays documents tab for the imported bundle', () => {
|
||||
importBundle();
|
||||
cy.get('[data-test-subj="documents-tab"]').click();
|
||||
|
||||
// integration package
|
||||
cy.get('[data-test-subj="integrationPackageStatus_Badge"]').should(
|
||||
'have.text',
|
||||
'OK'
|
||||
);
|
||||
|
||||
cy.get('[data-test-subj="integrationPackageStatus_Content"]').should(
|
||||
'have.text',
|
||||
'APM integration (8.8.0)'
|
||||
);
|
||||
|
||||
// data stream
|
||||
cy.get('[data-test-subj="dataStreamsStatus_Badge"]').should(
|
||||
'have.text',
|
||||
'OK'
|
||||
);
|
||||
|
||||
// Index template
|
||||
cy.get('[data-test-subj="indexTemplatesStatus_Badge"]').should(
|
||||
'have.text',
|
||||
'OK'
|
||||
);
|
||||
|
||||
// Index template
|
||||
cy.get('[data-test-subj="fieldMappingStatus_Badge"]').should(
|
||||
'have.text',
|
||||
'Warning'
|
||||
);
|
||||
});
|
||||
|
||||
it('can display index template tab', () => {
|
||||
cy.get('[data-test-subj="index-templates-tab"]').click();
|
||||
cy.get('.euiTableRow').should('have.length', 19);
|
||||
});
|
||||
|
||||
it('can display data streams tab', () => {
|
||||
cy.get('[data-test-subj="data-streams-tab"]').click();
|
||||
cy.get('.euiTableRow').should('have.length', 8);
|
||||
});
|
||||
|
||||
it('can display indices tab', () => {
|
||||
cy.get('[data-test-subj="indices-tab"]').click();
|
||||
|
||||
cy.get('[data-test-subj="indicedWithProblems"] .euiTableRow').should(
|
||||
'have.length',
|
||||
138
|
||||
);
|
||||
|
||||
cy.get('[data-test-subj="indicedWithoutProblems"] .euiTableRow').should(
|
||||
'have.length',
|
||||
27
|
||||
);
|
||||
});
|
||||
|
||||
it('can display documents tab', () => {
|
||||
cy.get('[data-test-subj="documents-tab"]').click();
|
||||
|
||||
cy.get('[data-test-subj="documents-table"] .euiTableRow').should(
|
||||
'have.length',
|
||||
10
|
||||
);
|
||||
cy.get('[data-test-subj="documents-table"] .euiTableRow').should(
|
||||
'have.length',
|
||||
10
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function importBundle() {
|
||||
cy.visitKibana('/app/apm/diagnostics/import-export');
|
||||
cy.get('#file-picker').selectFile(
|
||||
'./cypress/e2e/power_user/diagnostics/apm-diagnostics-8.8.0-1687436214804.json'
|
||||
);
|
||||
}
|
||||
|
||||
function clearBundle() {
|
||||
cy.get('[data-test-subj="apmTemplateDescriptionClearBundleButton"]').click();
|
||||
}
|
||||
|
|
|
@ -11,6 +11,10 @@ import moment from 'moment';
|
|||
import { AXE_CONFIG, AXE_OPTIONS } from '@kbn/axe-config';
|
||||
import { ApmUsername } from '../../../server/test_helpers/create_apm_users/authentication';
|
||||
|
||||
Cypress.Commands.add('loginAsSuperUser', () => {
|
||||
return cy.loginAs({ username: 'elastic', password: 'changeme' });
|
||||
});
|
||||
|
||||
Cypress.Commands.add('loginAsViewerUser', () => {
|
||||
return cy.loginAs({ username: ApmUsername.viewerUser, password: 'changeme' });
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
loginAsSuperUser(): Cypress.Chainable<Cypress.Response<any>>;
|
||||
loginAsViewerUser(): Cypress.Chainable<Cypress.Response<any>>;
|
||||
loginAsEditorUser(): Cypress.Chainable<Cypress.Response<any>>;
|
||||
loginAsMonitorUser(): Cypress.Chainable<Cypress.Response<any>>;
|
||||
|
|
|
@ -18,9 +18,9 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { APIReturnType } from '../../../services/rest/create_call_apm_api';
|
||||
import { useDiagnosticsContext } from './context/use_diagnostics';
|
||||
import { getIndexTemplateStatus } from './summary_tab/index_templates_status';
|
||||
import { getIndicesTabStatus } from './summary_tab/indicies_status';
|
||||
import { getDataStreamTabStatus } from './summary_tab/data_streams_status';
|
||||
import { getIsIndexTemplateOk } from './summary_tab/index_templates_status';
|
||||
import { getIsIndicesTabOk } from './summary_tab/indicies_status';
|
||||
import { getIsDataStreamTabOk } from './summary_tab/data_streams_status';
|
||||
|
||||
type DiagnosticsBundle = APIReturnType<'GET /internal/apm/diagnostics'>;
|
||||
|
||||
|
@ -82,7 +82,12 @@ function ExportCard() {
|
|||
|
||||
function ImportCard() {
|
||||
const { setImportedDiagnosticsBundle, isImported } = useDiagnosticsContext();
|
||||
const [importError, setImportError] = useState(false);
|
||||
const [importStatus, setImportStatus] = useState<{
|
||||
isValid: boolean;
|
||||
errorMessage?: string;
|
||||
}>({
|
||||
isValid: true,
|
||||
});
|
||||
return (
|
||||
<EuiCard
|
||||
icon={<EuiIcon size="xxl" type="exportAction" />}
|
||||
|
@ -104,10 +109,11 @@ function ImportCard() {
|
|||
</EuiButton>
|
||||
) : (
|
||||
<>
|
||||
{importError && (
|
||||
{!importStatus.isValid && (
|
||||
<>
|
||||
<EuiCallOut color="danger" iconType="warning">
|
||||
The uploaded file could not be parsed
|
||||
The uploaded file could not be parsed:{' '}
|
||||
{importStatus.errorMessage}
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
|
@ -117,7 +123,7 @@ function ImportCard() {
|
|||
id="file-picker"
|
||||
multiple
|
||||
onChange={(_files) => {
|
||||
setImportError(false);
|
||||
setImportStatus({ isValid: true });
|
||||
|
||||
if (_files && _files.length > 0) {
|
||||
const file = Array.from(_files)[0];
|
||||
|
@ -129,13 +135,14 @@ function ImportCard() {
|
|||
evt?.target?.result
|
||||
) as DiagnosticsBundle;
|
||||
|
||||
if (isBundleValid(diagnosticsBundle)) {
|
||||
setImportedDiagnosticsBundle(diagnosticsBundle);
|
||||
} else {
|
||||
setImportError(true);
|
||||
}
|
||||
validateBundle(diagnosticsBundle);
|
||||
setImportedDiagnosticsBundle(diagnosticsBundle);
|
||||
} catch (e) {
|
||||
setImportError(true);
|
||||
setImportStatus({
|
||||
isValid: false,
|
||||
errorMessage: e.message,
|
||||
});
|
||||
|
||||
console.error(
|
||||
`Could not parse file ${file.name}. ${e.message}`
|
||||
);
|
||||
|
@ -154,13 +161,13 @@ function ImportCard() {
|
|||
);
|
||||
}
|
||||
|
||||
function isBundleValid(diagnosticsBundle: DiagnosticsBundle) {
|
||||
function validateBundle(diagnosticsBundle: DiagnosticsBundle) {
|
||||
try {
|
||||
getIndexTemplateStatus(diagnosticsBundle);
|
||||
getIndicesTabStatus(diagnosticsBundle);
|
||||
getDataStreamTabStatus(diagnosticsBundle);
|
||||
return true;
|
||||
getIsIndexTemplateOk(diagnosticsBundle);
|
||||
getIsIndicesTabOk(diagnosticsBundle);
|
||||
getIsDataStreamTabOk(diagnosticsBundle);
|
||||
} catch (e) {
|
||||
return false;
|
||||
console.error('Error parsing uploaded bundle', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,30 +5,37 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Outlet } from '@kbn/typed-react-router-config';
|
||||
import React from 'react';
|
||||
import * as t from 'io-ts';
|
||||
import { EuiButton, EuiCallOut, EuiIcon } from '@elastic/eui';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiIcon,
|
||||
EuiLoadingLogo,
|
||||
EuiEmptyPrompt,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useApmRouter } from '../../../hooks/use_apm_router';
|
||||
import { useApmRoutePath } from '../../../hooks/use_apm_route_path';
|
||||
import { DiagnosticsSummary } from './summary_tab';
|
||||
import { DiagnosticsSummary, getIsCrossCluster } from './summary_tab';
|
||||
import { ApmMainTemplate } from '../../routing/templates/apm_main_template';
|
||||
import { DiagnosticsIndexTemplates } from './index_templates_tab';
|
||||
import { DiagnosticsIndices } from './indices_tab';
|
||||
import { DiagnosticsDataStreams } from './data_stream_tab';
|
||||
import {
|
||||
DiagnosticsIndexPatternSettings,
|
||||
getIndexPatternTabStatus,
|
||||
getIsIndexPatternTabOk,
|
||||
} from './index_pattern_settings_tab';
|
||||
import { DiagnosticsImportExport } from './import_export_tab';
|
||||
import { DiagnosticsContextProvider } from './context/diagnostics_context';
|
||||
import { useDiagnosticsContext } from './context/use_diagnostics';
|
||||
import { getIndexTemplateStatus } from './summary_tab/index_templates_status';
|
||||
import { getDataStreamTabStatus } from './summary_tab/data_streams_status';
|
||||
import { getIndicesTabStatus } from './summary_tab/indicies_status';
|
||||
import { getIsIndexTemplateOk } from './summary_tab/index_templates_status';
|
||||
import { getIsDataStreamTabOk } from './summary_tab/data_streams_status';
|
||||
import { getIsIndicesTabOk } from './summary_tab/indicies_status';
|
||||
import { DiagnosticsApmDocuments } from './apm_documents_tab';
|
||||
import { isPending } from '../../../hooks/use_fetcher';
|
||||
|
||||
const params = t.type({
|
||||
query: t.intersection([
|
||||
|
@ -90,11 +97,110 @@ export const diagnosticsRoute = {
|
|||
function DiagnosticsTemplate({ children }: { children: React.ReactChild }) {
|
||||
const routePath = useApmRoutePath();
|
||||
const router = useApmRouter();
|
||||
const { diagnosticsBundle } = useDiagnosticsContext();
|
||||
const { diagnosticsBundle, status } = useDiagnosticsContext();
|
||||
const { query } = useApmParams('/diagnostics/*');
|
||||
const isCrossCluster = getIsCrossCluster(diagnosticsBundle);
|
||||
const isLoading = isPending(status);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
icon={<EuiLoadingLogo logo="logoObservability" size="xl" />}
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate('xpack.apm.diagnostics.loading', {
|
||||
defaultMessage: 'Loading diagnostics',
|
||||
})}
|
||||
</h2>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const hasAllClusterPrivileges =
|
||||
diagnosticsBundle?.diagnosticsPrivileges.hasAllClusterPrivileges ?? true;
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
'data-test-subj': 'summary-tab',
|
||||
href: router.link('/diagnostics', { query }),
|
||||
label: i18n.translate('xpack.apm.diagnostics.tab.summary', {
|
||||
defaultMessage: 'Summary',
|
||||
}),
|
||||
isSelected: routePath === '/diagnostics',
|
||||
},
|
||||
{
|
||||
'data-test-subj': 'index-pattern-tab',
|
||||
prepend: !getIsIndexPatternTabOk(diagnosticsBundle) && (
|
||||
<EuiIcon type="warning" color="red" />
|
||||
),
|
||||
href: router.link('/diagnostics/index-pattern-settings', { query }),
|
||||
label: i18n.translate(
|
||||
'xpack.apm.diagnostics.tab.index_pattern_settings',
|
||||
{
|
||||
defaultMessage: 'Index pattern settings',
|
||||
}
|
||||
),
|
||||
isSelected: routePath === '/diagnostics/index-pattern-settings',
|
||||
isHidden: isCrossCluster || !hasAllClusterPrivileges,
|
||||
},
|
||||
{
|
||||
'data-test-subj': 'index-templates-tab',
|
||||
prepend: !getIsIndexTemplateOk(diagnosticsBundle) && (
|
||||
<EuiIcon type="warning" color="red" />
|
||||
),
|
||||
href: router.link('/diagnostics/index-templates', { query }),
|
||||
label: i18n.translate('xpack.apm.diagnostics.tab.index_templates', {
|
||||
defaultMessage: 'Index templates',
|
||||
}),
|
||||
isSelected: routePath === '/diagnostics/index-templates',
|
||||
isHidden: isCrossCluster || !hasAllClusterPrivileges,
|
||||
},
|
||||
{
|
||||
'data-test-subj': 'data-streams-tab',
|
||||
prepend: !getIsDataStreamTabOk(diagnosticsBundle) && (
|
||||
<EuiIcon type="warning" color="red" />
|
||||
),
|
||||
href: router.link('/diagnostics/data-streams', { query }),
|
||||
label: i18n.translate('xpack.apm.diagnostics.tab.datastreams', {
|
||||
defaultMessage: 'Data streams',
|
||||
}),
|
||||
isSelected: routePath === '/diagnostics/data-streams',
|
||||
isHidden: isCrossCluster || !hasAllClusterPrivileges,
|
||||
},
|
||||
{
|
||||
'data-test-subj': 'indices-tab',
|
||||
prepend: !getIsIndicesTabOk(diagnosticsBundle) && (
|
||||
<EuiIcon type="warning" color="red" />
|
||||
),
|
||||
href: router.link('/diagnostics/indices', { query }),
|
||||
label: i18n.translate('xpack.apm.diagnostics.tab.indices', {
|
||||
defaultMessage: 'Indices',
|
||||
}),
|
||||
isSelected: routePath === '/diagnostics/indices',
|
||||
isHidden: isCrossCluster || !hasAllClusterPrivileges,
|
||||
},
|
||||
{
|
||||
'data-test-subj': 'documents-tab',
|
||||
href: router.link('/diagnostics/documents', { query }),
|
||||
label: i18n.translate('xpack.apm.diagnostics.tab.apmEvents', {
|
||||
defaultMessage: 'Documents',
|
||||
}),
|
||||
isSelected: routePath === '/diagnostics/documents',
|
||||
},
|
||||
{
|
||||
'data-test-subj': 'import-export-tab',
|
||||
href: router.link('/diagnostics/import-export', { query }),
|
||||
label: i18n.translate('xpack.apm.diagnostics.tab.import_export', {
|
||||
defaultMessage: 'Import/Export',
|
||||
}),
|
||||
isSelected: routePath === '/diagnostics/import-export',
|
||||
},
|
||||
].filter((tab) => !tab.isHidden);
|
||||
|
||||
return (
|
||||
<ApmMainTemplate
|
||||
data-test-subj="apmDiagnosticsTemplate"
|
||||
pageTitle="Diagnostics"
|
||||
environmentFilter={false}
|
||||
showServiceGroupSaveButton={false}
|
||||
|
@ -103,79 +209,7 @@ function DiagnosticsTemplate({ children }: { children: React.ReactChild }) {
|
|||
iconType: 'magnifyWithExclamation',
|
||||
rightSideItems: [<RefreshButton />],
|
||||
description: <TemplateDescription />,
|
||||
tabs: [
|
||||
{
|
||||
'data-test-subj': 'summary-tab',
|
||||
href: router.link('/diagnostics', { query }),
|
||||
label: i18n.translate('xpack.apm.diagnostics.tab.summary', {
|
||||
defaultMessage: 'Summary',
|
||||
}),
|
||||
isSelected: routePath === '/diagnostics',
|
||||
},
|
||||
{
|
||||
'data-test-subj': 'index-pattern-tab',
|
||||
prepend: !getIndexPatternTabStatus(diagnosticsBundle) && (
|
||||
<EuiIcon type="warning" color="red" />
|
||||
),
|
||||
href: router.link('/diagnostics/index-pattern-settings', { query }),
|
||||
label: i18n.translate(
|
||||
'xpack.apm.diagnostics.tab.index_pattern_settings',
|
||||
{
|
||||
defaultMessage: 'Index pattern settings',
|
||||
}
|
||||
),
|
||||
isSelected: routePath === '/diagnostics/index-pattern-settings',
|
||||
},
|
||||
{
|
||||
'data-test-subj': 'index-templates-tab',
|
||||
prepend: !getIndexTemplateStatus(diagnosticsBundle) && (
|
||||
<EuiIcon type="warning" color="red" />
|
||||
),
|
||||
href: router.link('/diagnostics/index-templates', { query }),
|
||||
label: i18n.translate('xpack.apm.diagnostics.tab.index_templates', {
|
||||
defaultMessage: 'Index templates',
|
||||
}),
|
||||
isSelected: routePath === '/diagnostics/index-templates',
|
||||
},
|
||||
{
|
||||
'data-test-subj': 'data-streams-tab',
|
||||
prepend: !getDataStreamTabStatus(diagnosticsBundle) && (
|
||||
<EuiIcon type="warning" color="red" />
|
||||
),
|
||||
href: router.link('/diagnostics/data-streams', { query }),
|
||||
label: i18n.translate('xpack.apm.diagnostics.tab.datastreams', {
|
||||
defaultMessage: 'Data streams',
|
||||
}),
|
||||
isSelected: routePath === '/diagnostics/data-streams',
|
||||
},
|
||||
{
|
||||
'data-test-subj': 'indices-tab',
|
||||
prepend: !getIndicesTabStatus(diagnosticsBundle) && (
|
||||
<EuiIcon type="warning" color="red" />
|
||||
),
|
||||
href: router.link('/diagnostics/indices', { query }),
|
||||
label: i18n.translate('xpack.apm.diagnostics.tab.indices', {
|
||||
defaultMessage: 'Indices',
|
||||
}),
|
||||
isSelected: routePath === '/diagnostics/indices',
|
||||
},
|
||||
{
|
||||
'data-test-subj': 'documents-tab',
|
||||
href: router.link('/diagnostics/documents', { query }),
|
||||
label: i18n.translate('xpack.apm.diagnostics.tab.apmEvents', {
|
||||
defaultMessage: 'Documents',
|
||||
}),
|
||||
isSelected: routePath === '/diagnostics/documents',
|
||||
},
|
||||
{
|
||||
'data-test-subj': 'import-export-tab',
|
||||
href: router.link('/diagnostics/import-export', { query }),
|
||||
label: i18n.translate('xpack.apm.diagnostics.tab.import_export', {
|
||||
defaultMessage: 'Import/Export',
|
||||
}),
|
||||
isSelected: routePath === '/diagnostics/import-export',
|
||||
},
|
||||
],
|
||||
tabs,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -184,13 +218,20 @@ function DiagnosticsTemplate({ children }: { children: React.ReactChild }) {
|
|||
}
|
||||
|
||||
function TemplateDescription() {
|
||||
const { isImported } = useDiagnosticsContext();
|
||||
const { isImported, setImportedDiagnosticsBundle } = useDiagnosticsContext();
|
||||
if (isImported) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
title="Displaying results from the uploaded diagnostics report"
|
||||
iconType="exportAction"
|
||||
/>
|
||||
>
|
||||
<EuiButton
|
||||
data-test-subj="apmTemplateDescriptionClearBundleButton"
|
||||
onClick={() => setImportedDiagnosticsBundle(undefined)}
|
||||
>
|
||||
Clear bundle
|
||||
</EuiButton>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -102,9 +102,7 @@ export function DiagnosticsIndexPatternSettings() {
|
|||
);
|
||||
}
|
||||
|
||||
export function getIndexPatternTabStatus(
|
||||
diagnosticsBundle?: DiagnosticsBundle
|
||||
) {
|
||||
export function getIsIndexPatternTabOk(diagnosticsBundle?: DiagnosticsBundle) {
|
||||
if (!diagnosticsBundle) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -22,12 +22,12 @@ export function DataStreamsStatus() {
|
|||
const router = useApmRouter();
|
||||
const { query } = useApmParams('/diagnostics/*');
|
||||
const isLoading = status === FETCH_STATUS.LOADING;
|
||||
const tabStatus = getDataStreamTabStatus(diagnosticsBundle);
|
||||
const isOk = getIsDataStreamTabOk(diagnosticsBundle);
|
||||
|
||||
return (
|
||||
<TabStatus
|
||||
isLoading={isLoading}
|
||||
isOk={tabStatus}
|
||||
isOk={isOk}
|
||||
data-test-subj="dataStreamsStatus"
|
||||
>
|
||||
Data streams
|
||||
|
@ -41,9 +41,9 @@ export function DataStreamsStatus() {
|
|||
);
|
||||
}
|
||||
|
||||
export function getDataStreamTabStatus(diagnosticsBundle?: DiagnosticsBundle) {
|
||||
export function getIsDataStreamTabOk(diagnosticsBundle?: DiagnosticsBundle) {
|
||||
if (!diagnosticsBundle) {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return diagnosticsBundle.dataStreams.every((ds) => {
|
||||
|
|
|
@ -6,27 +6,41 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiCallOut } from '@elastic/eui';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiCallOut,
|
||||
EuiDescriptionList,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
|
||||
import { ApmIntegrationPackageStatus } from './apm_integration_package_status';
|
||||
import { IndexTemplatesStatus } from './index_templates_status';
|
||||
import { FieldMappingStatus } from './indicies_status';
|
||||
import { DataStreamsStatus } from './data_streams_status';
|
||||
import { useDiagnosticsContext } from '../context/use_diagnostics';
|
||||
|
||||
type DiagnosticsBundle = APIReturnType<'GET /internal/apm/diagnostics'>;
|
||||
|
||||
export function DiagnosticsSummary() {
|
||||
const { diagnosticsBundle } = useDiagnosticsContext();
|
||||
const isCrossCluster = getIsCrossCluster(diagnosticsBundle);
|
||||
const hasAllPrivileges =
|
||||
diagnosticsBundle?.diagnosticsPrivileges.hasAllPrivileges ?? true;
|
||||
|
||||
const isCrossCluster = Object.values(
|
||||
diagnosticsBundle?.apmIndices ?? {}
|
||||
).some((indicies) => indicies.includes(':'));
|
||||
|
||||
if (isCrossCluster) {
|
||||
if (isCrossCluster || !hasAllPrivileges) {
|
||||
return (
|
||||
<EuiCallOut title="Cross cluster search not supported" color="warning">
|
||||
The APM index settings is targetting remote clusters. Please note: this
|
||||
is not currently supported by the Diagnostics Tool and functionality
|
||||
will therefore be limited.
|
||||
</EuiCallOut>
|
||||
<>
|
||||
{isCrossCluster && (
|
||||
<>
|
||||
<CrossClusterSearchCallout />
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)}
|
||||
{diagnosticsBundle && !hasAllPrivileges && (
|
||||
<PrivilegesCallout diagnosticsBundle={diagnosticsBundle} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -39,3 +53,68 @@ export function DiagnosticsSummary() {
|
|||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function CrossClusterSearchCallout() {
|
||||
return (
|
||||
<EuiCallOut title="Cross cluster search not supported" color="warning">
|
||||
The APM index settings is targetting remote clusters. Please note that
|
||||
this is not currently supported by the Diagnostics Tool and functionality
|
||||
will therefore be limited.
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
||||
function PrivilegesCallout({
|
||||
diagnosticsBundle,
|
||||
}: {
|
||||
diagnosticsBundle: DiagnosticsBundle;
|
||||
}) {
|
||||
const missingClusterPrivileges = Object.entries(
|
||||
diagnosticsBundle.diagnosticsPrivileges.cluster
|
||||
)
|
||||
.filter(([privilegeName, hasPrivilege]) => !hasPrivilege)
|
||||
.map(([privilegeName]) => privilegeName);
|
||||
|
||||
const missingIndexPrivileges = Object.entries(
|
||||
diagnosticsBundle.diagnosticsPrivileges.index
|
||||
)
|
||||
.filter(([indexName, privObject]) => !privObject.read)
|
||||
.map(([indexName, privObject]) => indexName);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiCallOut title="Insufficient access" color="warning">
|
||||
Not all features are available due to missing privileges.
|
||||
<br />
|
||||
<br />
|
||||
<EuiDescriptionList
|
||||
listItems={[
|
||||
...(missingClusterPrivileges.length > 0
|
||||
? [
|
||||
{
|
||||
title: 'Missing cluster privileges',
|
||||
description: missingClusterPrivileges.join(', '),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
...(missingIndexPrivileges.length > 0
|
||||
? [
|
||||
{
|
||||
title: 'Missing index privileges',
|
||||
description: missingIndexPrivileges.join(', '),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</EuiCallOut>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function getIsCrossCluster(diagnosticsBundle?: DiagnosticsBundle) {
|
||||
return Object.values(diagnosticsBundle?.apmIndices ?? {}).some((indicies) =>
|
||||
indicies.includes(':')
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ export function IndexTemplatesStatus() {
|
|||
const { query } = useApmParams('/diagnostics/*');
|
||||
const { diagnosticsBundle, status } = useDiagnosticsContext();
|
||||
const isLoading = status === FETCH_STATUS.LOADING;
|
||||
const tabStatus = getIndexTemplateStatus(diagnosticsBundle);
|
||||
const tabStatus = getIsIndexTemplateOk(diagnosticsBundle);
|
||||
|
||||
return (
|
||||
<TabStatus
|
||||
|
@ -39,14 +39,18 @@ export function IndexTemplatesStatus() {
|
|||
);
|
||||
}
|
||||
|
||||
export function getIndexTemplateStatus(diagnosticsBundle?: DiagnosticsBundle) {
|
||||
export function getIsIndexTemplateOk(diagnosticsBundle?: DiagnosticsBundle) {
|
||||
if (!diagnosticsBundle) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hasNonStandardIndexTemplates =
|
||||
diagnosticsBundle?.apmIndexTemplates?.some(
|
||||
diagnosticsBundle.apmIndexTemplates?.some(
|
||||
({ isNonStandard }) => isNonStandard
|
||||
);
|
||||
|
||||
const isEveryExpectedApmIndexTemplateInstalled =
|
||||
diagnosticsBundle?.apmIndexTemplates.every(
|
||||
diagnosticsBundle.apmIndexTemplates.every(
|
||||
({ exists, isNonStandard }) => isNonStandard || exists
|
||||
);
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ export function FieldMappingStatus() {
|
|||
const { query } = useApmParams('/diagnostics/*');
|
||||
const { diagnosticsBundle, status } = useDiagnosticsContext();
|
||||
const isLoading = status === FETCH_STATUS.LOADING;
|
||||
const isOk = getIndicesTabStatus(diagnosticsBundle);
|
||||
const isOk = getIsIndicesTabOk(diagnosticsBundle);
|
||||
|
||||
return (
|
||||
<TabStatus
|
||||
|
@ -40,6 +40,10 @@ export function FieldMappingStatus() {
|
|||
);
|
||||
}
|
||||
|
||||
export function getIndicesTabStatus(diagnosticsBundle?: DiagnosticsBundle) {
|
||||
return diagnosticsBundle?.invalidIndices.length === 0;
|
||||
export function getIsIndicesTabOk(diagnosticsBundle?: DiagnosticsBundle) {
|
||||
if (!diagnosticsBundle) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return diagnosticsBundle.invalidIndices.length === 0;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import { IndicesSimulateTemplateResponse } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { orderBy } from 'lodash';
|
||||
import { errors } from '@elastic/elasticsearch';
|
||||
import { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices';
|
||||
import { getApmIndexPatterns } from './get_indices';
|
||||
import { getIndexTemplate } from './get_index_template';
|
||||
|
@ -27,16 +28,13 @@ export async function getIndexTemplatesByIndexPattern({
|
|||
apmIndices.transaction,
|
||||
]);
|
||||
|
||||
try {
|
||||
return await Promise.all(
|
||||
return await handleInvalidIndexTemplateException(
|
||||
Promise.all(
|
||||
indexPatterns.map(async (indexPattern) =>
|
||||
getSimulatedIndexTemplateForIndexPattern({ indexPattern, esClient })
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return [];
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function getSimulatedIndexTemplateForIndexPattern({
|
||||
|
@ -95,3 +93,23 @@ function getIsNonStandardIndexTemplate(templateName: string) {
|
|||
|
||||
return isNonStandard;
|
||||
}
|
||||
|
||||
async function handleInvalidIndexTemplateException<T>(promise: Promise<T>) {
|
||||
try {
|
||||
return await promise;
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof errors.ResponseError &&
|
||||
error.meta.statusCode === 400 &&
|
||||
// @ts-expect-error
|
||||
error.meta.body.error.type === 'invalid_index_template_exception'
|
||||
) {
|
||||
console.error(
|
||||
`Suppressed exception caused by cross cluster search: ${error.message}}`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,29 +5,33 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
FieldCapsResponse,
|
||||
IndicesGetResponse,
|
||||
IngestGetPipelineResponse,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import { ApmIndicesConfig } from '@kbn/observability-plugin/common/typings';
|
||||
import { SERVICE_NAME } from '../../../../common/es_fields/apm';
|
||||
import { getApmIndexTemplateNames } from '../helpers/get_apm_index_template_names';
|
||||
import { getFieldCaps } from './get_field_caps';
|
||||
import { getIndicesAndIngestPipelines } from './get_indices';
|
||||
|
||||
export function getIndicesStates({
|
||||
indices,
|
||||
fieldCaps,
|
||||
ingestPipelines,
|
||||
export async function getIndicesStates({
|
||||
esClient,
|
||||
apmIndices,
|
||||
}: {
|
||||
indices: IndicesGetResponse;
|
||||
fieldCaps: FieldCapsResponse;
|
||||
ingestPipelines: IngestGetPipelineResponse;
|
||||
esClient: ElasticsearchClient;
|
||||
apmIndices: ApmIndicesConfig;
|
||||
}) {
|
||||
const { indices, ingestPipelines } = await getIndicesAndIngestPipelines({
|
||||
esClient,
|
||||
apmIndices,
|
||||
});
|
||||
|
||||
const indicesWithPipelineId = Object.entries(indices).map(([key, value]) => ({
|
||||
index: key,
|
||||
dataStream: value.data_stream,
|
||||
pipelineId: value.settings?.index?.default_pipeline,
|
||||
}));
|
||||
|
||||
const fieldCaps = await getFieldCaps({ esClient, apmIndices });
|
||||
|
||||
const invalidFieldMappings = Object.values(
|
||||
fieldCaps.fields[SERVICE_NAME] ?? {}
|
||||
).filter(({ type }): boolean => type !== 'keyword');
|
||||
|
@ -72,7 +76,7 @@ export function getIndicesStates({
|
|||
const invalidIndices = items.filter((item) => !item.isValid);
|
||||
const validIndices = items.filter((item) => item.isValid);
|
||||
|
||||
return { invalidIndices, validIndices };
|
||||
return { invalidIndices, validIndices, indices, ingestPipelines, fieldCaps };
|
||||
}
|
||||
|
||||
export function validateIngestPipelineName(
|
||||
|
|
|
@ -12,11 +12,11 @@ import { getNonDataStreamIndices } from './bundle/get_non_data_stream_indices';
|
|||
import { getElasticsearchVersion } from './get_elasticsearch_version';
|
||||
import { getIndexTemplatesByIndexPattern } from './bundle/get_index_templates_by_index_pattern';
|
||||
import { getExistingApmIndexTemplates } from './bundle/get_existing_index_templates';
|
||||
import { getFieldCaps } from './bundle/get_field_caps';
|
||||
import { getIndicesAndIngestPipelines } from './bundle/get_indices';
|
||||
import { getIndicesStates } from './bundle/get_indices_states';
|
||||
import { getApmEvents } from './bundle/get_apm_events';
|
||||
import { getApmIndexTemplates } from './helpers/get_apm_index_template_names';
|
||||
import { handle403Exception } from './helpers/handle_403_exception';
|
||||
import { getDiagnosticsPrivileges } from './helpers/get_diagnostic_privileges';
|
||||
|
||||
const DEFEAULT_START = Date.now() - 60 * 5 * 1000; // 5 minutes
|
||||
const DEFAULT_END = Date.now();
|
||||
|
@ -34,45 +34,73 @@ export async function getDiagnosticsBundle({
|
|||
end: number | undefined;
|
||||
kuery: string | undefined;
|
||||
}) {
|
||||
const { indices, ingestPipelines } = await getIndicesAndIngestPipelines({
|
||||
const diagnosticsPrivileges = await getDiagnosticsPrivileges({
|
||||
esClient,
|
||||
apmIndices,
|
||||
});
|
||||
|
||||
const indexTemplatesByIndexPattern = await getIndexTemplatesByIndexPattern({
|
||||
esClient,
|
||||
apmIndices,
|
||||
});
|
||||
const indexTemplatesByIndexPattern = await handle403Exception(
|
||||
getIndexTemplatesByIndexPattern({
|
||||
esClient,
|
||||
apmIndices,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const existingIndexTemplates = await getExistingApmIndexTemplates({
|
||||
esClient,
|
||||
});
|
||||
const existingIndexTemplates = await handle403Exception(
|
||||
getExistingApmIndexTemplates({
|
||||
esClient,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const fieldCaps = await getFieldCaps({ esClient, apmIndices });
|
||||
const dataStreams = await getDataStreams({ esClient, apmIndices });
|
||||
const nonDataStreamIndices = await getNonDataStreamIndices({
|
||||
esClient,
|
||||
apmIndices,
|
||||
});
|
||||
const dataStreams = await handle403Exception(
|
||||
getDataStreams({ esClient, apmIndices }),
|
||||
[]
|
||||
);
|
||||
const nonDataStreamIndices = await handle403Exception(
|
||||
getNonDataStreamIndices({
|
||||
esClient,
|
||||
apmIndices,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const { invalidIndices, validIndices } = getIndicesStates({
|
||||
fieldCaps,
|
||||
indices,
|
||||
ingestPipelines,
|
||||
});
|
||||
const { invalidIndices, validIndices, indices, ingestPipelines, fieldCaps } =
|
||||
await handle403Exception(
|
||||
getIndicesStates({
|
||||
esClient,
|
||||
apmIndices,
|
||||
}),
|
||||
{
|
||||
invalidIndices: [],
|
||||
validIndices: [],
|
||||
indices: [],
|
||||
ingestPipelines: [],
|
||||
fieldCaps: {},
|
||||
}
|
||||
);
|
||||
|
||||
const apmEvents = await getApmEvents({
|
||||
esClient,
|
||||
apmIndices,
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
});
|
||||
const apmEvents = await handle403Exception(
|
||||
getApmEvents({
|
||||
esClient,
|
||||
apmIndices,
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const elasticsearchVersion = await handle403Exception(
|
||||
getElasticsearchVersion(esClient),
|
||||
'N/A'
|
||||
);
|
||||
|
||||
return {
|
||||
created_at: new Date().toISOString(),
|
||||
diagnosticsPrivileges,
|
||||
apmIndices,
|
||||
elasticsearchVersion: await getElasticsearchVersion(esClient),
|
||||
elasticsearchVersion,
|
||||
esResponses: {
|
||||
fieldCaps,
|
||||
indices,
|
||||
|
|
|
@ -5,16 +5,29 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FleetUnauthorizedError } from '@kbn/fleet-plugin/server/errors';
|
||||
import { APMRouteHandlerResources } from '../typings';
|
||||
|
||||
export async function getFleetPackageInfo(resources: APMRouteHandlerResources) {
|
||||
const fleetPluginStart = await resources.plugins.fleet?.start();
|
||||
const packageInfo = await fleetPluginStart?.packageService
|
||||
.asScoped(resources.request)
|
||||
.getInstallation('apm');
|
||||
|
||||
return {
|
||||
isInstalled: packageInfo?.install_status === 'installed',
|
||||
version: packageInfo?.version,
|
||||
};
|
||||
try {
|
||||
const packageInfo = await fleetPluginStart?.packageService
|
||||
.asScoped(resources.request)
|
||||
.getInstallation('apm');
|
||||
|
||||
return {
|
||||
isInstalled: packageInfo?.install_status === 'installed',
|
||||
version: packageInfo?.version,
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof FleetUnauthorizedError) {
|
||||
console.error('Insufficient permissions to access fleet package info');
|
||||
return {
|
||||
isInstalled: false,
|
||||
version: 'N/A',
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices';
|
||||
import { getApmIndexPatterns } from '../bundle/get_indices';
|
||||
|
||||
export async function getDiagnosticsPrivileges({
|
||||
esClient,
|
||||
apmIndices,
|
||||
}: {
|
||||
esClient: ElasticsearchClient;
|
||||
apmIndices: ApmIndicesConfig;
|
||||
}) {
|
||||
const indexPatterns = getApmIndexPatterns([
|
||||
apmIndices.error,
|
||||
apmIndices.metric,
|
||||
apmIndices.span,
|
||||
apmIndices.transaction,
|
||||
]);
|
||||
|
||||
const clusterPrivileges = [
|
||||
'manage_index_templates',
|
||||
'monitor',
|
||||
'read_pipeline',
|
||||
];
|
||||
const { index, cluster } = await esClient.security.hasPrivileges({
|
||||
body: {
|
||||
index: [
|
||||
{
|
||||
names: indexPatterns,
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
cluster: clusterPrivileges,
|
||||
},
|
||||
});
|
||||
|
||||
const hasAllIndexPrivileges = Object.values(index).every((indexPrivs) =>
|
||||
Object.values(indexPrivs).every((priv) => priv)
|
||||
);
|
||||
|
||||
const hasAllClusterPrivileges = Object.values(cluster).every((priv) => priv);
|
||||
|
||||
return {
|
||||
index,
|
||||
cluster,
|
||||
hasAllIndexPrivileges,
|
||||
hasAllClusterPrivileges,
|
||||
hasAllPrivileges: hasAllIndexPrivileges && hasAllClusterPrivileges,
|
||||
};
|
||||
}
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { errors } from '@elastic/elasticsearch';
|
||||
|
||||
export async function handle403Exception<T>(
|
||||
promise: Promise<T>,
|
||||
defaultValue: unknown
|
||||
) {
|
||||
try {
|
||||
return await promise;
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof errors.ResponseError &&
|
||||
error.meta.statusCode === 403
|
||||
) {
|
||||
console.error(`Suppressed insufficient access error: ${error.message}}`);
|
||||
return defaultValue as T;
|
||||
}
|
||||
|
||||
console.error(
|
||||
`Unhandled error: ${error.message} ${JSON.stringify(error)}}`
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import {
|
|||
IndicesGetIndexTemplateIndexTemplateItem,
|
||||
IndicesGetResponse,
|
||||
IngestGetPipelineResponse,
|
||||
SecurityHasPrivilegesPrivileges,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import * as t from 'io-ts';
|
||||
import { isoToEpochRt } from '@kbn/io-ts-utils';
|
||||
|
@ -56,6 +57,13 @@ const getDiagnosticsRoute = createApmServerRoute({
|
|||
indices: IndicesGetResponse;
|
||||
ingestPipelines: IngestGetPipelineResponse;
|
||||
};
|
||||
diagnosticsPrivileges: {
|
||||
index: Record<string, SecurityHasPrivilegesPrivileges>;
|
||||
cluster: Record<string, boolean>;
|
||||
hasAllClusterPrivileges: boolean;
|
||||
hasAllIndexPrivileges: boolean;
|
||||
hasAllPrivileges: boolean;
|
||||
};
|
||||
apmIndices: ApmIndicesConfig;
|
||||
apmIndexTemplates: Array<{
|
||||
name: string;
|
||||
|
|
|
@ -39370,4 +39370,4 @@
|
|||
"xpack.painlessLab.title": "Painless Lab",
|
||||
"xpack.painlessLab.walkthroughButtonLabel": "Présentation"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39344,4 +39344,4 @@
|
|||
"xpack.painlessLab.title": "Painless Lab",
|
||||
"xpack.painlessLab.walkthroughButtonLabel": "実地検証"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39338,4 +39338,4 @@
|
|||
"xpack.painlessLab.title": "Painless 实验室",
|
||||
"xpack.painlessLab.walkthroughButtonLabel": "指导"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('returns zero data streams`', async () => {
|
||||
const { status, body } = await apmApiClient.adminUser({
|
||||
const { status, body } = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/diagnostics',
|
||||
});
|
||||
expect(status).to.be(200);
|
||||
|
@ -59,7 +59,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
it('returns zero doc_counts when no time range is specified', async () => {
|
||||
const { body } = await apmApiClient.adminUser({
|
||||
const { body } = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/diagnostics',
|
||||
});
|
||||
|
||||
|
@ -67,7 +67,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('returns non-zero doc_counts when time range is specified', async () => {
|
||||
const { body } = await apmApiClient.adminUser({
|
||||
const { body } = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/diagnostics',
|
||||
params: {
|
||||
query: { start: new Date(start).toISOString(), end: new Date(end).toISOString() },
|
||||
|
@ -105,7 +105,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
const expectedDocCount = 450;
|
||||
|
||||
beforeEach(async () => {
|
||||
const res = await apmApiClient.adminUser({
|
||||
const res = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/diagnostics',
|
||||
params: {
|
||||
query: { start: new Date(start).toISOString(), end: new Date(end).toISOString() },
|
||||
|
@ -168,7 +168,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('returns zero doc_counts when filtering by a non-existing service', async () => {
|
||||
const { body } = await apmApiClient.adminUser({
|
||||
const { body } = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/diagnostics',
|
||||
params: {
|
||||
query: {
|
||||
|
@ -183,7 +183,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('returns non-zero doc_counts when filtering by an existing service', async () => {
|
||||
const { body } = await apmApiClient.adminUser({
|
||||
const { body } = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/diagnostics',
|
||||
params: {
|
||||
query: {
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
|
||||
registry.when('Diagnostics: Privileges', { config: 'basic', archives: [] }, () => {
|
||||
describe('superuser', () => {
|
||||
let body: APIReturnType<'GET /internal/apm/diagnostics'>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const res = await apmApiClient.adminUser({
|
||||
endpoint: 'GET /internal/apm/diagnostics',
|
||||
});
|
||||
body = res.body;
|
||||
expect(res.status).to.be(200);
|
||||
});
|
||||
|
||||
it('has all privileges', async () => {
|
||||
expect(body.diagnosticsPrivileges.hasAllPrivileges).to.be(true);
|
||||
});
|
||||
|
||||
it('has all index privileges', async () => {
|
||||
expect(body.diagnosticsPrivileges.hasAllIndexPrivileges).to.be(true);
|
||||
expect(body.diagnosticsPrivileges.index).to.eql({
|
||||
'apm-*': { read: true },
|
||||
'logs-apm*': { read: true },
|
||||
'metrics-apm*': { read: true },
|
||||
'traces-apm*': { read: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('has all cluster privileges', async () => {
|
||||
expect(body.diagnosticsPrivileges.hasAllClusterPrivileges).to.be(true);
|
||||
expect(body.diagnosticsPrivileges.cluster).to.eql({
|
||||
read_pipeline: true,
|
||||
manage_index_templates: true,
|
||||
monitor: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('viewer', () => {
|
||||
let body: APIReturnType<'GET /internal/apm/diagnostics'>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const res = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/diagnostics',
|
||||
});
|
||||
body = res.body;
|
||||
expect(res.status).to.be(200);
|
||||
});
|
||||
|
||||
it('does not have all privileges', async () => {
|
||||
expect(body.diagnosticsPrivileges.hasAllPrivileges).to.be(false);
|
||||
});
|
||||
|
||||
it('has all index privileges', async () => {
|
||||
expect(body.diagnosticsPrivileges.hasAllIndexPrivileges).to.be(true);
|
||||
expect(body.diagnosticsPrivileges.index).to.eql({
|
||||
'apm-*': { read: true },
|
||||
'logs-apm*': { read: true },
|
||||
'metrics-apm*': { read: true },
|
||||
'traces-apm*': { read: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('does not have any cluster privileges', async () => {
|
||||
expect(body.diagnosticsPrivileges.hasAllClusterPrivileges).to.be(false);
|
||||
expect(body.diagnosticsPrivileges.cluster).to.eql({
|
||||
read_pipeline: false,
|
||||
manage_index_templates: false,
|
||||
monitor: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue