[Fleet] Add integrations page gate (#213637)

Closes [#4169](https://github.com/elastic/ingest-dev/issues/4169)
## Summary


![image](https://github.com/user-attachments/assets/ad07e2c9-a37b-4a34-a87d-bdcb29f31e27)


Ths PR fixes an issue with integrations not being gated by
authentication rules by adding an auth check and returning error card if
the user doesnt have appropriate permissions.

- Moves `errors` layout file from fleet application to shared layouts
for both fleet and integrations

- Adds `callingApplication` prop to `permissionError` component to
properly display verbiage based on application its being used in

- Updated `fleet` application to have its own check when visiting the
`add-integrations` path that checks for appropriate permissions. Uses
`integrations.all` for check.

- Updated verbiage on error component with `guideLink`

- Adds top-level permission check on `integrations` application to
ensure user has permissions by using `integrations.read || fleet.all`
(may need to be extended in the future to make it more robust)


### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

### Identify risks

N/A

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Mason Herron 2025-03-11 14:04:58 -06:00 committed by GitHub
parent 37a0a69d43
commit 35bfbf0484
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 99 additions and 28 deletions

View file

@ -32,7 +32,6 @@ module.exports = {
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]fleet[\/\\]components[\/\\]fleet_server_instructions[\/\\]steps[\/\\]create_service_token.tsx/,
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]fleet[\/\\]components[\/\\]generate_service_token.tsx/,
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]fleet[\/\\]components[\/\\]search_bar.tsx/,
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]fleet[\/\\]layouts[\/\\]error.tsx/,
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]fleet[\/\\]sections[\/\\]agent_policy[\/\\]components[\/\\]agent_policy_advanced_fields[\/\\]advanced_monitoring.tsx/,
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]fleet[\/\\]sections[\/\\]agent_policy[\/\\]components[\/\\]agent_policy_advanced_fields[\/\\]custom_fields[\/\\]index.tsx/,
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]fleet[\/\\]sections[\/\\]agent_policy[\/\\]components[\/\\]agent_policy_create_inline.tsx/,

View file

@ -18736,7 +18736,6 @@
"xpack.fleet.packagePolicyValidation.quoteStringErrorMessage": "Les chaînes commençant par des caractères YAML spéciaux, comme * ou &, requièrent des guillemets doubles.",
"xpack.fleet.packagePolicyValidation.requiredErrorMessage": "{fieldName} obligatoire",
"xpack.fleet.pagePermissionDeniedErrorMessage": "Vous n'êtes pas autorisé à accéder à cette page. Le privilège Kibana {roleName} pour Fleet est requis.",
"xpack.fleet.permissionDeniedErrorMessage": "Vous n'êtes pas autorisé à accéder à Fleet. Des autorisations Kibana sont requises pour accéder à Fleet; l'autorisation {roleName2} ou {roleName1} est requise pour accéder aux intégrations.",
"xpack.fleet.permissionDeniedErrorTitle": "Autorisation refusée",
"xpack.fleet.permissionRequireRootMessage.guideLink": "Privilèges racine",
"xpack.fleet.permissionsRequestErrorMessageDescription": "Un problème est survenu lors de la vérification des autorisations Fleet",
@ -46386,4 +46385,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "Ce champ est requis.",
"xpack.watcher.watcherDescription": "Détectez les modifications survenant dans vos données en créant, gérant et monitorant des alertes."
}
}
}

View file

@ -18597,7 +18597,6 @@
"xpack.fleet.packagePolicyValidation.quoteStringErrorMessage": "*や&などの特殊YAML文字で始まる文字列は二重引用符で囲む必要があります。",
"xpack.fleet.packagePolicyValidation.requiredErrorMessage": "{fieldName}が必要です",
"xpack.fleet.pagePermissionDeniedErrorMessage": "このページへのアクセスが許可されていません。Fleetの\"{roleName}\" Kibana権限が必要です。",
"xpack.fleet.permissionDeniedErrorMessage": "Fleet へのアクセスが許可されていません。Fleetにアクセスするには、Kibana権限が必要です。統合にアクセスするには、{roleName2}または{roleName1}権限が必要です。",
"xpack.fleet.permissionDeniedErrorTitle": "パーミッションが拒否されました",
"xpack.fleet.permissionRequireRootMessage.guideLink": "ルート権限",
"xpack.fleet.permissionsRequestErrorMessageDescription": "Fleet アクセス権の確認中に問題が発生しました",
@ -46237,4 +46236,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。",
"xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。"
}
}
}

View file

