Skip to content

Commit

Permalink
[Logs ML] Check permissions before granting access to Logs ML pages (e…
Browse files Browse the repository at this point in the history
…lastic#195278)

## 📓 Summary

Closes elastic#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
elastic#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

<img width="3008" alt="Screenshot 2024-10-07 at 17 01 17"
src="https://github.com/user-attachments/assets/bf6478ce-b007-4f15-9538-c7959c497e8a">

### User with a valid license (or Trial), but no ML privileges

<img width="3003" alt="Screenshot 2024-10-07 at 17 03 48"
src="https://github.com/user-attachments/assets/c5a82159-b4e8-4f22-9531-23d5e5a9377f">

### User with a valid license (or Trial) and only Read ML privileges

<img width="3003" alt="Screenshot 2024-10-07 at 17 04 21"
src="https://github.com/user-attachments/assets/990f4695-e07e-46a2-9214-d0de3628caf7">

### User with a valid license (or Trial) and All ML privileges, which
are the requirements to work with ML Logs features

<img width="3000" alt="Screenshot 2024-10-07 at 17 04 52"
src="https://github.com/user-attachments/assets/c9b4d832-d3c8-4337-9e17-8a220e7be084">

---------

Co-authored-by: Marco Antonio Ghiani <marcoantonio.ghiani@elastic.co>
(cherry picked from commit e0e4ec1)
  • Loading branch information
tonyghiani committed Oct 10, 2024
1 parent d9aeca0 commit 5a190f1
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { UserManagementLink } from './user_management_link';

export const MissingResultsPrivilegesPrompt: React.FunctionComponent = () => (
<EmptyPrompt
data-test-subj="logsMissingMLReadPrivileges"
title={<h2>{missingMlPrivilegesTitle}</h2>}
body={<p>{missingMlResultsPrivilegesDescription}</p>}
actions={<UserManagementLink />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { UserManagementLink } from './user_management_link';

export const MissingSetupPrivilegesPrompt: React.FunctionComponent = () => (
<EmptyPrompt
data-test-subj="logsMissingMLAllPrivileges"
title={<h2>{missingMlPrivilegesTitle}</h2>}
body={<p>{missingMlSetupPrivilegesDescription}</p>}
actions={<UserManagementLink />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -20,6 +23,28 @@ export const LogEntryCategoriesPage = () => {
},
]);

const { hasLogAnalysisReadCapabilities, hasLogAnalysisCapabilites } =
useLogAnalysisCapabilitiesContext();

if (!hasLogAnalysisCapabilites) {
return (
<SubscriptionSplashPage
data-test-subj="logsLogEntryCategoriesPage"
pageHeader={{
pageTitle: logCategoriesTitle,
}}
/>
);
}

if (!hasLogAnalysisReadCapabilities) {
return (
<CategoriesPageTemplate isEmptyState={true}>
<MissingResultsPrivilegesPrompt />
</CategoriesPageTemplate>
);
}

return (
<EuiErrorBoundary>
<LogMlJobIdFormatsShimProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();

Expand All @@ -55,22 +50,7 @@ export const LogEntryCategoriesPageContent = () => {

const { idFormats } = useLogMlJobIdFormatsShimContext();

if (!hasLogAnalysisCapabilites) {
return (
<SubscriptionSplashPage
data-test-subj="logsLogEntryCategoriesPage"
pageHeader={{
pageTitle: logCategoriesTitle,
}}
/>
);
} else if (!hasLogAnalysisReadCapabilities) {
return (
<CategoriesPageTemplate isEmptyState={true}>
<MissingResultsPrivilegesPrompt />
</CategoriesPageTemplate>
);
} else if (setupStatus.type === 'initializing') {
if (setupStatus.type === 'initializing') {
return (
<LoadingPage
message={i18n.translate('xpack.infra.logs.logEntryCategories.jobStatusLoadingMessage', {
Expand Down Expand Up @@ -115,7 +95,7 @@ export const LogEntryCategoriesPageContent = () => {

const allowedSetupModules = ['logs_ui_categories' as const];

const CategoriesPageTemplate: React.FC<LazyObservabilityPageTemplateProps> = ({
export const CategoriesPageTemplate: React.FC<LazyObservabilityPageTemplateProps> = ({
children,
...rest
}) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -19,6 +22,29 @@ export const LogEntryRatePage = () => {
text: logsAnomaliesTitle,
},
]);

const { hasLogAnalysisReadCapabilities, hasLogAnalysisCapabilites } =
useLogAnalysisCapabilitiesContext();

if (!hasLogAnalysisCapabilites) {
return (
<SubscriptionSplashPage
data-test-subj="logsLogEntryRatePage"
pageHeader={{
pageTitle: logsAnomaliesTitle,
}}
/>
);
}

if (!hasLogAnalysisReadCapabilities) {
return (
<AnomaliesPageTemplate isEmptyState={true}>
<MissingResultsPrivilegesPrompt />
</AnomaliesPageTemplate>
);
}

return (
<EuiErrorBoundary>
<LogMlJobIdFormatsShimProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -96,22 +91,7 @@ export const LogEntryRatePageContent = memo(() => {

const { idFormats } = useLogMlJobIdFormatsShimContext();

if (!hasLogAnalysisCapabilites) {
return (
<SubscriptionSplashPage
data-test-subj="logsLogEntryRatePage"
pageHeader={{
pageTitle: logsAnomaliesTitle,
}}
/>
);
} else if (!hasLogAnalysisReadCapabilities) {
return (
<AnomaliesPageTemplate isEmptyState={true}>
<MissingResultsPrivilegesPrompt />
</AnomaliesPageTemplate>
);
} else if (
if (
logEntryCategoriesSetupStatus.type === 'initializing' ||
logEntryRateSetupStatus.type === 'initializing'
) {
Expand Down Expand Up @@ -159,7 +139,7 @@ export const LogEntryRatePageContent = memo(() => {
}
});

const AnomaliesPageTemplate: React.FC<LazyObservabilityPageTemplateProps> = ({
export const AnomaliesPageTemplate: React.FC<LazyObservabilityPageTemplateProps> = ({
children,
...rest
}) => {
Expand Down
82 changes: 75 additions & 7 deletions x-pack/test/functional/apps/infra/logs/log_entry_categories_tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string[]>) => {
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();
Expand All @@ -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');
});
});
});
Expand Down
Loading

0 comments on commit 5a190f1

Please sign in to comment.