Skip to content

Commit

Permalink
[Search] Implement RBAC Kibana feature for Search (elastic#192130)
Browse files Browse the repository at this point in the history
## Summary

This implements RBAC properly for Search. We no longer rely on the
enterprise search node to limit access to Search, so except for App
Search and Workplace Search. That means we need proper Kibana features
to manage RBAC, which this adds: one for Search and a separate one for
Relevance as that feature is guarded by an Enterprise license.

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
sphilipse and kibanamachine authored Sep 12, 2024
1 parent 99057f8 commit f7c708f
Show file tree
Hide file tree
Showing 23 changed files with 177 additions and 191 deletions.
4 changes: 2 additions & 2 deletions x-pack/plugins/enterprise_search/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export const SEMANTIC_SEARCH_PLUGIN = {
URL: '/app/enterprise_search/semantic_search',
};

export const INFERENCE_ENDPOINTS_PLUGIN = {
export const SEARCH_RELEVANCE_PLUGIN = {
ID: ENTERPRISE_SEARCH_RELEVANCE_APP_ID,
NAME: i18n.translate('xpack.enterpriseSearch.inferenceEndpoints.productName', {
defaultMessage: 'Inference Endpoints',
Expand All @@ -203,7 +203,7 @@ export const INFERENCE_ENDPOINTS_PLUGIN = {
defaultMessage: 'Relevance',
}),
DESCRIPTION: i18n.translate('xpack.enterpriseSearch.inferenceEndpoints.description', {
defaultMessage: 'View for managing inference endpoints.',
defaultMessage: 'Manage your inference endpoints for semantic search and AI use cases.',
}),
URL: '/app/enterprise_search/relevance',
LOGO: 'logoEnterpriseSearch',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import React from 'react';

import { INFERENCE_ENDPOINTS_PLUGIN } from '../../../../../common/constants';
import { SEARCH_RELEVANCE_PLUGIN } from '../../../../../common/constants';
import { PageTemplateProps } from '../../../shared/layout';
import { NotFoundPrompt } from '../../../shared/not_found';
import { SendEnterpriseSearchTelemetry } from '../../../shared/telemetry';
Expand All @@ -17,7 +17,7 @@ export const NotFound: React.FC<PageTemplateProps> = ({ pageChrome = [] }) => {
return (
<EnterpriseSearchRelevancePageTemplate pageChrome={[...pageChrome, '404']} customPageSections>
<SendEnterpriseSearchTelemetry action="error" metric="not_found" />
<NotFoundPrompt productSupportUrl={INFERENCE_ENDPOINTS_PLUGIN.SUPPORT_URL} />
<NotFoundPrompt productSupportUrl={SEARCH_RELEVANCE_PLUGIN.SUPPORT_URL} />
</EnterpriseSearchRelevancePageTemplate>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,17 @@
* 2.0.
*/

import { setMockValues } from '../__mocks__/kea_logic';
import '../__mocks__/shallow_useeffect.mock';
import '../__mocks__/enterprise_search_url.mock';

import React from 'react';

import { shallow } from 'enzyme';

import { VersionMismatchPage } from '../shared/version_mismatch';

import { EnterpriseSearchRelevance, EnterpriseSearchRelevanceConfigured } from '.';

describe('EnterpriseSearchRelevance', () => {
it('renders VersionMismatchPage when there are mismatching versions', () => {
setMockValues({ config: { canDeployEntSearch: true, host: 'host' } });
const wrapper = shallow(
<EnterpriseSearchRelevance enterpriseSearchVersion="7.15.0" kibanaVersion="7.16.0" />
);

expect(wrapper.find(VersionMismatchPage)).toHaveLength(1);
});

it('renders EnterpriseSearchRelevanceConfigured when config.host is set & available', () => {
setMockValues({
config: { canDeployEntSearch: true, host: 'some.url' },
errorConnectingMessage: '',
});
const wrapper = shallow(<EnterpriseSearchRelevance />);

expect(wrapper.find(EnterpriseSearchRelevanceConfigured)).toHaveLength(1);
});

it('renders EnterpriseSearchRelevanceConfigured when config.host is not set & Ent Search cannot be deployed', () => {
setMockValues({ config: { canDeployEntSearch: false, host: '' }, errorConnectingMessage: '' });
it('renders EnterpriseSearchRelevanceConfigured', () => {
const wrapper = shallow(<EnterpriseSearchRelevance />);

expect(wrapper.find(EnterpriseSearchRelevanceConfigured)).toHaveLength(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,20 @@
import React from 'react';
import { Redirect } from 'react-router-dom';

import { useValues } from 'kea';

import { Route, Routes } from '@kbn/shared-ux-router';

import { isVersionMismatch } from '../../../common/is_version_mismatch';
import { InitialAppData } from '../../../common/types';
import { ErrorStatePrompt } from '../shared/error_state';
import { HttpLogic } from '../shared/http';
import { VersionMismatchPage } from '../shared/version_mismatch';

import { InferenceEndpoints } from './components/inference_endpoints';
import { NotFound } from './components/not_found';
import { INFERENCE_ENDPOINTS_PATH, ERROR_STATE_PATH, ROOT_PATH } from './routes';
import { INFERENCE_ENDPOINTS_PATH, ROOT_PATH } from './routes';

export const EnterpriseSearchRelevance: React.FC<InitialAppData> = (props) => {
const { errorConnectingMessage } = useValues(HttpLogic);
const { enterpriseSearchVersion, kibanaVersion } = props;
const incompatibleVersions = isVersionMismatch(enterpriseSearchVersion, kibanaVersion);

const showView = () => {
if (incompatibleVersions) {
return (
<VersionMismatchPage
enterpriseSearchVersion={enterpriseSearchVersion}
kibanaVersion={kibanaVersion}
/>
);
}

return <EnterpriseSearchRelevanceConfigured {...(props as Required<InitialAppData>)} />;
};

return (
<Routes>
<Route exact path={ERROR_STATE_PATH}>
{errorConnectingMessage ? <ErrorStatePrompt /> : <Redirect to={INFERENCE_ENDPOINTS_PATH} />}
<Route>
<EnterpriseSearchRelevanceConfigured {...(props as Required<InitialAppData>)} />
</Route>
<Route>{showView()}</Route>
</Routes>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
ANALYTICS_PLUGIN,
APP_SEARCH_PLUGIN,
ENTERPRISE_SEARCH_CONTENT_PLUGIN,
INFERENCE_ENDPOINTS_PLUGIN,
SEARCH_RELEVANCE_PLUGIN,
ENTERPRISE_SEARCH_PRODUCT_NAME,
AI_SEARCH_PLUGIN,
SEARCH_EXPERIENCES_PLUGIN,
Expand Down Expand Up @@ -155,7 +155,7 @@ export const useEnterpriseSearchContentBreadcrumbs = (breadcrumbs: Breadcrumbs =
]);

export const useEnterpriseSearchRelevanceBreadcrumbs = (breadcrumbs: Breadcrumbs = []) =>
useSearchBreadcrumbs([{ text: INFERENCE_ENDPOINTS_PLUGIN.NAV_TITLE, path: '/' }, ...breadcrumbs]);
useSearchBreadcrumbs([{ text: SEARCH_RELEVANCE_PLUGIN.NAV_TITLE, path: '/' }, ...breadcrumbs]);

export const useSearchExperiencesBreadcrumbs = (breadcrumbs: Breadcrumbs = []) =>
useSearchBreadcrumbs([{ text: SEARCH_EXPERIENCES_PLUGIN.NAV_TITLE, path: '/' }, ...breadcrumbs]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
AI_SEARCH_PLUGIN,
VECTOR_SEARCH_PLUGIN,
WORKPLACE_SEARCH_PLUGIN,
INFERENCE_ENDPOINTS_PLUGIN,
SEARCH_RELEVANCE_PLUGIN,
SEMANTIC_SEARCH_PLUGIN,
} from '../../../../common/constants';
import {
Expand Down Expand Up @@ -174,7 +174,7 @@ export const useEnterpriseSearchNav = (alwaysReturn = false) => {
...generateNavLink({
shouldNotCreateHref: true,
shouldShowActiveForSubroutes: true,
to: INFERENCE_ENDPOINTS_PLUGIN.URL + INFERENCE_ENDPOINTS_PATH,
to: SEARCH_RELEVANCE_PLUGIN.URL + INFERENCE_ENDPOINTS_PATH,
}),
},
],
Expand Down
64 changes: 28 additions & 36 deletions x-pack/plugins/enterprise_search/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { BehaviorSubject, firstValueFrom, Subscription } from 'rxjs';
import { BehaviorSubject, firstValueFrom } from 'rxjs';

import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
Expand All @@ -27,7 +27,6 @@ import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { i18n } from '@kbn/i18n';
import type { IndexManagementPluginStart } from '@kbn/index-management';
import { LensPublicStart } from '@kbn/lens-plugin/public';
import { ILicense } from '@kbn/licensing-plugin/public';
import { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import { MlPluginStart } from '@kbn/ml-plugin/public';
import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
Expand All @@ -54,8 +53,8 @@ import {
SEARCH_PRODUCT_NAME,
VECTOR_SEARCH_PLUGIN,
WORKPLACE_SEARCH_PLUGIN,
INFERENCE_ENDPOINTS_PLUGIN,
SEMANTIC_SEARCH_PLUGIN,
SEARCH_RELEVANCE_PLUGIN,
} from '../common/constants';
import { registerLocators } from '../common/locators';

Expand Down Expand Up @@ -142,7 +141,7 @@ const contentLinks: AppDeepLink[] = [

const relevanceLinks: AppDeepLink[] = [
{
id: 'inferenceEndpoints',
id: 'searchInferenceEndpoints',
path: `/${INFERENCE_ENDPOINTS_PATH}`,
title: i18n.translate(
'xpack.enterpriseSearch.navigation.relevanceInferenceEndpointsLinkLabel',
Expand Down Expand Up @@ -188,7 +187,6 @@ const appSearchLinks: AppDeepLink[] = [

export class EnterpriseSearchPlugin implements Plugin {
private config: ClientConfigType;
private licenseSubscription: Subscription | null = null;

constructor(initializerContext: PluginInitializerContext) {
this.config = initializerContext.config.get<ClientConfigType>();
Expand Down Expand Up @@ -264,7 +262,7 @@ export class EnterpriseSearchPlugin implements Plugin {
if (!config.ui?.enabled) {
return;
}
const { cloud, share, licensing } = plugins;
const { cloud, share } = plugins;

const useSearchHomepage =
plugins.searchHomepage && plugins.searchHomepage.isHomepageFeatureEnabled();
Expand Down Expand Up @@ -470,33 +468,29 @@ export class EnterpriseSearchPlugin implements Plugin {
title: ANALYTICS_PLUGIN.NAME,
});

this.licenseSubscription = licensing?.license$.subscribe((license: ILicense) => {
if (license.isActive && license.hasAtLeast('enterprise')) {
core.application.register({
appRoute: INFERENCE_ENDPOINTS_PLUGIN.URL,
category: DEFAULT_APP_CATEGORIES.enterpriseSearch,
deepLinks: relevanceLinks,
euiIconType: INFERENCE_ENDPOINTS_PLUGIN.LOGO,
id: INFERENCE_ENDPOINTS_PLUGIN.ID,
mount: async (params: AppMountParameters) => {
const kibanaDeps = await this.getKibanaDeps(core, params, cloud);
const { chrome, http } = kibanaDeps.core;
chrome.docTitle.change(INFERENCE_ENDPOINTS_PLUGIN.NAME);

await this.getInitialData(http);
const pluginData = this.getPluginData();

const { renderApp } = await import('./applications');
const { EnterpriseSearchRelevance } = await import(
'./applications/enterprise_search_relevance'
);

return renderApp(EnterpriseSearchRelevance, kibanaDeps, pluginData);
},
title: INFERENCE_ENDPOINTS_PLUGIN.NAV_TITLE,
visibleIn: [],
});
}
core.application.register({
appRoute: SEARCH_RELEVANCE_PLUGIN.URL,
category: DEFAULT_APP_CATEGORIES.enterpriseSearch,
deepLinks: relevanceLinks,
euiIconType: SEARCH_RELEVANCE_PLUGIN.LOGO,
id: SEARCH_RELEVANCE_PLUGIN.ID,
mount: async (params: AppMountParameters) => {
const kibanaDeps = await this.getKibanaDeps(core, params, cloud);
const { chrome, http } = kibanaDeps.core;
chrome.docTitle.change(SEARCH_RELEVANCE_PLUGIN.NAME);

await this.getInitialData(http);
const pluginData = this.getPluginData();

const { renderApp } = await import('./applications');
const { EnterpriseSearchRelevance } = await import(
'./applications/enterprise_search_relevance'
);

return renderApp(EnterpriseSearchRelevance, kibanaDeps, pluginData);
},
title: SEARCH_RELEVANCE_PLUGIN.NAV_TITLE,
visibleIn: [],
});

core.application.register({
Expand Down Expand Up @@ -674,9 +668,7 @@ export class EnterpriseSearchPlugin implements Plugin {
return {};
}

public stop() {
this.licenseSubscription?.unsubscribe();
}
public stop() {}

private updateSideNavDefinition = (items: Partial<DynamicSideNavItems>) => {
this.sideNavDynamicItems$.next({ ...this.sideNavDynamicItems$.getValue(), ...items });
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/enterprise_search/server/lib/check_access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const checkAccess = async ({
}: CheckAccess): Promise<ProductAccess> => {
const isRbacEnabled = security.authz.mode.useRbacForRequest(request);

// If security has been disabled, always hide the plugin
// If security has been disabled, always hide app search and workplace search
if (!isRbacEnabled) {
return DENY_ALL_PLUGINS;
}
Expand Down Expand Up @@ -79,7 +79,7 @@ export const checkAccess = async ({
const { hasAllRequested } = await security.authz
.checkPrivilegesWithRequest(request)
.globally({ kibana: security.authz.actions.ui.get('enterpriseSearch', 'all') });
return hasAllRequested;
return hasAllRequested || false;
} catch (err) {
if (err.statusCode === 401 || err.statusCode === 403) {
return false;
Expand Down
Loading

0 comments on commit f7c708f

Please sign in to comment.