From 5a190f100a1c931763e7d6951272f2120f098cd9 Mon Sep 17 00:00:00 2001 From: Marco Antonio Ghiani Date: Thu, 10 Oct 2024 14:33:18 +0200 Subject: [PATCH] [Logs ML] Check permissions before granting access to Logs ML pages (#195278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📓 Summary Closes #191206 This work fixes issues while accessing the Logs Anomalies and Logs Categories pages due to a lack of user privileges. The privileges were correctly handled until https://github.com/elastic/kibana/pull/168234 was merged, which introduced a call to retrieve ml formats information higher in the React hierarchy before the privileges could be asserted for the logged user. This was resulting in the call failing and letting the user stack in loading states or erroneous error pages. These changes lift the license + ML read privileges checks higher in the hierarchy so we can display the right prompts before calling the ml formats API, which will resolve correctly if the user has the right privileges. ### User without valid license Screenshot 2024-10-07 at 17 01 17 ### User with a valid license (or Trial), but no ML privileges Screenshot 2024-10-07 at 17 03 48 ### User with a valid license (or Trial) and only Read ML privileges Screenshot 2024-10-07 at 17 04 21 ### User with a valid license (or Trial) and All ML privileges, which are the requirements to work with ML Logs features Screenshot 2024-10-07 at 17 04 52 --------- Co-authored-by: Marco Antonio Ghiani (cherry picked from commit e0e4ec10e3c329f933bed0a01dbeaecdf79cfa99) --- .../missing_results_privileges_prompt.tsx | 1 + .../missing_setup_privileges_prompt.tsx | 1 + .../pages/logs/log_entry_categories/page.tsx | 27 +++++- .../log_entry_categories/page_content.tsx | 28 +------ .../public/pages/logs/log_entry_rate/page.tsx | 28 ++++++- .../logs/log_entry_rate/page_content.tsx | 28 +------ .../infra/logs/log_entry_categories_tab.ts | 82 ++++++++++++++++-- .../apps/infra/logs/log_entry_rate_tab.ts | 84 +++++++++++++++++-- .../services/logs_ui/log_entry_categories.ts | 8 ++ .../services/logs_ui/log_entry_rate.ts | 8 ++ 10 files changed, 230 insertions(+), 65 deletions(-) diff --git a/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_results_privileges_prompt.tsx b/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_results_privileges_prompt.tsx index 97eeeabe8721b..dce819ffb0930 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_results_privileges_prompt.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_results_privileges_prompt.tsx @@ -16,6 +16,7 @@ import { UserManagementLink } from './user_management_link'; export const MissingResultsPrivilegesPrompt: React.FunctionComponent = () => ( {missingMlPrivilegesTitle}} body={

{missingMlResultsPrivilegesDescription}

} actions={} diff --git a/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_prompt.tsx b/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_prompt.tsx index f959c5035d1a4..4e2a360b55ceb 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_prompt.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_prompt.tsx @@ -16,6 +16,7 @@ import { UserManagementLink } from './user_management_link'; export const MissingSetupPrivilegesPrompt: React.FunctionComponent = () => ( {missingMlPrivilegesTitle}} body={

{missingMlSetupPrivilegesDescription}

} actions={} diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page.tsx b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page.tsx index f5b1e89c69e0b..650a5b119d755 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page.tsx @@ -7,8 +7,11 @@ import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; +import { MissingResultsPrivilegesPrompt } from '../../../components/logging/log_analysis_setup'; +import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis'; +import { SubscriptionSplashPage } from '../../../components/subscription_splash_content'; import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs'; -import { LogEntryCategoriesPageContent } from './page_content'; +import { CategoriesPageTemplate, LogEntryCategoriesPageContent } from './page_content'; import { LogEntryCategoriesPageProviders } from './page_providers'; import { logCategoriesTitle } from '../../../translations'; import { LogMlJobIdFormatsShimProvider } from '../shared/use_log_ml_job_id_formats_shim'; @@ -20,6 +23,28 @@ export const LogEntryCategoriesPage = () => { }, ]); + const { hasLogAnalysisReadCapabilities, hasLogAnalysisCapabilites } = + useLogAnalysisCapabilitiesContext(); + + if (!hasLogAnalysisCapabilites) { + return ( + + ); + } + + if (!hasLogAnalysisReadCapabilities) { + return ( + + + + ); + } + return ( diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page_content.tsx index c58ffc5f36e84..8059cdcb093e2 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page_content.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page_content.tsx @@ -13,14 +13,12 @@ import { isJobStatusWithResults, logEntryCategoriesJobType } from '../../../../c import { LoadingPage } from '../../../components/loading_page'; import { LogAnalysisSetupStatusUnknownPrompt, - MissingResultsPrivilegesPrompt, MissingSetupPrivilegesPrompt, } from '../../../components/logging/log_analysis_setup'; import { LogAnalysisSetupFlyout, useLogAnalysisSetupFlyoutStateContext, } from '../../../components/logging/log_analysis_setup/setup_flyout'; -import { SubscriptionSplashPage } from '../../../components/subscription_splash_content'; import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis'; import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories'; import { LogsPageTemplate } from '../shared/page_template'; @@ -33,11 +31,8 @@ const logCategoriesTitle = i18n.translate('xpack.infra.logs.logCategoriesTitle', }); export const LogEntryCategoriesPageContent = () => { - const { - hasLogAnalysisCapabilites, - hasLogAnalysisReadCapabilities, - hasLogAnalysisSetupCapabilities, - } = useLogAnalysisCapabilitiesContext(); + const { hasLogAnalysisReadCapabilities, hasLogAnalysisSetupCapabilities } = + useLogAnalysisCapabilitiesContext(); const { fetchJobStatus, setupStatus, jobStatus } = useLogEntryCategoriesModuleContext(); @@ -55,22 +50,7 @@ export const LogEntryCategoriesPageContent = () => { const { idFormats } = useLogMlJobIdFormatsShimContext(); - if (!hasLogAnalysisCapabilites) { - return ( - - ); - } else if (!hasLogAnalysisReadCapabilities) { - return ( - - - - ); - } else if (setupStatus.type === 'initializing') { + if (setupStatus.type === 'initializing') { return ( { const allowedSetupModules = ['logs_ui_categories' as const]; -const CategoriesPageTemplate: React.FC = ({ +export const CategoriesPageTemplate: React.FC = ({ children, ...rest }) => { diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page.tsx b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page.tsx index 97841745ae13a..ed46ea9dc2680 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page.tsx @@ -7,7 +7,10 @@ import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; -import { LogEntryRatePageContent } from './page_content'; +import { SubscriptionSplashPage } from '../../../components/subscription_splash_content'; +import { MissingResultsPrivilegesPrompt } from '../../../components/logging/log_analysis_setup'; +import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis'; +import { AnomaliesPageTemplate, LogEntryRatePageContent } from './page_content'; import { LogEntryRatePageProviders } from './page_providers'; import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs'; import { logsAnomaliesTitle } from '../../../translations'; @@ -19,6 +22,29 @@ export const LogEntryRatePage = () => { text: logsAnomaliesTitle, }, ]); + + const { hasLogAnalysisReadCapabilities, hasLogAnalysisCapabilites } = + useLogAnalysisCapabilitiesContext(); + + if (!hasLogAnalysisCapabilites) { + return ( + + ); + } + + if (!hasLogAnalysisReadCapabilities) { + return ( + + + + ); + } + return ( diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page_content.tsx index 3ac8d7d9137d1..350094b5df6a3 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page_content.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -18,14 +18,12 @@ import { import { LoadingPage } from '../../../components/loading_page'; import { LogAnalysisSetupStatusUnknownPrompt, - MissingResultsPrivilegesPrompt, MissingSetupPrivilegesPrompt, } from '../../../components/logging/log_analysis_setup'; import { LogAnalysisSetupFlyout, useLogAnalysisSetupFlyoutStateContext, } from '../../../components/logging/log_analysis_setup/setup_flyout'; -import { SubscriptionSplashPage } from '../../../components/subscription_splash_content'; import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis'; import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories'; import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate'; @@ -41,11 +39,8 @@ const logsAnomaliesTitle = i18n.translate('xpack.infra.logs.anomaliesPageTitle', }); export const LogEntryRatePageContent = memo(() => { - const { - hasLogAnalysisCapabilites, - hasLogAnalysisReadCapabilities, - hasLogAnalysisSetupCapabilities, - } = useLogAnalysisCapabilitiesContext(); + const { hasLogAnalysisReadCapabilities, hasLogAnalysisSetupCapabilities } = + useLogAnalysisCapabilitiesContext(); const { fetchJobStatus: fetchLogEntryCategoriesJobStatus, @@ -96,22 +91,7 @@ export const LogEntryRatePageContent = memo(() => { const { idFormats } = useLogMlJobIdFormatsShimContext(); - if (!hasLogAnalysisCapabilites) { - return ( - - ); - } else if (!hasLogAnalysisReadCapabilities) { - return ( - - - - ); - } else if ( + if ( logEntryCategoriesSetupStatus.type === 'initializing' || logEntryRateSetupStatus.type === 'initializing' ) { @@ -159,7 +139,7 @@ export const LogEntryRatePageContent = memo(() => { } }); -const AnomaliesPageTemplate: React.FC = ({ +export const AnomaliesPageTemplate: React.FC = ({ children, ...rest }) => { diff --git a/x-pack/test/functional/apps/infra/logs/log_entry_categories_tab.ts b/x-pack/test/functional/apps/infra/logs/log_entry_categories_tab.ts index 33396497fc83c..0d4a5440ebd58 100644 --- a/x-pack/test/functional/apps/infra/logs/log_entry_categories_tab.ts +++ b/x-pack/test/functional/apps/infra/logs/log_entry_categories_tab.ts @@ -9,14 +9,54 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -export default ({ getService }: FtrProviderContext) => { +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const PageObjects = getPageObjects(['security']); const esArchiver = getService('esArchiver'); const logsUi = getService('logsUi'); const retry = getService('retry'); + const security = getService('security'); describe('Log Entry Categories Tab', function () { this.tags('includeFirefox'); + const loginWithMLPrivileges = async (privileges: Record) => { + await security.role.create('global_logs_role', { + elasticsearch: { + cluster: ['all'], + indices: [{ names: ['*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + logs: ['read'], + ...privileges, + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_logs_read_user', { + password: 'global_logs_read_user-password', + roles: ['global_logs_role'], + full_name: 'logs test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login('global_logs_read_user', 'global_logs_read_user-password', { + expectSpaceSelector: false, + }); + }; + + const logoutAndDeleteUser = async () => { + await PageObjects.security.forceLogout(); + await Promise.all([ + security.role.delete('global_logs_role'), + security.user.delete('global_logs_read_user'), + ]); + }; + describe('with a trial license', () => { it('Shows no data page when indices do not exist', async () => { await logsUi.logEntryCategoriesPage.navigateTo(); @@ -26,14 +66,42 @@ export default ({ getService }: FtrProviderContext) => { }); }); - it('shows setup page when indices exist', async () => { - await esArchiver.load('x-pack/test/functional/es_archives/infra/simple_logs'); - await logsUi.logEntryCategoriesPage.navigateTo(); + describe('when indices exists', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); + }); - await retry.try(async () => { - expect(await logsUi.logEntryCategoriesPage.getSetupScreen()).to.be.ok(); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs'); + }); + + it('shows setup page when indices exist', async () => { + await logsUi.logEntryCategoriesPage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryCategoriesPage.getSetupScreen()).to.be.ok(); + }); + }); + + it('shows required ml read privileges prompt when the user has not any ml privileges', async () => { + await loginWithMLPrivileges({}); + await logsUi.logEntryCategoriesPage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryCategoriesPage.getNoMlReadPrivilegesPrompt()).to.be.ok(); + }); + await logoutAndDeleteUser(); + }); + + it('shows required ml all privileges prompt when the user has only ml read privileges', async () => { + await loginWithMLPrivileges({ ml: ['read'] }); + await logsUi.logEntryCategoriesPage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryCategoriesPage.getNoMlAllPrivilegesPrompt()).to.be.ok(); + }); + await logoutAndDeleteUser(); }); - await esArchiver.unload('x-pack/test/functional/es_archives/infra/simple_logs'); }); }); }); diff --git a/x-pack/test/functional/apps/infra/logs/log_entry_rate_tab.ts b/x-pack/test/functional/apps/infra/logs/log_entry_rate_tab.ts index b2b4b5bcfc0be..35aa6ec6ca4ae 100644 --- a/x-pack/test/functional/apps/infra/logs/log_entry_rate_tab.ts +++ b/x-pack/test/functional/apps/infra/logs/log_entry_rate_tab.ts @@ -9,16 +9,56 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -export default ({ getService }: FtrProviderContext) => { +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const PageObjects = getPageObjects(['security']); const logsUi = getService('logsUi'); const retry = getService('retry'); const esArchiver = getService('esArchiver'); + const security = getService('security'); describe('Log Entry Rate Tab', function () { this.tags('includeFirefox'); + const loginWithMLPrivileges = async (privileges: Record) => { + await security.role.create('global_logs_role', { + elasticsearch: { + cluster: ['all'], + indices: [{ names: ['*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + logs: ['read'], + ...privileges, + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_logs_read_user', { + password: 'global_logs_read_user-password', + roles: ['global_logs_role'], + full_name: 'logs test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login('global_logs_read_user', 'global_logs_read_user-password', { + expectSpaceSelector: false, + }); + }; + + const logoutAndDeleteUser = async () => { + await PageObjects.security.forceLogout(); + await Promise.all([ + security.role.delete('global_logs_role'), + security.user.delete('global_logs_read_user'), + ]); + }; + describe('with a trial license', () => { - it('Shows no data page when indices do not exist', async () => { + it('shows no data page when indices do not exist', async () => { await logsUi.logEntryRatePage.navigateTo(); await retry.try(async () => { @@ -26,14 +66,42 @@ export default ({ getService }: FtrProviderContext) => { }); }); - it('shows setup page when indices exist', async () => { - await esArchiver.load('x-pack/test/functional/es_archives/infra/simple_logs'); - await logsUi.logEntryRatePage.navigateTo(); + describe('when indices exists', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); + }); - await retry.try(async () => { - expect(await logsUi.logEntryRatePage.getSetupScreen()).to.be.ok(); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs'); + }); + + it('shows setup page when indices exist', async () => { + await logsUi.logEntryRatePage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryRatePage.getSetupScreen()).to.be.ok(); + }); + }); + + it('shows required ml read privileges prompt when the user has not any ml privileges', async () => { + await loginWithMLPrivileges({}); + await logsUi.logEntryRatePage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryRatePage.getNoMlReadPrivilegesPrompt()).to.be.ok(); + }); + await logoutAndDeleteUser(); + }); + + it('shows required ml all privileges prompt when the user has only ml read privileges', async () => { + await loginWithMLPrivileges({ ml: ['read'] }); + await logsUi.logEntryRatePage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryRatePage.getNoMlAllPrivilegesPrompt()).to.be.ok(); + }); + await logoutAndDeleteUser(); }); - await esArchiver.unload('x-pack/test/functional/es_archives/infra/simple_logs'); }); }); }); diff --git a/x-pack/test/functional/services/logs_ui/log_entry_categories.ts b/x-pack/test/functional/services/logs_ui/log_entry_categories.ts index 77098bd918ea6..d270b510bffbd 100644 --- a/x-pack/test/functional/services/logs_ui/log_entry_categories.ts +++ b/x-pack/test/functional/services/logs_ui/log_entry_categories.ts @@ -24,5 +24,13 @@ export function LogEntryCategoriesPageProvider({ getPageObjects, getService }: F async getSetupScreen(): Promise { return await testSubjects.find('logEntryCategoriesSetupPage'); }, + + getNoMlReadPrivilegesPrompt() { + return testSubjects.find('logsMissingMLReadPrivileges'); + }, + + getNoMlAllPrivilegesPrompt() { + return testSubjects.find('logsMissingMLAllPrivileges'); + }, }; } diff --git a/x-pack/test/functional/services/logs_ui/log_entry_rate.ts b/x-pack/test/functional/services/logs_ui/log_entry_rate.ts index f8a68f6c924e0..9b704db9eb021 100644 --- a/x-pack/test/functional/services/logs_ui/log_entry_rate.ts +++ b/x-pack/test/functional/services/logs_ui/log_entry_rate.ts @@ -29,6 +29,14 @@ export function LogEntryRatePageProvider({ getPageObjects, getService }: FtrProv return await testSubjects.find('noDataPage'); }, + getNoMlReadPrivilegesPrompt() { + return testSubjects.find('logsMissingMLReadPrivileges'); + }, + + getNoMlAllPrivilegesPrompt() { + return testSubjects.find('logsMissingMLAllPrivileges'); + }, + async startJobSetup() { await testSubjects.click('infraLogEntryRateSetupContentMlSetupButton'); },