[Search] remove entsearch product access usage (#206682)

This commit is contained in:
Rodney Norris 2025-01-15 07:19:10 -06:00 committed by GitHub
parent a4c1c2066a
commit e3f54e8ce8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 3 additions and 407 deletions

View file

@ -27,10 +27,6 @@ export const DEFAULT_INITIAL_APP_DATA = {
},
},
},
access: {
hasAppSearchAccess: true,
hasWorkplaceSearchAccess: true,
},
features: {
hasConnectors: true,
hasDefaultIngestPipeline: true,

View file

@ -15,7 +15,6 @@ import {
} from './workplace_search';
export interface InitialAppData {
access?: ProductAccess;
appSearch?: AppSearchAccount;
configuredLimits?: ConfiguredLimits;
enterpriseSearchVersion?: string;
@ -31,11 +30,6 @@ export interface ConfiguredLimits {
workplaceSearch: WorkplaceSearchConfiguredLimits;
}
export interface ProductAccess {
hasAppSearchAccess: boolean;
hasWorkplaceSearchAccess: boolean;
}
export interface ProductFeatures {
hasConnectors: boolean;
hasDefaultIngestPipeline: boolean;

View file

@ -60,10 +60,6 @@ export const mockKibanaValues = {
} as unknown as LensPublicStart,
ml: mlPluginMock.createStartContract(),
navigateToUrl: jest.fn(),
productAccess: {
hasAppSearchAccess: true,
hasWorkplaceSearchAccess: true,
},
productFeatures: {
hasDocumentLevelSecurityEnabled: true,
hasIncrementalSyncEnabled: true,

View file

@ -8,7 +8,6 @@
import { kea, MakeLogicType } from 'kea';
import { Actions } from '../../../shared/api_logic/create_api_logic';
import { KibanaLogic } from '../../../shared/kibana/kibana_logic';
import {
AddConnectorApiLogic,
AddConnectorApiLogicArgs,
@ -88,15 +87,12 @@ export const NewSearchIndexLogic = kea<MakeLogicType<NewSearchIndexValues, NewSe
},
listeners: ({ actions, values }) => ({
apiIndexCreated: () => {
if (!KibanaLogic.values.productAccess.hasAppSearchAccess) return;
flashIndexCreatedToast();
},
connectorIndexCreated: () => {
if (!KibanaLogic.values.productAccess.hasAppSearchAccess) return;
flashIndexCreatedToast();
},
crawlerIndexCreated: () => {
if (!KibanaLogic.values.productAccess.hasAppSearchAccess) return;
flashIndexCreatedToast();
},
setRawName: async (_, breakpoint) => {

View file

@ -25,7 +25,7 @@ import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
import { Router } from '@kbn/shared-ux-router';
import { DEFAULT_PRODUCT_FEATURES } from '../../common/constants';
import { ClientConfigType, InitialAppData, ProductAccess } from '../../common/types';
import { ClientConfigType, InitialAppData } from '../../common/types';
import { PluginsStart, ClientData, ESConfig, UpdateSideNavDefinitionFn } from '../plugin';
import { externalUrl } from './shared/enterprise_search_url';
@ -59,7 +59,6 @@ export const renderApp = (
{ config, data, esConfig }: { config: ClientConfigType; data: ClientData; esConfig: ESConfig }
) => {
const {
access,
appSearch,
configuredLimits,
enterpriseSearchVersion,
@ -89,12 +88,6 @@ export const renderApp = (
const entCloudHost = getCloudEnterpriseSearchHost(plugins.cloud);
externalUrl.enterpriseSearchUrl = publicUrl || entCloudHost || config.host || '';
const noProductAccess: ProductAccess = {
hasAppSearchAccess: false,
hasWorkplaceSearchAccess: false,
};
const productAccess = access || noProductAccess;
const productFeatures = features ?? { ...DEFAULT_PRODUCT_FEATURES };
const EmptyContext: FC<PropsWithChildren<unknown>> = ({ children }) => <>{children}</>;
@ -128,7 +121,6 @@ export const renderApp = (
lens,
ml,
navigateToUrl,
productAccess,
productFeatures,
renderHeaderActions: (HeaderActions) =>
params.setHeaderActionMenu(
@ -170,7 +162,6 @@ export const renderApp = (
<Provider store={store}>
<Router history={params.history}>
<App
access={productAccess}
appSearch={appSearch}
configuredLimits={configuredLimits}
enterpriseSearchVersion={enterpriseSearchVersion}

View file

@ -33,7 +33,7 @@ import { ConnectorDefinition } from '@kbn/search-connectors';
import { AuthenticatedUser, SecurityPluginStart } from '@kbn/security-plugin/public';
import { SharePluginStart } from '@kbn/share-plugin/public';
import { ClientConfigType, ProductAccess, ProductFeatures } from '../../../../common/types';
import { ClientConfigType, ProductFeatures } from '../../../../common/types';
import { ESConfig, UpdateSideNavDefinitionFn } from '../../../plugin';
import { HttpLogic } from '../http';
@ -64,7 +64,6 @@ export interface KibanaLogicProps {
lens?: LensPublicStart;
ml?: MlPluginStart;
navigateToUrl: RequiredFieldsOnly<ApplicationStart['navigateToUrl']>;
productAccess: ProductAccess;
productFeatures: ProductFeatures;
renderHeaderActions(HeaderActions?: FC): void;
security?: SecurityPluginStart;
@ -100,7 +99,6 @@ export interface KibanaValues {
lens: LensPublicStart | null;
ml: MlPluginStart | null;
navigateToUrl(path: string, options?: CreateHrefOptions): Promise<void>;
productAccess: ProductAccess;
productFeatures: ProductFeatures;
renderHeaderActions(HeaderActions?: FC): void;
security: SecurityPluginStart | null;
@ -146,7 +144,6 @@ export const KibanaLogic = kea<MakeLogicType<KibanaValues>>({
},
{},
],
productAccess: [props.productAccess, {}],
productFeatures: [props.productFeatures, {}],
renderHeaderActions: [props.renderHeaderActions, {}],
security: [props.security || null, {}],

View file

@ -20,7 +20,6 @@ import { renderHook } from '@testing-library/react';
import { EuiSideNavItemType } from '@elastic/eui';
import { DEFAULT_PRODUCT_FEATURES } from '../../../../common/constants';
import { ProductAccess } from '../../../../common/types';
import {
useEnterpriseSearchNav,
@ -28,10 +27,6 @@ import {
useEnterpriseSearchAnalyticsNav,
} from './nav';
const DEFAULT_PRODUCT_ACCESS: ProductAccess = {
hasAppSearchAccess: true,
hasWorkplaceSearchAccess: true,
};
const baseNavItems = [
expect.objectContaining({
'data-test-subj': 'searchSideNav-Home',
@ -186,16 +181,6 @@ const mockNavLinks = [
title: 'Inference Endpoints',
url: '/app/elasticsearch/relevance/inference_endpoints',
},
{
id: 'appSearch:engines',
title: 'App Search',
url: '/app/enterprise_search/app_search',
},
{
id: 'workplaceSearch',
title: 'Workplace Search',
url: '/app/enterprise_search/workplace_search',
},
{
id: 'enterpriseSearchElasticsearch',
title: 'Elasticsearch',
@ -221,7 +206,6 @@ const mockNavLinks = [
const defaultMockValues = {
hasEnterpriseLicense: true,
isSidebarEnabled: true,
productAccess: DEFAULT_PRODUCT_ACCESS,
productFeatures: DEFAULT_PRODUCT_FEATURES,
};
@ -233,11 +217,7 @@ describe('useEnterpriseSearchContentNav', () => {
});
it('returns an array of top-level Enterprise Search nav items', () => {
const fullProductAccess: ProductAccess = DEFAULT_PRODUCT_ACCESS;
setMockValues({
...defaultMockValues,
productAccess: fullProductAccess,
});
setMockValues(defaultMockValues);
const { result } = renderHook(() => useEnterpriseSearchNav());

View file

@ -71,10 +71,6 @@ export const mockKibanaProps: KibanaLogicProps = {
} as unknown as LensPublicStart,
ml: mlPluginMock.createStartContract(),
navigateToUrl: jest.fn(),
productAccess: {
hasAppSearchAccess: true,
hasWorkplaceSearchAccess: true,
},
productFeatures: {
hasConnectors: true,
hasDefaultIngestPipeline: true,

View file

@ -1,227 +0,0 @@
/*
* 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 { spacesMock } from '@kbn/spaces-plugin/server/mocks';
import { GlobalConfigService } from '../services/global_config_service';
import { checkAccess } from './check_access';
jest.mock('./enterprise_search_config_api', () => ({
callEnterpriseSearchConfigAPI: jest.fn(),
}));
import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api';
const enabledSpace = {
id: 'space',
name: 'space',
disabledFeatures: [],
};
const disabledSpace = {
id: 'space',
name: 'space',
disabledFeatures: ['enterpriseSearch'],
};
describe('checkAccess', () => {
const mockSecurity = {
authz: {
mode: {
useRbacForRequest: () => true,
},
checkPrivilegesWithRequest: () => ({
globally: () => ({
hasAllRequested: false,
}),
}),
actions: {
ui: {
get: () => null,
},
},
},
};
const mockSpaces = spacesMock.createStart();
const mockDependencies = {
request: { auth: { isAuthenticated: true } },
config: {
host: 'http://localhost:3002',
},
globalConfigService: new GlobalConfigService(),
security: mockSecurity,
spaces: mockSpaces,
} as any;
describe('when security is disabled', () => {
it('should deny all access', async () => {
const security = {
authz: { mode: { useRbacForRequest: () => false } },
};
expect(await checkAccess({ ...mockDependencies, security })).toEqual({
hasAppSearchAccess: false,
hasWorkplaceSearchAccess: false,
});
});
});
describe('when the current request is unauthenticated', () => {
it('should deny all access', async () => {
const request = {
auth: { isAuthenticated: false },
};
expect(await checkAccess({ ...mockDependencies, request })).toEqual({
hasAppSearchAccess: false,
hasWorkplaceSearchAccess: false,
});
});
});
describe('when the space is disabled', () => {
it('should deny all access', async () => {
mockSpaces.spacesService.getActiveSpace.mockResolvedValueOnce(disabledSpace);
expect(await checkAccess({ ...mockDependencies })).toEqual({
hasAppSearchAccess: false,
hasWorkplaceSearchAccess: false,
});
});
});
describe('when the Spaces plugin is unavailable', () => {
describe('when getActiveSpace returns 403 forbidden', () => {
it('should deny all access', async () => {
mockSpaces.spacesService.getActiveSpace.mockReturnValueOnce(
Promise.reject({ output: { statusCode: 403 } })
);
expect(await checkAccess({ ...mockDependencies })).toEqual({
hasAppSearchAccess: false,
hasWorkplaceSearchAccess: false,
});
});
});
describe('when getActiveSpace throws', () => {
it('should re-throw', async () => {
mockSpaces.spacesService.getActiveSpace.mockReturnValueOnce(Promise.reject('Error'));
let expectedError = '';
try {
await checkAccess({ ...mockDependencies });
} catch (e) {
expectedError = e;
}
expect(expectedError).toEqual('Error');
});
});
describe('when spaces plugin is not available', () => {
it('should not throw', async () => {
await expect(checkAccess({ ...mockDependencies, spaces: undefined })).resolves.toEqual({
hasAppSearchAccess: false,
hasWorkplaceSearchAccess: false,
});
});
});
});
describe('when the space is enabled', () => {
beforeEach(() => {
mockSpaces.spacesService.getActiveSpace.mockResolvedValueOnce(enabledSpace);
});
describe('when the user is a superuser', () => {
it('should allow all access when enabled at the space ', async () => {
const security = {
...mockSecurity,
authz: {
mode: { useRbacForRequest: () => true },
checkPrivilegesWithRequest: () => ({
globally: () => ({
hasAllRequested: true,
}),
}),
actions: { ui: { get: () => {} } },
},
};
expect(await checkAccess({ ...mockDependencies, security })).toEqual({
hasAppSearchAccess: true,
hasWorkplaceSearchAccess: true,
});
});
it('falls back to assuming a non-superuser role if auth credentials are missing', async () => {
const security = {
authz: {
...mockSecurity.authz,
checkPrivilegesWithRequest: () => ({
globally: () => Promise.reject({ statusCode: 403 }),
}),
},
};
expect(await checkAccess({ ...mockDependencies, security })).toEqual({
hasAppSearchAccess: false,
hasWorkplaceSearchAccess: false,
});
});
it('throws other authz errors', async () => {
const security = {
authz: {
...mockSecurity.authz,
checkPrivilegesWithRequest: undefined,
},
};
await expect(checkAccess({ ...mockDependencies, security })).rejects.toThrow();
});
});
describe('when the user is a non-superuser', () => {
describe('when enterpriseSearch.host is not set in kibana.yml', () => {
it('should deny all access', async () => {
const config = { host: undefined };
expect(await checkAccess({ ...mockDependencies, config })).toEqual({
hasAppSearchAccess: false,
hasWorkplaceSearchAccess: false,
});
});
});
describe('when enterpriseSearch.host is set in kibana.yml', () => {
it('should make a http call and return the access response', async () => {
(callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({
access: {
hasAppSearchAccess: false,
hasWorkplaceSearchAccess: true,
},
}));
expect(await checkAccess(mockDependencies)).toEqual({
hasAppSearchAccess: false,
hasWorkplaceSearchAccess: true,
});
});
it('falls back to no access if no http response', async () => {
(callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({}));
expect(await checkAccess(mockDependencies)).toEqual({
hasAppSearchAccess: false,
hasWorkplaceSearchAccess: false,
});
});
it('falls back to no access if response error', async () => {
(callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({
responseStatus: 500,
responseStatusText: 'failed',
}));
expect(await checkAccess(mockDependencies)).toEqual({
hasAppSearchAccess: false,
hasWorkplaceSearchAccess: false,
});
});
});
});
});
});

View file

@ -1,103 +0,0 @@
/*
* 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 { KibanaRequest, Logger } from '@kbn/core/server';
import { SecurityPluginSetup } from '@kbn/security-plugin/server';
import { SpacesPluginStart } from '@kbn/spaces-plugin/server';
import { ConfigType } from '..';
import { ProductAccess } from '../../common/types';
import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api';
interface CheckAccess {
request: KibanaRequest;
security: SecurityPluginSetup;
spaces?: SpacesPluginStart;
config: ConfigType;
log: Logger;
}
const ALLOW_ALL_PLUGINS: ProductAccess = {
hasAppSearchAccess: true,
hasWorkplaceSearchAccess: true,
};
const DENY_ALL_PLUGINS: ProductAccess = {
hasAppSearchAccess: false,
hasWorkplaceSearchAccess: false,
};
/**
* Determines whether the user has access to our Enterprise Search products
* via HTTP call. If not, we hide the corresponding plugin links from the
* nav and catalogue in `plugin.ts`, which disables plugin access
*/
export const checkAccess = async ({
config,
security,
spaces,
request,
log,
}: CheckAccess): Promise<ProductAccess> => {
const isRbacEnabled = security.authz.mode.useRbacForRequest(request);
// If security has been disabled, always hide app search and workplace search
if (!isRbacEnabled) {
return DENY_ALL_PLUGINS;
}
// We can only retrieve the active space when security is enabled and the request has already been authenticated
const attemptSpaceRetrieval = request.auth.isAuthenticated && !!spaces;
let allowedAtSpace = false;
if (attemptSpaceRetrieval) {
try {
const space = await spaces.spacesService.getActiveSpace(request);
allowedAtSpace = !space.disabledFeatures?.includes('enterpriseSearch');
} catch (err) {
if (err?.output?.statusCode === 403) {
allowedAtSpace = false;
} else {
throw err;
}
}
}
// Hide the plugin if turned off in the current space.
if (!allowedAtSpace) {
return DENY_ALL_PLUGINS;
}
// If the user is a "superuser" or has the base Kibana all privilege globally, always show the plugin
const isSuperUser = async (): Promise<boolean> => {
try {
const { hasAllRequested } = await security.authz
.checkPrivilegesWithRequest(request)
.globally({ kibana: security.authz.actions.ui.get('enterpriseSearch', 'all') });
return hasAllRequested || false;
} catch (err) {
if (err.statusCode === 401 || err.statusCode === 403) {
return false;
}
throw err;
}
};
if (await isSuperUser()) {
return ALLOW_ALL_PLUGINS;
}
// Hide the plugin when enterpriseSearch.host is not defined in kibana.yml
if (!config.host) {
return DENY_ALL_PLUGINS;
}
// When enterpriseSearch.host is defined in kibana.yml,
// make a HTTP call which returns product access
const response = (await callEnterpriseSearchConfigAPI({ request, config, log })) || {};
return 'access' in response ? response.access || DENY_ALL_PLUGINS : DENY_ALL_PLUGINS;
};

View file

@ -128,10 +128,6 @@ describe('callEnterpriseSearchConfigAPI', () => {
expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({
...DEFAULT_INITIAL_APP_DATA,
kibanaVersion: '1.0.0',
access: {
hasAppSearchAccess: true,
hasWorkplaceSearchAccess: false,
},
features: {
hasNativeConnectors: true,
hasWebCrawler: true,
@ -145,10 +141,6 @@ describe('callEnterpriseSearchConfigAPI', () => {
expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({
kibanaVersion: '1.0.0',
access: {
hasAppSearchAccess: false,
hasWorkplaceSearchAccess: false,
},
features: {
hasNativeConnectors: true,
hasWebCrawler: true,
@ -217,10 +209,6 @@ describe('callEnterpriseSearchConfigAPI', () => {
};
expect(await callEnterpriseSearchConfigAPI({ ...mockDependencies, config })).toEqual({
access: {
hasAppSearchAccess: false,
hasWorkplaceSearchAccess: false,
},
features: {
hasConnectors: false,
hasDefaultIngestPipeline: false,

View file

@ -45,10 +45,6 @@ export const callEnterpriseSearchConfigAPI = async ({
if (!config.host)
// Return Access and Features for when running without `ent-search`
return {
access: {
hasAppSearchAccess: false,
hasWorkplaceSearchAccess: false,
},
features: {
hasConnectors: config.hasConnectors,
hasDefaultIngestPipeline: config.hasDefaultIngestPipeline,
@ -100,10 +96,6 @@ export const callEnterpriseSearchConfigAPI = async ({
return {
enterpriseSearchVersion: data?.version?.number,
kibanaVersion: kibanaPackageJson.version,
access: {
hasAppSearchAccess: !!data?.current_user?.access?.app_search,
hasWorkplaceSearchAccess: !!data?.current_user?.access?.workplace_search,
},
features: {
hasConnectors: config.hasConnectors,
hasDefaultIngestPipeline: config.hasDefaultIngestPipeline,