@ -18303,7 +18303,6 @@
"xpack.fleet.packagePolicyValidation.nameRequiredErrorMessage": "'名称'必填",
"xpack.fleet.packagePolicyValidation.quoteStringErrorMessage": "以特殊 YAML 字符(* 或 &)开头的字符串需要使用双引号引起。",
"xpack.fleet.pagePermissionDeniedErrorMessage": "您无权访问该页面。它需要 Fleet 的 {roleName} Kibana 权限。",
"xpack.fleet.permissionDeniedErrorMessage": "您无权访问 Fleet。需要 Kibana 权限才能访问 Fleet需要 {roleName2} 或 {roleName1} 权限才能访问集成。",
"xpack.fleet.permissionDeniedErrorTitle": "权限被拒绝",
"xpack.fleet.permissionRequireRootMessage.guideLink": "根权限",
"xpack.fleet.permissionsRequestErrorMessageDescription": "检查 Fleet 权限时遇到问题",
@ -45563,4 +45562,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。",
"xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。"
}
}
}

View file

@ -121,6 +121,7 @@ export const StorybookContext: React.FC<{
addFleetServers: true,
},
integrations: {
all: true,
readPackageInfo: true,
readInstalledPackages: true,
installPackages: true,

View file

@ -34,6 +34,7 @@ export interface FleetAuthz {
};
integrations: {
all: boolean;
readPackageInfo: boolean;
readInstalledPackages: boolean;
installPackages: boolean;
@ -151,6 +152,7 @@ export const calculateAuthz = ({
return {
fleet: fleetAuthz,
integrations: {
all: integrations.all,
readPackageInfo: hasFleetAll || fleet.setup || integrations.all || integrations.read,
readInstalledPackages: integrations.all || integrations.read,
installPackages: writeIntegrationPolicies && integrations.all,

View file

@ -92,6 +92,7 @@ export const createFleetAuthzMock = (): FleetAuthz => {
addFleetServers: true,
},
integrations: {
all: true,
readPackageInfo: true,
readInstalledPackages: true,
installPackages: true,

View file

@ -60,7 +60,7 @@ import { EnrollmentTokenListPage } from './sections/agents/enrollment_token_list
import { UninstallTokenListPage } from './sections/agents/uninstall_token_list_page';
import { SettingsApp } from './sections/settings';
import { DebugPage } from './sections/debug';
import { ErrorLayout, PermissionsError } from './layouts';
import { ErrorLayout, PermissionsError } from '../../layouts/error';
const FEEDBACK_URL = 'https://ela.st/fleet-feedback';
@ -77,6 +77,8 @@ export const WithPermissionsAndSetup = memo<{ children?: React.ReactNode }>(({ c
authz.fleet.readAgentPolicies ||
authz.fleet.readSettings;
const hasIntegrationsCreateOrUpdatePrivileges = authz.integrations.all;
const [isPermissionsLoading, setIsPermissionsLoading] = useState<boolean>(false);
const [permissionsError, setPermissionsError] = useState<string>();
const [isInitialized, setIsInitialized] = useState(false);
@ -111,6 +113,9 @@ export const WithPermissionsAndSetup = memo<{ children?: React.ReactNode }>(({ c
if (!hasAnyFleetReadPrivileges) {
setPermissionsError('MISSING_PRIVILEGES');
}
if (!hasIntegrationsCreateOrUpdatePrivileges && isAddIntegrationsPath) {
setPermissionsError('MISSING_PRIVILEGES');
}
} catch (err) {
setInitializationError(err);
}
@ -122,12 +127,21 @@ export const WithPermissionsAndSetup = memo<{ children?: React.ReactNode }>(({ c
setPermissionsError('REQUEST_ERROR');
}
})();
}, [notifications.toasts, hasAnyFleetReadPrivileges]);
}, [
notifications.toasts,
hasAnyFleetReadPrivileges,
hasIntegrationsCreateOrUpdatePrivileges,
isAddIntegrationsPath,
]);
if (isPermissionsLoading || permissionsError) {
return (
<ErrorLayout isAddIntegrationsPath={isAddIntegrationsPath}>
{isPermissionsLoading ? <Loading /> : <PermissionsError error={permissionsError!} />}
{isPermissionsLoading ? (
<Loading />
) : (
<PermissionsError callingApplication="Fleet" error={permissionsError!} />
)}
</ErrorLayout>
);
}
@ -322,7 +336,11 @@ export const AppRoutes = memo(
) : (
<AppLayout setHeaderActionMenu={setHeaderActionMenu}>
<ErrorLayout isAddIntegrationsPath={false}>
<PermissionsError error="MISSING_PRIVILEGES" requiredFleetRole="Agents Read" />
<PermissionsError
callingApplication="Fleet"
error="MISSING_PRIVILEGES"
requiredFleetRole="Agents Read"
/>
</ErrorLayout>
</AppLayout>
)}
@ -340,6 +358,7 @@ export const AppRoutes = memo(
<AppLayout setHeaderActionMenu={setHeaderActionMenu}>
<ErrorLayout isAddIntegrationsPath={false}>
<PermissionsError
callingApplication="Fleet"
error="MISSING_PRIVILEGES"
requiredFleetRole="Agent policies Read"
/>
@ -356,7 +375,11 @@ export const AppRoutes = memo(
) : (
<AppLayout setHeaderActionMenu={setHeaderActionMenu}>
<ErrorLayout isAddIntegrationsPath={false}>
<PermissionsError error="MISSING_PRIVILEGES" requiredFleetRole="Agents All" />
<PermissionsError
callingApplication="Fleet"
error="MISSING_PRIVILEGES"
requiredFleetRole="Agents All"
/>
</ErrorLayout>
</AppLayout>
)}
@ -369,7 +392,11 @@ export const AppRoutes = memo(
) : (
<AppLayout setHeaderActionMenu={setHeaderActionMenu}>
<ErrorLayout isAddIntegrationsPath={false}>
<PermissionsError error="MISSING_PRIVILEGES" requiredFleetRole="Agents All" />
<PermissionsError
callingApplication="Fleet"
error="MISSING_PRIVILEGES"
requiredFleetRole="Agents All"
/>
</ErrorLayout>
</AppLayout>
)}
@ -391,7 +418,11 @@ export const AppRoutes = memo(
) : (
<ErrorLayout isAddIntegrationsPath={false}>
<AppLayout setHeaderActionMenu={setHeaderActionMenu}>
<PermissionsError error="MISSING_PRIVILEGES" requiredFleetRole="Settings Read" />
<PermissionsError
callingApplication="Fleet"
error="MISSING_PRIVILEGES"
requiredFleetRole="Settings Read"
/>
</AppLayout>
</ErrorLayout>
)}

View file

@ -8,5 +8,3 @@
export * from '../../../layouts';
export { DefaultLayout, DefaultPageTitle } from './default';
export { ErrorLayout, PermissionsError } from './error';

View file

@ -9,7 +9,7 @@ import React, { memo } from 'react';
import type { AppMountParameters } from '@kbn/core/public';
import { EuiPortal } from '@elastic/eui';
import type { History } from 'history';
import { Redirect } from 'react-router-dom';
import { Redirect, useRouteMatch } from 'react-router-dom';
import { Router, Routes, Route } from '@kbn/shared-ux-router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
@ -27,22 +27,25 @@ import {
type FleetStatusProviderProps,
KibanaVersionContext,
useFleetStatus,
useAuthz,
} from '../../hooks';
import { SpaceSettingsContextProvider } from '../../hooks/use_space_settings_context';
import { FleetServerFlyout } from '../fleet/components';
import { AgentPolicyContextProvider, useFlyoutContext } from './hooks';
import { INTEGRATIONS_ROUTING_PATHS, pagePathGetters } from './constants';
import { FLEET_ROUTING_PATHS, INTEGRATIONS_ROUTING_PATHS, pagePathGetters } from './constants';
import type { UIExtensionsStorage } from './types';
import { EPMApp } from './sections/epm';
import { ErrorLayout, PermissionsError } from '../../layouts/error';
import { PackageInstallProvider, UIExtensionsContext, FlyoutContextProvider } from './hooks';
import { IntegrationsHeader } from './components/header';
import { AgentEnrollmentFlyout } from './components';
import { ReadOnlyContextProvider } from './hooks/use_read_only_context';
const queryClient = new QueryClient();
const EmptyContext = () => <></>;
@ -141,6 +144,19 @@ export const IntegrationsAppContext: React.FC<{
export const AppRoutes = memo(() => {
const flyoutContext = useFlyoutContext();
const fleetStatus = useFleetStatus();
const authz = useAuthz();
const isAddIntegrationsPath = !!useRouteMatch(FLEET_ROUTING_PATHS.add_integration_to_policy);
const allowedToAccess =
authz.integrations.readIntegrationPolicies || authz.integrations.all || authz.fleet.all;
const missingPrivilegesString = 'MISSING_PRIVILEGES';
if (!allowedToAccess) {
return (
<ErrorLayout isAddIntegrationsPath={isAddIntegrationsPath}>
<PermissionsError callingApplication="Integrations" error={missingPrivilegesString} />
</ErrorLayout>
);
}
return (
<>

View file

@ -68,7 +68,7 @@ import {
import type { WithHeaderLayoutProps } from '../../../../layouts';
import { WithHeaderLayout } from '../../../../layouts';
import { PermissionsError } from '../../../../../fleet/layouts';
import { PermissionsError } from '../../../../layouts';
import { DeferredAssetsWarning } from './assets/deferred_assets_warning';
import { useIsFirstTimeAgentUserQuery } from './hooks';
@ -855,6 +855,7 @@ export function Detail() {
<PermissionsError
error="MISSING_PRIVILEGES"
requiredFleetRole="Agent Policies Read and Integrations Read"
callingApplication="Integrations"
/>
)}
</Route>

View file

@ -5,16 +5,20 @@
* 2.0.
*/
import React from 'react';
import { EuiErrorBoundary, EuiPanel, EuiEmptyPrompt, EuiCode } from '@elastic/eui';
import { EuiErrorBoundary, EuiPanel, EuiEmptyPrompt, EuiCode, EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import styled from '@emotion/styled';
import { MissingESRequirementsPage } from '../sections/agents/agent_requirements_page';
import { WithHeaderLayout, WithoutHeaderLayout } from '../../../layouts';
import { Error } from '../components';
import { useStartServices } from '../hooks';
import { DefaultLayout, DefaultPageTitle } from './default';
import { MissingESRequirementsPage } from '../applications/fleet/sections/agents/agent_requirements_page';
import { WithHeaderLayout, WithoutHeaderLayout } from '.';
import { Error } from '../applications/fleet/components';
import { DefaultLayout, DefaultPageTitle } from '../applications/fleet/layouts/default';
const Panel = styled(EuiPanel)`
max-width: 500px;
@ -40,7 +44,10 @@ export const ErrorLayout: React.FunctionComponent<{
export const PermissionsError: React.FunctionComponent<{
error: string;
requiredFleetRole?: string;
}> = React.memo(({ error, requiredFleetRole }) => {
callingApplication: string;
}> = React.memo(({ error, requiredFleetRole, callingApplication }) => {
const { docLinks } = useStartServices();
if (error === 'MISSING_SECURITY') {
return <MissingESRequirementsPage missingRequirements={['security_required', 'api_keys']} />;
}
@ -71,10 +78,23 @@ export const PermissionsError: React.FunctionComponent<{
) : (
<FormattedMessage
id="xpack.fleet.permissionDeniedErrorMessage"
defaultMessage="You are not authorized to access Fleet. Kibana privileges are required to access Fleet; the {roleName2} or {roleName1} privilege is required to access Integrations."
defaultMessage="You are not currently authorized to access {callingApplication}. For access, your Kibana role must include the {roleName2} or {roleName1} privilege for {callingApplication}. {guideLink}"
values={{
callingApplication,
roleName1: <EuiCode>&quot;All&quot;</EuiCode>,
roleName2: <EuiCode>&quot;Read&quot;</EuiCode>,
guideLink: (
<EuiLink
href={docLinks.links.fleet.roleAndPrivileges}
target="_blank"
external
>
<FormattedMessage
id="xpack.fleet.settings.rolesAndPrivilegesGuideLink"
defaultMessage="Learn more."
/>
</EuiLink>
),
}}
/>
)}

View file

@ -8,3 +8,4 @@
export type { WithHeaderLayoutProps } from './with_header';
export { WithHeaderLayout } from './with_header';
export { WithoutHeaderLayout } from './without_header';
export { ErrorLayout, PermissionsError } from './error';

View file

@ -48,6 +48,7 @@ const fleetAuthzMock: FleetAuthz = {
addFleetServers: true,
},
integrations: {
all: true,
readPackageInfo: true,
readInstalledPackages: true,
installPackages: true,

View file

@ -30,6 +30,7 @@ export const createStartMock = (extensionsStorage: UIExtensionsStorage = {}): Mo
addFleetServers: true,
},
integrations: {
all: true,
readPackageInfo: true,
readInstalledPackages: true,
installPackages: true,

View file

@ -78,6 +78,7 @@ describe('AgentService', () => {
addFleetServers: false,
},
integrations: {
all: true,
readPackageInfo: false,
readInstalledPackages: false,
installPackages: false,

View file

@ -33,6 +33,7 @@ describe('When using calculateRouteAuthz()', () => {
addFleetServers: false,
},
integrations: {
all: false,
readPackageInfo: false,
readInstalledPackages: false,
installPackages: false,