[Fleet] Add support for non-superuser access to Fleet and Integrations (#122347)

* [Fleet] Split Fleet and Integration privileges

* Update UI when Fleet has All privileges and Integrations have None

* Replace remaining superuser checks

* Updates to server/plugin

* Update getAuthzFromRequest

* Update start method in the client side

* Fix tests

* Fix functional tests

* Make changes to the UI based on new privilege system

* Further UI changes

* Make capabilities accessible to unit tests in createStartServices

* Fix failing tests

* Fix ts checks

* Address most review comments

* Introduce hook exposing authz and make UI checks more granular; address rest of comments

* Remove capabilities hook

* Get rid of useCapabilites

* Address review comments

* Other fixes

* Fix tutorial app privileges

* Address code review comments and update privileges naming

* Fix i18n failing check

* Block fleet server setup UI  when user does not have manage_service_account privilege

* Minor changes

* Use unique i18n id

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Josh Dover <doverfake@elastic.co>
This commit is contained in:
Cristina Amico 2022-01-31 18:59:18 +01:00 committed by GitHub
parent a88d4a8f6b
commit b12f70800c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 461 additions and 294 deletions

View file

@ -27,7 +27,12 @@ export const getApplication = () => {
management: {},
navLinks: {},
fleet: {
write: true,
read: true,
all: true,
},
fleetv2: {
read: true,
all: true,
},
},
applications$: of(applications),

View file

@ -68,7 +68,7 @@ export const calculateAuthz = ({
readPackageSettings: fleet.all && integrations.all,
writePackageSettings: fleet.all && integrations.all,
readIntegrationPolicies: fleet.all && integrations.all,
readIntegrationPolicies: fleet.all && (integrations.all || integrations.read),
writeIntegrationPolicies: fleet.all && integrations.all,
},
});

View file

@ -184,7 +184,7 @@ export const settingsRoutesService = {
};
export const appRoutesService = {
getCheckPermissionsPath: () => APP_API_ROUTES.CHECK_PERMISSIONS_PATTERN,
getCheckPermissionsPath: (fleetServerSetup?: boolean) => APP_API_ROUTES.CHECK_PERMISSIONS_PATTERN,
getRegenerateServiceTokenPath: () => APP_API_ROUTES.GENERATE_SERVICE_TOKEN_PATTERN,
};

View file

@ -6,7 +6,7 @@
*/
export interface CheckPermissionsResponse {
error?: 'MISSING_SECURITY' | 'MISSING_SUPERUSER_ROLE';
error?: 'MISSING_SECURITY' | 'MISSING_PRIVILEGES' | 'MISSING_FLEET_SERVER_SETUP_PRIVILEGES';
success: boolean;
}

View file

@ -29,6 +29,8 @@ import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/com
import { PackageInstallProvider } from '../integrations/hooks';
import { useAuthz } from './hooks';
import {
ConfigContext,
FleetStatusProvider,
@ -80,9 +82,9 @@ const PermissionsError: React.FunctionComponent<{ error: string }> = memo(({ err
return <MissingESRequirementsPage missingRequirements={['security_required', 'api_keys']} />;
}
if (error === 'MISSING_SUPERUSER_ROLE') {
if (error === 'MISSING_PRIVILEGES') {
return (
<Panel>
<Panel data-test-subj="missingPrivilegesPrompt">
<EuiEmptyPrompt
iconType="securityApp"
title={
@ -97,8 +99,11 @@ const PermissionsError: React.FunctionComponent<{ error: string }> = memo(({ err
<p>
<FormattedMessage
id="xpack.fleet.permissionDeniedErrorMessage"
defaultMessage="You are not authorized to access Fleet. Fleet requires {roleName} privileges."
values={{ roleName: <EuiCode>superuser</EuiCode> }}
defaultMessage="You are not authorized to access Fleet. It requires the {roleName1} Kibana privilege for Fleet, and the {roleName2} or {roleName1} privilege for Integrations."
values={{
roleName1: <EuiCode>&quot;All&quot;</EuiCode>,
roleName2: <EuiCode>&quot;Read&quot;</EuiCode>,
}}
/>
</p>
}
@ -124,7 +129,10 @@ const PermissionsError: React.FunctionComponent<{ error: string }> = memo(({ err
export const WithPermissionsAndSetup: React.FC = memo(({ children }) => {
useBreadcrumbs('base');
const { notifications } = useStartServices();
const core = useStartServices();
const { notifications } = core;
const hasFleetAllPrivileges = useAuthz().fleet.all;
const [isPermissionsLoading, setIsPermissionsLoading] = useState<boolean>(false);
const [permissionsError, setPermissionsError] = useState<string>();
@ -156,6 +164,9 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => {
}),
});
}
if (!hasFleetAllPrivileges) {
setPermissionsError('MISSING_PRIVILEGES');
}
} catch (err) {
setInitializationError(err);
}
@ -167,7 +178,7 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => {
setPermissionsError('REQUEST_ERROR');
}
})();
}, [notifications.toasts]);
}, [notifications.toasts, hasFleetAllPrivileges]);
if (isPermissionsLoading || permissionsError) {
return (

View file

@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { EuiContextMenuItem, EuiPortal } from '@elastic/eui';
import type { AgentPolicy } from '../../../types';
import { useCapabilities } from '../../../hooks';
import { useAuthz } from '../../../hooks';
import { AgentEnrollmentFlyout, ContextMenuActions } from '../../../components';
import { AgentPolicyYamlFlyout } from './agent_policy_yaml_flyout';
@ -30,7 +30,7 @@ export const AgentPolicyActionMenu = memo<{
enrollmentFlyoutOpenByDefault = false,
onCancelEnrollment,
}) => {
const hasWriteCapabilities = useCapabilities().write;
const canWriteIntegrationPolicies = useAuthz().integrations.writeIntegrationPolicies;
const [isYamlFlyoutOpen, setIsYamlFlyoutOpen] = useState<boolean>(false);
const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState<boolean>(
enrollmentFlyoutOpenByDefault
@ -76,7 +76,6 @@ export const AgentPolicyActionMenu = memo<{
? [viewPolicyItem]
: [
<EuiContextMenuItem
disabled={!hasWriteCapabilities}
icon="plusInCircle"
onClick={() => {
setIsContextMenuOpen(false);
@ -91,7 +90,7 @@ export const AgentPolicyActionMenu = memo<{
</EuiContextMenuItem>,
viewPolicyItem,
<EuiContextMenuItem
disabled={!hasWriteCapabilities}
disabled={!canWriteIntegrationPolicies}
icon="copy"
onClick={() => {
setIsContextMenuOpen(false);

View file

@ -28,7 +28,7 @@ import { isPackageLimited, doesAgentPolicyAlreadyIncludePackage } from '../../..
import {
useGetAgentPolicies,
sendGetOneAgentPolicy,
useCapabilities,
useAuthz,
useFleetStatus,
} from '../../../hooks';
import { CreateAgentPolicyFlyout } from '../list_page/components';
@ -63,7 +63,7 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{
const [selectedAgentPolicyError, setSelectedAgentPolicyError] = useState<Error>();
// Create new agent policy flyout state
const hasWriteCapabilites = useCapabilities().write;
const hasFleetAllPrivileges = useAuthz().fleet.all;
const [isCreateAgentPolicyFlyoutOpen, setIsCreateAgentPolicyFlyoutOpen] =
useState<boolean>(false);
@ -251,7 +251,7 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{
<EuiFlexItem grow={false}>
<div>
<EuiLink
disabled={!hasWriteCapabilites}
disabled={!hasFleetAllPrivileges}
onClick={() => setIsCreateAgentPolicyFlyoutOpen(true)}
>
<FormattedMessage

View file

@ -9,12 +9,12 @@ import React, { memo } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
import { useCapabilities, useStartServices } from '../../../../../hooks';
import { useAuthz, useStartServices } from '../../../../../hooks';
import { pagePathGetters, INTEGRATIONS_PLUGIN_ID } from '../../../../../constants';
export const NoPackagePolicies = memo<{ policyId: string }>(({ policyId }) => {
const { application } = useStartServices();
const hasWriteCapabilities = useCapabilities().write;
const canWriteIntegrationPolicies = useAuthz().integrations.writeIntegrationPolicies;
return (
<EuiEmptyPrompt
@ -35,7 +35,7 @@ export const NoPackagePolicies = memo<{ policyId: string }>(({ policyId }) => {
}
actions={
<EuiButton
isDisabled={!hasWriteCapabilities}
isDisabled={!canWriteIntegrationPolicies}
fill
onClick={() =>
application.navigateToApp(INTEGRATIONS_PLUGIN_ID, {

View file

@ -25,12 +25,7 @@ import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../common';
import { pagePathGetters } from '../../../../../../../constants';
import type { AgentPolicy, InMemoryPackagePolicy, PackagePolicy } from '../../../../../types';
import { PackageIcon, PackagePolicyActionsMenu } from '../../../../../components';
import {
useCapabilities,
useLink,
usePackageInstallations,
useStartServices,
} from '../../../../../hooks';
import { useAuthz, useLink, usePackageInstallations, useStartServices } from '../../../../../hooks';
import { pkgKeyFromPackageInfo } from '../../../../../services';
interface Props {
@ -55,7 +50,8 @@ export const PackagePoliciesTable: React.FunctionComponent<Props> = ({
...rest
}) => {
const { application } = useStartServices();
const hasWriteCapabilities = useCapabilities().write;
const canWriteIntegrationPolicies = useAuthz().integrations.writeIntegrationPolicies;
const canReadIntegrationPolicies = useAuthz().integrations.readIntegrationPolicies;
const { updatableIntegrations } = usePackageInstallations();
const { getHref } = useLink();
@ -109,7 +105,7 @@ export const PackagePoliciesTable: React.FunctionComponent<Props> = ({
render: (value: string, packagePolicy: InMemoryPackagePolicy) => (
<EuiLink
title={value}
{...(hasWriteCapabilities
{...(canReadIntegrationPolicies
? {
href: getHref('edit_integration', {
policyId: agentPolicy.id,
@ -144,7 +140,7 @@ export const PackagePoliciesTable: React.FunctionComponent<Props> = ({
render(packageTitle: string, packagePolicy: InMemoryPackagePolicy) {
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiFlexItem data-test-subj="PackagePoliciesTableLink" grow={false}>
<EuiLink
href={
packagePolicy.package &&
@ -195,6 +191,7 @@ export const PackagePoliciesTable: React.FunctionComponent<Props> = ({
<EuiButton
size="s"
minWidth="0"
isDisabled={!canWriteIntegrationPolicies}
href={`${getHref('upgrade_package_policy', {
policyId: agentPolicy.id,
packagePolicyId: packagePolicy.id,
@ -231,7 +228,7 @@ export const PackagePoliciesTable: React.FunctionComponent<Props> = ({
actions: [
{
render: (packagePolicy: InMemoryPackagePolicy) => {
return (
return canWriteIntegrationPolicies ? (
<PackagePolicyActionsMenu
agentPolicy={agentPolicy}
packagePolicy={packagePolicy}
@ -240,13 +237,15 @@ export const PackagePoliciesTable: React.FunctionComponent<Props> = ({
packagePolicyId: packagePolicy.id,
})}?from=fleet-policy-list`}
/>
) : (
<></>
);
},
},
],
},
],
[agentPolicy, getHref, hasWriteCapabilities]
[agentPolicy, getHref, canWriteIntegrationPolicies, canReadIntegrationPolicies]
);
return (
@ -268,7 +267,7 @@ export const PackagePoliciesTable: React.FunctionComponent<Props> = ({
<EuiButton
key="addPackagePolicyButton"
fill
isDisabled={!hasWriteCapabilities}
isDisabled={!canWriteIntegrationPolicies}
iconType="plusInCircle"
onClick={() => {
application.navigateToApp(INTEGRATIONS_PLUGIN_ID, {

View file

@ -23,7 +23,7 @@ import type { AgentPolicy } from '../../../../../types';
import {
useLink,
useStartServices,
useCapabilities,
useAuthz,
sendUpdateAgentPolicy,
useConfig,
sendGetAgentStatus,
@ -51,7 +51,7 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>(
} = useConfig();
const history = useHistory();
const { getPath } = useLink();
const hasWriteCapabilites = useCapabilities().write;
const hasFleetAllPrivileges = useAuthz().fleet.all;
const refreshAgentPolicy = useAgentPolicyRefresh();
const [agentPolicy, setAgentPolicy] = useState<AgentPolicy>({
...originalAgentPolicy,
@ -186,7 +186,7 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>(
onClick={onSubmit}
isLoading={isLoading}
isDisabled={
!hasWriteCapabilites || isLoading || Object.keys(validation).length > 0
!hasFleetAllPrivileges || isLoading || Object.keys(validation).length > 0
}
iconType="save"
color="primary"

View file

@ -12,7 +12,6 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { safeLoad } from 'js-yaml';
import {
EuiButtonEmpty,
EuiButton,
EuiBottomBar,
EuiCallOut,
EuiFlexGroup,
@ -42,6 +41,7 @@ import {
sendGetOnePackagePolicy,
sendGetPackageInfoByKey,
sendUpgradePackagePolicyDryRun,
useAuthz,
} from '../../../hooks';
import {
useBreadcrumbs as useIntegrationsBreadcrumbs,
@ -65,6 +65,8 @@ import type {
import type { PackagePolicyEditExtensionComponentProps } from '../../../types';
import { pkgKeyFromPackageInfo, storedPackagePoliciesToAgentInputs } from '../../../services';
import { EuiButtonWithTooltip } from '../../../../integrations/sections/epm/screens/detail';
import { hasUpgradeAvailable } from './utils';
export const EditPackagePolicyPage = memo(() => {
@ -123,6 +125,8 @@ export const EditPackagePolicyForm = memo<{
const [isUpgrade, setIsUpgrade] = useState<boolean>(false);
const canWriteIntegrationPolicies = useAuthz().integrations.writeIntegrationPolicies;
useEffect(() => {
if (forceUpgrade) {
setIsUpgrade(true);
@ -625,11 +629,27 @@ export const EditPackagePolicyForm = memo<{
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
<EuiButtonWithTooltip
onClick={onSubmit}
isLoading={formState === 'LOADING'}
// Allow to save only if the package policy is upgraded or had been edited
disabled={formState !== 'VALID' || (!isEdited && !isUpgrade)}
isDisabled={
!canWriteIntegrationPolicies ||
formState !== 'VALID' ||
(!isEdited && !isUpgrade)
}
tooltip={
!canWriteIntegrationPolicies
? {
content: (
<FormattedMessage
id="xpack.fleet.agentPolicy.saveIntegrationTooltip"
defaultMessage="To save the integration policy, you must have security enabled and have the All privilege for Integrations. Contact your administrator."
/>
),
}
: undefined
}
iconType="save"
color="primary"
fill
@ -646,7 +666,7 @@ export const EditPackagePolicyForm = memo<{
defaultMessage="Save integration"
/>
)}
</EuiButton>
</EuiButtonWithTooltip>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>

View file

@ -26,7 +26,7 @@ import {
import { dataTypes } from '../../../../../../../common';
import type { NewAgentPolicy, AgentPolicy } from '../../../../types';
import { useCapabilities, useStartServices, sendCreateAgentPolicy } from '../../../../hooks';
import { useAuthz, useStartServices, sendCreateAgentPolicy } from '../../../../hooks';
import { AgentPolicyForm, agentPolicyFormValidation } from '../../components';
const FlyoutWithHigherZIndex = styled(EuiFlyout)`
@ -43,7 +43,7 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent<Props> = ({
...restOfProps
}) => {
const { notifications } = useStartServices();
const hasWriteCapabilites = useCapabilities().write;
const hasFleetAllPrivileges = useAuthz().fleet.all;
const [agentPolicy, setAgentPolicy] = useState<NewAgentPolicy>({
name: '',
description: '',
@ -115,7 +115,7 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent<Props> = ({
<EuiButton
fill
isLoading={isLoading}
isDisabled={!hasWriteCapabilites || isLoading || Object.keys(validation).length > 0}
isDisabled={!hasFleetAllPrivileges || isLoading || Object.keys(validation).length > 0}
onClick={async () => {
setIsLoading(true);
try {

View file

@ -25,7 +25,7 @@ import { useHistory } from 'react-router-dom';
import type { AgentPolicy } from '../../../types';
import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../constants';
import {
useCapabilities,
useAuthz,
useGetAgentPolicies,
usePagination,
useSorting,
@ -42,7 +42,8 @@ import { CreateAgentPolicyFlyout } from './components';
export const AgentPolicyListPage: React.FunctionComponent<{}> = () => {
useBreadcrumbs('policies_list');
const { getPath } = useLink();
const hasWriteCapabilites = useCapabilities().write;
const hasFleetAllPrivileges = useAuthz().fleet.all;
const {
agents: { enabled: isFleetEnabled },
} = useConfig();
@ -148,6 +149,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => {
packagePolicies ? packagePolicies.length : 0,
},
{
field: 'actions',
name: i18n.translate('xpack.fleet.agentPolicyList.actionsColumnTitle', {
defaultMessage: 'Actions',
}),
@ -177,7 +179,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => {
<EuiButton
fill
iconType="plusInCircle"
isDisabled={!hasWriteCapabilites}
isDisabled={!hasFleetAllPrivileges}
onClick={() => setIsCreateAgentPolicyFlyoutOpen(true)}
>
<FormattedMessage
@ -186,7 +188,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => {
/>
</EuiButton>
),
[hasWriteCapabilites, setIsCreateAgentPolicyFlyoutOpen]
[hasFleetAllPrivileges, setIsCreateAgentPolicyFlyoutOpen]
);
const emptyPrompt = useMemo(

View file

@ -10,7 +10,7 @@ import { EuiPortal, EuiContextMenuItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { Agent, AgentPolicy, PackagePolicy } from '../../../../types';
import { useCapabilities, useKibanaVersion } from '../../../../hooks';
import { useAuthz, useKibanaVersion } from '../../../../hooks';
import { ContextMenuActions } from '../../../../components';
import {
AgentUnenrollAgentModal,
@ -27,7 +27,7 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{
assignFlyoutOpenByDefault?: boolean;
onCancelReassign?: () => void;
}> = memo(({ agent, assignFlyoutOpenByDefault = false, onCancelReassign, agentPolicy }) => {
const hasWriteCapabilites = useCapabilities().write;
const hasFleetAllPrivileges = useAuthz().fleet.all;
const kibanaVersion = useKibanaVersion();
const refreshAgent = useAgentRefresh();
const [isReassignFlyoutOpen, setIsReassignFlyoutOpen] = useState(assignFlyoutOpenByDefault);
@ -110,7 +110,7 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{
</EuiContextMenuItem>,
<EuiContextMenuItem
icon="cross"
disabled={!hasWriteCapabilites || !agent.active}
disabled={!hasFleetAllPrivileges || !agent.active}
onClick={() => {
setIsUnenrollModalOpen(true);
}}

View file

@ -139,6 +139,7 @@ export const AgentDetailsIntegration: React.FunctionComponent<{
<EuiFlexItem className="eui-textTruncate">
<EuiLink
className="eui-textTruncate"
data-test-subj="agentPolicyDetailsLink"
href={getHref('edit_integration', {
policyId: agentPolicy.id,
packagePolicyId: packagePolicy.id,

View file

@ -25,7 +25,7 @@ import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react';
import type { Agent, AgentPolicy, PackagePolicy, SimplifiedAgentStatus } from '../../../types';
import {
usePagination,
useCapabilities,
useAuthz,
useGetAgentPolicies,
sendGetAgents,
sendGetAgentStatus,
@ -68,7 +68,7 @@ const RowActions = React.memo<{
onUpgradeClick: () => void;
}>(({ agent, agentPolicy, refresh, onReassignClick, onUnenrollClick, onUpgradeClick }) => {
const { getHref } = useLink();
const hasWriteCapabilites = useCapabilities().write;
const hasFleetAllPrivileges = useAuthz().fleet.all;
const isUnenrolling = agent.status === 'unenrolling';
const kibanaVersion = useKibanaVersion();
@ -99,7 +99,7 @@ const RowActions = React.memo<{
/>
</EuiContextMenuItem>,
<EuiContextMenuItem
disabled={!hasWriteCapabilites || !agent.active}
disabled={!hasFleetAllPrivileges || !agent.active}
icon="trash"
onClick={() => {
onUnenrollClick();
@ -152,7 +152,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
useBreadcrumbs('agent_list');
const { getHref } = useLink();
const defaultKuery: string = (useUrlParams().urlParams.kuery as string) || '';
const hasWriteCapabilites = useCapabilities().write;
const hasFleetAllPrivileges = useAuthz().fleet.all;
const isGoldPlus = useLicense().isGoldPlus();
const kibanaVersion = useKibanaVersion();
@ -507,7 +507,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
</h2>
}
actions={
hasWriteCapabilites ? (
hasFleetAllPrivileges ? (
<EuiButton
fill
iconType="plusInCircle"

View file

@ -5,11 +5,15 @@
* 2.0.
*/
import React from 'react';
import React, { useState, useEffect } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import { useStartServices } from '../../../hooks';
import { useStartServices, sendGetPermissionsCheck } from '../../../hooks';
import { FleetServerMissingPrivileges } from '../../agents/components/fleet_server_callouts';
import { Loading } from '../../../components';
import { CloudInstructions, OnPremInstructions } from './components';
@ -27,6 +31,29 @@ export const FleetServerRequirementPage = () => {
const startService = useStartServices();
const deploymentUrl = startService.cloud?.deploymentUrl;
const [isPermissionsLoading, setIsPermissionsLoading] = useState<boolean>(false);
const [permissionsError, setPermissionsError] = useState<string>();
useEffect(() => {
async function checkPermissions() {
setIsPermissionsLoading(false);
setPermissionsError(undefined);
try {
setIsPermissionsLoading(true);
const permissionsResponse = await sendGetPermissionsCheck(true);
setIsPermissionsLoading(false);
if (!permissionsResponse.data?.success) {
setPermissionsError(permissionsResponse.data?.error || 'REQUEST_ERROR');
}
} catch (err) {
setPermissionsError('REQUEST_ERROR');
}
}
checkPermissions();
}, []);
return (
<>
<ContentWrapper
@ -38,6 +65,10 @@ export const FleetServerRequirementPage = () => {
<FlexItemWithMinWidth grow={false}>
{deploymentUrl ? (
<CloudInstructions deploymentUrl={deploymentUrl} />
) : isPermissionsLoading ? (
<Loading />
) : permissionsError ? (
<FleetServerMissingPrivileges />
) : (
<OnPremInstructions />
)}

View file

@ -0,0 +1,46 @@
/*
* 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 React from 'react';
import { EuiCode, EuiEmptyPrompt, EuiPanel } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import styled from 'styled-components';
const Panel = styled(EuiPanel)`
max-width: 500px;
margin-right: auto;
margin-left: auto;
`;
export const FleetServerMissingPrivileges = () => {
return (
<Panel data-test-subj="fleetServerMissingPrivilegesPrompt">
<EuiEmptyPrompt
iconType="securityApp"
title={
<h2>
<FormattedMessage
id="xpack.fleet.fleetServerSetupPermissionDeniedErrorTitle"
defaultMessage="Permission denied"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.fleet.fleetServerSetupPermissionDeniedErrorMessage"
defaultMessage="Fleet Server needs to be set up. This requires the {roleName} cluster privilege. Contact your administrator."
values={{
roleName: <EuiCode>&quot;manage_service_account&quot;</EuiCode>,
}}
/>
</p>
}
/>
</Panel>
);
};

View file

@ -8,3 +8,4 @@
export * from './fleet_server_cloud_unhealthy_callout';
export * from './fleet_server_on_prem_unhealthy_callout';
export * from './fleet_server_on_prem_required_callout';
export * from './fleet_server_missing_privileges';

View file

@ -22,7 +22,7 @@ export const NoAccessPage = injectI18n(({ intl }) => (
<FormattedMessage
id="xpack.fleet.noAccess.accessDeniedDescription"
defaultMessage="You are not authorized to access Elastic Fleet. To use Elastic Fleet,
you need a user role that contains read or all permissions for this application."
you need a user role that contains All permissions for this application."
/>
</p>
</NoDataLayout>

View file

@ -16,7 +16,7 @@ import {
useConfig,
useFleetStatus,
useBreadcrumbs,
useCapabilities,
useAuthz,
useGetSettings,
useGetAgentPolicies,
} from '../../hooks';
@ -32,7 +32,7 @@ export const AgentsApp: React.FunctionComponent = () => {
useBreadcrumbs('agent_list');
const history = useHistory();
const { agents } = useConfig();
const capabilities = useCapabilities();
const hasFleetAllPrivileges = useAuthz().fleet.all;
const agentPoliciesRequest = useGetAgentPolicies({
page: 1,
@ -93,7 +93,7 @@ export const AgentsApp: React.FunctionComponent = () => {
) {
return <MissingESRequirementsPage missingRequirements={fleetStatus.missingRequirements} />;
}
if (!capabilities.read) {
if (!hasFleetAllPrivileges) {
return <NoAccessPage />;
}

View file

@ -85,7 +85,7 @@ describe('when on integration detail', () => {
expect(renderResult.queryByTestId('agentPolicyCount')).toBeNull();
});
it('should NOT the Policies tab', async () => {
it('should NOT display the Policies tab', async () => {
await mockedApi.waitForApi();
expect(renderResult.queryByTestId('tab-policies')).toBeNull();
});

View file

@ -35,6 +35,7 @@ import {
useUIExtension,
useBreadcrumbs,
useStartServices,
useAuthz,
usePermissionCheck,
} from '../../../../hooks';
import {
@ -43,12 +44,7 @@ import {
INTEGRATIONS_ROUTING_PATHS,
pagePathGetters,
} from '../../../../constants';
import {
useCapabilities,
useGetPackageInfoByKey,
useLink,
useAgentPolicyContext,
} from '../../../../hooks';
import { useGetPackageInfoByKey, useLink, useAgentPolicyContext } from '../../../../hooks';
import { pkgKeyFromPackageInfo } from '../../../../services';
import type {
CreatePackagePolicyRouteState,
@ -102,11 +98,13 @@ export function Detail() {
const { getId: getAgentPolicyId } = useAgentPolicyContext();
const { pkgkey, panel } = useParams<DetailParams>();
const { getHref } = useLink();
const hasWriteCapabilities = useCapabilities().write;
const canInstallPackages = useAuthz().integrations.installPackages;
const canReadPackageSettings = useAuthz().integrations.readPackageSettings;
const canReadIntegrationPolicies = useAuthz().integrations.readIntegrationPolicies;
const permissionCheck = usePermissionCheck();
const missingSecurityConfiguration =
!permissionCheck.data?.success && permissionCheck.data?.error === 'MISSING_SECURITY';
const userCanInstallIntegrations = hasWriteCapabilities && permissionCheck.data?.success;
const userCanInstallPackages = canInstallPackages && permissionCheck.data?.success;
const history = useHistory();
const { pathname, search, hash } = useLocation();
const queryParams = useMemo(() => new URLSearchParams(search), [search]);
@ -367,7 +365,7 @@ export function Detail() {
content: (
<EuiButtonWithTooltip
fill
isDisabled={!userCanInstallIntegrations}
isDisabled={!userCanInstallPackages}
iconType="plusInCircle"
href={getHref('add_integration_to_policy', {
pkgkey,
@ -379,17 +377,17 @@ export function Detail() {
onClick={handleAddIntegrationPolicyClick}
data-test-subj="addIntegrationPolicyButton"
tooltip={
!userCanInstallIntegrations
!userCanInstallPackages
? {
content: missingSecurityConfiguration ? (
<FormattedMessage
id="xpack.fleet.epm.addPackagePolicyButtonSecurityRequiredTooltip"
defaultMessage="To add Elastic Agent Integrations, you must have security enabled and have the superuser role. Contact your administrator."
defaultMessage="To add Elastic Agent Integrations, you must have security enabled and have the All privilege for Fleet. Contact your administrator."
/>
) : (
<FormattedMessage
id="xpack.fleet.epm.addPackagePolicyButtonPrivilegesRequiredTooltip"
defaultMessage="To add Elastic Agent integrations, you must have the superuser role. Contact your adminstrator."
defaultMessage="Elastic Agent Integrations require the All privilege for Fleet and All privilege for Integrations. Contact your administrator."
/>
),
}
@ -427,7 +425,7 @@ export function Detail() {
packageInfo,
updateAvailable,
packageInstallStatus,
userCanInstallIntegrations,
userCanInstallPackages,
getHref,
pkgkey,
integration,
@ -462,7 +460,7 @@ export function Detail() {
},
];
if (userCanInstallIntegrations && packageInstallStatus === InstallStatus.installed) {
if (canReadIntegrationPolicies && packageInstallStatus === InstallStatus.installed) {
tabs.push({
id: 'policies',
name: (
@ -498,7 +496,7 @@ export function Detail() {
});
}
if (userCanInstallIntegrations) {
if (canReadPackageSettings) {
tabs.push({
id: 'settings',
name: (
@ -540,7 +538,8 @@ export function Detail() {
panel,
getHref,
integration,
userCanInstallIntegrations,
canReadIntegrationPolicies,
canReadPackageSettings,
packageInstallStatus,
CustomAssets,
showCustomTab,
@ -628,7 +627,7 @@ export function Detail() {
type EuiButtonPropsFull = Parameters<typeof EuiButton>[0];
const EuiButtonWithTooltip: React.FC<
export const EuiButtonWithTooltip: React.FC<
EuiButtonPropsFull & { tooltip?: Partial<EuiToolTipProps> }
> = ({ tooltip: tooltipProps, ...buttonProps }) => {
return tooltipProps ? (

View file

@ -33,6 +33,7 @@ import {
AgentPolicyRefreshContext,
useUIExtension,
usePackageInstallations,
useAuthz,
} from '../../../../../hooks';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants';
import {
@ -104,6 +105,8 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
const { updatableIntegrations } = usePackageInstallations();
const agentEnrollmentFlyoutExtension = useUIExtension(name, 'agent-enrollment-flyout');
const canWriteIntegrationPolicies = useAuthz().integrations.writeIntegrationPolicies;
const packageAndAgentPolicies = useMemo((): Array<{
agentPolicy: GetAgentPoliciesResponseItem;
packagePolicy: InMemoryPackagePolicy;
@ -244,6 +247,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
policyId: agentPolicy.id,
packagePolicyId: packagePolicy.id,
})}?from=integrations-policy-list`}
isDisabled={!canWriteIntegrationPolicies}
>
<FormattedMessage
id="xpack.fleet.policyDetails.packagePoliciesTable.upgradeButton"
@ -329,7 +333,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
},
},
],
[getHref, showAddAgentHelpForPackagePolicyId, viewDataStep]
[getHref, showAddAgentHelpForPackagePolicyId, viewDataStep, canWriteIntegrationPolicies]
);
const noItemsMessage = useMemo(() => {

View file

@ -11,11 +11,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import type { PackageInfo, UpgradePackagePolicyDryRunResponse } from '../../../../../types';
import { InstallStatus } from '../../../../../types';
import {
useCapabilities,
useGetPackageInstallStatus,
useInstallPackage,
} from '../../../../../hooks';
import { useAuthz, useGetPackageInstallStatus, useInstallPackage } from '../../../../../hooks';
import { ConfirmPackageInstall } from './confirm_package_install';
@ -30,7 +26,7 @@ type InstallationButtonProps = Pick<PackageInfo, 'name' | 'title' | 'version'> &
};
export function InstallButton(props: InstallationButtonProps) {
const { name, numOfAssets, title, version } = props;
const hasWriteCapabilites = useCapabilities().write;
const canInstallPackages = useAuthz().integrations.installPackages;
const installPackage = useInstallPackage();
const getPackageInstallStatus = useGetPackageInstallStatus();
const { status: installationStatus } = getPackageInstallStatus(name);
@ -56,7 +52,7 @@ export function InstallButton(props: InstallationButtonProps) {
/>
);
return hasWriteCapabilites ? (
return canInstallPackages ? (
<Fragment>
<EuiButton iconType={'importAction'} isLoading={isInstalling} onClick={toggleInstallModal}>
{isInstalling ? (

View file

@ -12,11 +12,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { InstallStatus } from '../../../../../types';
import type { PackageInfo } from '../../../../../types';
import {
useCapabilities,
useGetPackageInstallStatus,
useUninstallPackage,
} from '../../../../../hooks';
import { useAuthz, useGetPackageInstallStatus, useUninstallPackage } from '../../../../../hooks';
import { ConfirmPackageUninstall } from './confirm_package_uninstall';
@ -34,7 +30,7 @@ export const UninstallButton: React.FunctionComponent<UninstallButtonProps> = ({
title,
version,
}) => {
const hasWriteCapabilites = useCapabilities().write;
const canRemovePackages = useAuthz().integrations.removePackages;
const uninstallPackage = useUninstallPackage();
const getPackageInstallStatus = useGetPackageInstallStatus();
const { status: installationStatus } = getPackageInstallStatus(name);
@ -59,7 +55,7 @@ export const UninstallButton: React.FunctionComponent<UninstallButtonProps> = ({
/>
);
return hasWriteCapabilites ? (
return canRemovePackages ? (
<>
<EuiButton
iconType={'trash'}

View file

@ -35,7 +35,7 @@ import {
useGetPackageInstallStatus,
sendUpgradePackagePolicy,
useStartServices,
useCapabilities,
useAuthz,
useLink,
} from '../../../../../hooks';
import { toMountPoint } from '../../../../../../../../../../../src/plugins/kibana_react/public';
@ -82,7 +82,7 @@ export const UpdateButton: React.FunctionComponent<UpdateButtonProps> = ({
const { getPath } = useLink();
const { notifications } = useStartServices();
const hasWriteCapabilites = useCapabilities().write;
const canUpgradePackages = useAuthz().integrations.upgradePackages;
const installPackage = useInstallPackage();
const getPackageInstallStatus = useGetPackageInstallStatus();
@ -287,7 +287,7 @@ export const UpdateButton: React.FunctionComponent<UpdateButtonProps> = ({
</EuiConfirmModal>
);
return hasWriteCapabilites ? (
return (
<>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
@ -297,6 +297,7 @@ export const UpdateButton: React.FunctionComponent<UpdateButtonProps> = ({
upgradePackagePolicies ? () => setIsUpdateModalVisible(true) : handleClickUpdate
}
data-test-subj="updatePackageBtn"
isDisabled={!canUpgradePackages}
>
<FormattedMessage
id="xpack.fleet.integrations.updatePackage.updatePackageButtonLabel"
@ -314,6 +315,7 @@ export const UpdateButton: React.FunctionComponent<UpdateButtonProps> = ({
},
}}
id="upgradePoliciesCheckbox"
disabled={!canUpgradePackages}
checked={upgradePackagePolicies}
onChange={handleUpgradePackagePoliciesChange}
label={i18n.translate(
@ -329,5 +331,5 @@ export const UpdateButton: React.FunctionComponent<UpdateButtonProps> = ({
{isUpdateModalVisible && updateModal}
</>
) : null;
);
};

View file

@ -11,17 +11,17 @@ import { EuiButtonEmpty } from '@elastic/eui';
import type { TutorialDirectoryHeaderLinkComponent } from 'src/plugins/home/public';
import { RedirectAppLinks } from '../../../../../../src/plugins/kibana_react/public';
import { useLink, useCapabilities, useStartServices } from '../../hooks';
import { useLink, useStartServices } from '../../hooks';
const TutorialDirectoryHeaderLink: TutorialDirectoryHeaderLinkComponent = memo(() => {
const { getHref } = useLink();
const { application } = useStartServices();
const { show: hasIngestManager } = useCapabilities();
const hasIntegrationsPermissions = application.capabilities.navLinks.integrations;
const [noticeState] = useState({
settingsDataLoaded: false,
});
return hasIngestManager && noticeState.settingsDataLoaded ? (
return hasIntegrationsPermissions && noticeState.settingsDataLoaded ? (
<RedirectAppLinks application={application}>
<EuiButtonEmpty size="s" iconType="link" flush="right" href={getHref('integrations')}>
<FormattedMessage

View file

@ -11,13 +11,14 @@ import { i18n } from '@kbn/i18n';
import { EuiText, EuiLink, EuiSpacer, EuiIcon } from '@elastic/eui';
import type { TutorialModuleNoticeComponent } from 'src/plugins/home/public';
import { useGetPackages, useLink, useCapabilities } from '../../hooks';
import { useGetPackages, useLink, useStartServices } from '../../hooks';
import { pkgKeyFromPackageInfo } from '../../services';
import { FLEET_APM_PACKAGE } from '../../../common/constants';
const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName }) => {
const { getHref } = useLink();
const { show: hasIngestManager } = useCapabilities();
const { application } = useStartServices();
const hasIntegrationsPermissions = application.capabilities.navLinks.integrations;
const { data: packagesData, isLoading } = useGetPackages();
const pkgInfo =
@ -25,7 +26,7 @@ const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName }
packagesData?.response &&
packagesData.response.find((pkg) => pkg.name === moduleName && pkg.name !== FLEET_APM_PACKAGE); // APM needs special handling
if (hasIngestManager && pkgInfo) {
if (hasIntegrationsPermissions && pkgInfo) {
return (
<>
<EuiSpacer />

View file

@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import type { AgentPolicy, InMemoryPackagePolicy } from '../types';
import { useAgentPolicyRefresh, useCapabilities, useLink } from '../hooks';
import { useAgentPolicyRefresh, useAuthz, useLink } from '../hooks';
import { AgentEnrollmentFlyout } from './agent_enrollment_flyout';
import { ContextMenuActions } from './context_menu_actions';
@ -36,7 +36,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
}) => {
const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false);
const { getHref } = useLink();
const hasWriteCapabilities = useCapabilities().write;
const canWriteIntegrationPolicies = useAuthz().integrations.writeIntegrationPolicies;
const refreshAgentPolicy = useAgentPolicyRefresh();
const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(defaultIsOpen);
@ -75,7 +75,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
]
: []),
<EuiContextMenuItem
disabled={!hasWriteCapabilities}
disabled={!canWriteIntegrationPolicies}
icon="pencil"
href={getHref('integration_policy_edit', {
packagePolicyId: packagePolicy.id,
@ -88,7 +88,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
/>
</EuiContextMenuItem>,
<EuiContextMenuItem
disabled={!packagePolicy.hasUpgrade}
disabled={!packagePolicy.hasUpgrade || !canWriteIntegrationPolicies}
icon="refresh"
href={upgradePackagePolicyHref}
key="packagePolicyUpgrade"
@ -113,7 +113,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
{(deletePackagePoliciesPrompt) => {
return (
<DangerEuiContextMenuItem
disabled={!hasWriteCapabilities}
disabled={!canWriteIntegrationPolicies}
icon="trash"
onClick={() => {
deletePackagePoliciesPrompt([packagePolicy.id], () => {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
export { useCapabilities } from './use_capabilities';
export { useAuthz } from './use_authz';
export { useStartServices } from './use_core';
export { useConfig, ConfigContext } from './use_config';
export { useKibanaVersion, KibanaVersionContext } from './use_kibana_version';

View file

@ -7,7 +7,8 @@
import { useStartServices } from './use_core';
export function useCapabilities() {
// Expose authz object, containing the privileges for Fleet and Integrations
export function useAuthz() {
const core = useStartServices();
return core.application.capabilities.fleet;
return core.authz;
}

View file

@ -10,10 +10,11 @@ import type { CheckPermissionsResponse, GenerateServiceTokenResponse } from '../
import { sendRequest, useRequest } from './use_request';
export const sendGetPermissionsCheck = () => {
export const sendGetPermissionsCheck = (fleetServerSetup?: boolean) => {
return sendRequest<CheckPermissionsResponse>({
path: appRoutesService.getCheckPermissionsPath(),
method: 'get',
query: { fleetServerSetup },
});
};

View file

@ -55,12 +55,18 @@ const configureStartServices = (services: MockedFleetStartServices): void => {
// Store the http for use by useRequest
setHttpClient(services.http);
// Set Fleet available capabilities
// Set Fleet and Integrations capabilities
services.application.capabilities = {
...services.application.capabilities,
// Fleet
fleetv2: {
read: true,
all: true,
},
// Integration
fleet: {
read: true,
write: true,
all: true,
},
};

View file

@ -14,7 +14,7 @@ export const createStartMock = (extensionsStorage: UIExtensionsStorage = {}): Mo
return {
isInitialized: jest.fn().mockResolvedValue(true),
registerExtension: createExtensionRegistrationCallback(extensionsStorage),
authz: Promise.resolve({
authz: {
fleet: {
all: true,
setup: true,
@ -33,6 +33,6 @@ export const createStartMock = (extensionsStorage: UIExtensionsStorage = {}): Mo
readIntegrationPolicies: true,
writeIntegrationPolicies: true,
},
}),
},
};
};

View file

@ -76,7 +76,7 @@ export interface FleetSetup {}
*/
export interface FleetStart {
/** Authorization for the current user */
authz: Promise<FleetAuthz>;
authz: FleetAuthz;
registerExtension: UIExtensionRegistrationCallback;
isInitialized: () => Promise<true>;
}
@ -261,33 +261,20 @@ export class FleetPlugin implements Plugin<FleetSetup, FleetStart, FleetSetupDep
view: 'package-detail-assets',
Component: LazyCustomLogsAssetsExtension,
});
return {
// Temporarily rely on superuser check to calculate authz. Once Kibana RBAC is in place for Fleet this should
// switch to a sync calculation based on `core.application.capabilites` properties.
authz: getPermissions()
.catch((e) => {
// eslint-disable-next-line no-console
console.warn(`Could not load Fleet permissions due to error: ${e}`);
return { success: false };
})
.then((permissionsResponse) => {
if (permissionsResponse?.success) {
// If superuser, give access to everything
return calculateAuthz({
fleet: { all: true, setup: true },
integrations: { all: true, read: true },
isSuperuser: true,
});
} else {
// All other users only get access to read integrations if they have the read privilege
const { capabilities } = core.application;
return calculateAuthz({
fleet: { all: false, setup: false },
integrations: { all: false, read: capabilities.fleet.read as boolean },
// capabilities.fleetv2 returns fleet privileges and capabilities.fleet returns integrations privileges
return {
authz: calculateAuthz({
fleet: {
all: capabilities.fleetv2.all as boolean,
setup: false,
},
integrations: {
all: capabilities.fleet.all as boolean,
read: capabilities.fleet.read as boolean,
},
isSuperuser: false,
});
}
}),
isInitialized: once(async () => {

View file

@ -7,6 +7,8 @@
import type { Observable } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import { i18n } from '@kbn/i18n';
import type {
CoreSetup,
CoreStart,
@ -222,14 +224,16 @@ export class FleetPlugin
registerEncryptedSavedObjects(deps.encryptedSavedObjects);
// Register feature
// TODO: Flesh out privileges
if (deps.features) {
deps.features.registerKibanaFeature({
id: PLUGIN_ID,
name: 'Fleet and Integrations',
id: `fleetv2`,
name: 'Fleet',
category: DEFAULT_APP_CATEGORIES.management,
app: [PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, 'kibana'],
app: [PLUGIN_ID],
catalogue: ['fleet'],
privilegesTooltip: i18n.translate('xpack.fleet.serverPlugin.privilegesTooltip', {
defaultMessage: 'All Spaces is required for Fleet access.',
}),
reserved: {
description:
'Privilege to setup Fleet packages and configured policies. Intended for use by the elastic/fleet-server service account only.',
@ -250,24 +254,64 @@ export class FleetPlugin
},
privileges: {
all: {
api: [`${PLUGIN_ID}-read`, `${PLUGIN_ID}-all`, `integrations-all`, `integrations-read`],
app: [PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, 'kibana'],
api: [`${PLUGIN_ID}-read`, `${PLUGIN_ID}-all`],
app: [PLUGIN_ID],
requireAllSpaces: true,
catalogue: ['fleet'],
savedObject: {
all: allSavedObjectTypes,
read: [],
},
ui: ['show', 'read', 'write'],
ui: ['read', 'all'],
},
read: {
api: [`${PLUGIN_ID}-read`, `integrations-read`],
app: [PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, 'kibana'],
catalogue: ['fleet'], // TODO: check if this is actually available to read user
api: [`${PLUGIN_ID}-read`],
app: [PLUGIN_ID],
catalogue: ['fleet'],
requireAllSpaces: true,
savedObject: {
all: [],
read: allSavedObjectTypes,
},
ui: ['show', 'read'],
ui: ['read'],
disabled: true,
},
},
});
deps.features.registerKibanaFeature({
id: 'fleet', // for BWC
name: 'Integrations',
category: DEFAULT_APP_CATEGORIES.management,
app: [INTEGRATIONS_PLUGIN_ID],
catalogue: ['fleet'],
privilegesTooltip: i18n.translate(
'xpack.fleet.serverPlugin.integrationsPrivilegesTooltip',
{
defaultMessage: 'All Spaces is required for All Integrations access.',
}
),
privileges: {
all: {
api: [`${INTEGRATIONS_PLUGIN_ID}-read`, `${INTEGRATIONS_PLUGIN_ID}-all`],
app: [INTEGRATIONS_PLUGIN_ID],
catalogue: ['fleet'],
requireAllSpaces: true,
savedObject: {
all: allSavedObjectTypes,
read: [],
},
ui: ['read', 'all'],
},
read: {
api: [`${INTEGRATIONS_PLUGIN_ID}-read`],
app: [INTEGRATIONS_PLUGIN_ID],
catalogue: ['fleet'],
savedObject: {
all: [],
read: allSavedObjectTypes,
},
ui: ['read'],
},
},
});

View file

@ -105,7 +105,7 @@ export const createAgentPolicyHandler: FleetRequestHandler<
TypeOf<typeof CreateAgentPolicyRequestSchema.query>,
TypeOf<typeof CreateAgentPolicyRequestSchema.body>
> = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
const soClient = context.fleet.epm.internalSoClient;
const esClient = context.core.elasticsearch.client.asInternalUser;
const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined;
const withSysMonitoring = request.query.sys_monitoring ?? false;
@ -229,11 +229,11 @@ export const deleteAgentPoliciesHandler: RequestHandler<
}
};
export const getFullAgentPolicy: RequestHandler<
export const getFullAgentPolicy: FleetRequestHandler<
TypeOf<typeof GetFullAgentPolicyRequestSchema.params>,
TypeOf<typeof GetFullAgentPolicyRequestSchema.query>
> = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
const soClient = context.fleet.epm.internalSoClient;
if (request.query.kubernetes === true) {
try {
@ -284,11 +284,11 @@ export const getFullAgentPolicy: RequestHandler<
}
};
export const downloadFullAgentPolicy: RequestHandler<
export const downloadFullAgentPolicy: FleetRequestHandler<
TypeOf<typeof GetFullAgentPolicyRequestSchema.params>,
TypeOf<typeof GetFullAgentPolicyRequestSchema.query>
> = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
const soClient = context.fleet.epm.internalSoClient;
const {
params: { agentPolicyId },
} = request;

View file

@ -6,14 +6,20 @@
*/
import type { RequestHandler } from 'src/core/server';
import type { TypeOf } from '@kbn/config-schema';
import { APP_API_ROUTES } from '../../constants';
import { appContextService } from '../../services';
import type { CheckPermissionsResponse, GenerateServiceTokenResponse } from '../../../common';
import { defaultIngestErrorHandler, GenerateServiceTokenError } from '../../errors';
import type { FleetAuthzRouter } from '../security';
import type { FleetRequestHandler } from '../../types';
import { CheckPermissionsRequestSchema } from '../../types';
export const getCheckPermissionsHandler: RequestHandler = async (context, request, response) => {
export const getCheckPermissionsHandler: FleetRequestHandler<
unknown,
TypeOf<typeof CheckPermissionsRequestSchema.query>
> = async (context, request, response) => {
const missingSecurityBody: CheckPermissionsResponse = {
success: false,
error: 'MISSING_SECURITY',
@ -22,25 +28,32 @@ export const getCheckPermissionsHandler: RequestHandler = async (context, reques
if (!appContextService.getSecurityLicense().isEnabled()) {
return response.ok({ body: missingSecurityBody });
} else {
const security = appContextService.getSecurity();
const user = security.authc.getCurrentUser(request);
// Defensively handle situation where user is undefined (should only happen when ES security is disabled)
// This should be covered by the `getSecurityLicense().isEnabled()` check above, but we leave this for robustness.
if (!user) {
return response.ok({
body: missingSecurityBody,
});
}
if (!user?.roles.includes('superuser')) {
if (!context.fleet.authz.fleet.all) {
return response.ok({
body: {
success: false,
error: 'MISSING_SUPERUSER_ROLE',
error: 'MISSING_PRIVILEGES',
} as CheckPermissionsResponse,
});
}
// check the manage_service_account cluster privilege
else if (request.query.fleetServerSetup) {
const esClient = context.core.elasticsearch.client.asCurrentUser;
const {
body: { has_all_requested: hasAllPrivileges },
} = await esClient.security.hasPrivileges({
body: { cluster: ['manage_service_account'] },
});
if (!hasAllPrivileges) {
return response.ok({
body: {
success: false,
error: 'MISSING_FLEET_SERVER_SETUP_PRIVILEGES',
} as CheckPermissionsResponse,
});
}
}
return response.ok({ body: { success: true } as CheckPermissionsResponse });
}
@ -77,9 +90,8 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
router.get(
{
path: APP_API_ROUTES.CHECK_PERMISSIONS_PATTERN,
validate: {},
validate: CheckPermissionsRequestSchema,
options: { tags: [] },
// no permission check for that route
},
getCheckPermissionsHandler
);

View file

@ -86,7 +86,7 @@ export const createPackagePolicyHandler: FleetRequestHandler<
undefined,
TypeOf<typeof CreatePackagePolicyRequestSchema.body>
> = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
const soClient = context.fleet.epm.internalSoClient;
const esClient = context.core.elasticsearch.client.asInternalUser;
const user = appContextService.getSecurity()?.authc.getCurrentUser(request) || undefined;
const { force, ...newPolicy } = request.body;

View file

@ -106,7 +106,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
path: PACKAGE_POLICY_API_ROUTES.DRYRUN_PATTERN,
validate: DryRunPackagePoliciesRequestSchema,
fleetAuthz: {
integrations: { writeIntegrationPolicies: true },
integrations: { readIntegrationPolicies: true },
},
},
dryRunUpgradePackagePolicyHandler

View file

@ -19,7 +19,10 @@ import { makeRouterWithFleetAuthz } from './security';
function getCheckPrivilegesMockedImplementation(kibanaRoles: string[]) {
return (checkPrivileges: CheckPrivilegesPayload) => {
const kibana = ((checkPrivileges?.kibana ?? []) as string[]).map((role: string) => {
return { authorized: kibanaRoles.includes(role) };
return {
privilege: role,
authorized: kibanaRoles.includes(role),
};
});
return Promise.resolve({
@ -141,14 +144,6 @@ describe('FleetAuthzRouter', () => {
path: '/api/fleet/test',
fleetAuthz: { fleet: { setup: true } },
};
it('allow users with superuser role', async () => {
expect(
await runTest({
security: { roles: ['superuser'] },
routeConfig,
})
).toEqual('ok');
});
it('allow users with fleet-setup role', async () => {
mockCheckPrivileges.mockImplementation(
@ -173,46 +168,12 @@ describe('FleetAuthzRouter', () => {
});
});
describe('with superuser privileges', () => {
const routeConfig = {
path: '/api/fleet/test',
fleetAuthz: { integrations: { uploadPackages: true } },
};
it('allow users with superuser role', async () => {
expect(
await runTest({
security: { roles: ['superuser'] },
routeConfig,
})
).toEqual('ok');
});
it('do not allow users without superuser role', async () => {
mockCheckPrivileges.mockImplementation(getCheckPrivilegesMockedImplementation([]));
expect(
await runTest({
security: { checkPrivilegesDynamically: mockCheckPrivileges },
routeConfig,
})
).toEqual('forbidden');
});
});
describe('with fleet role', () => {
const routeConfig = {
path: '/api/fleet/test',
fleetAuthz: { integrations: { readPackageInfo: true } },
};
it('allow users with superuser role', async () => {
expect(
await runTest({
security: { roles: ['superuser'] },
routeConfig,
})
).toEqual('ok');
});
it('allow users with all required fleet authz role', async () => {
mockCheckPrivileges.mockImplementation(
getCheckPrivilegesMockedImplementation(['api:integrations-read'])

View file

@ -16,10 +16,11 @@ import type {
} from 'src/core/server';
import type { FleetAuthz } from '../../common';
import { calculateAuthz } from '../../common';
import { calculateAuthz, INTEGRATIONS_PLUGIN_ID } from '../../common';
import { appContextService } from '../services';
import type { FleetRequestHandlerContext } from '../types';
import { PLUGIN_ID } from '../constants';
function checkSecurityEnabled() {
return appContextService.getSecurityLicense().isEnabled();
@ -44,61 +45,48 @@ export function checkSuperuser(req: KibanaRequest) {
return true;
}
async function checkFleetSetupPrivilege(req: KibanaRequest) {
if (!checkSecurityEnabled()) {
return false;
}
const security = appContextService.getSecurity();
if (security.authz.mode.useRbacForRequest(req)) {
const checkPrivileges = security.authz.checkPrivilegesDynamicallyWithRequest(req);
const { hasAllRequested } = await checkPrivileges(
{ kibana: [security.authz.actions.api.get('fleet-setup')] },
{ requireLoginAction: false } // exclude login access requirement
);
return !!hasAllRequested;
}
return true;
function getAuthorizationFromPrivileges(
kibanaPrivileges: Array<{
resource?: string;
privilege: string;
authorized: boolean;
}>,
searchPrivilege: string
) {
const privilege = kibanaPrivileges.find((p) => p.privilege.includes(searchPrivilege));
return privilege ? privilege.authorized : false;
}
export async function getAuthzFromRequest(req: KibanaRequest): Promise<FleetAuthz> {
const security = appContextService.getSecurity();
if (security.authz.mode.useRbacForRequest(req)) {
if (checkSuperuser(req)) {
// Superusers get access to everything
// Once we implement Kibana RBAC, remove this and use `checkPrivileges` exclusively
return calculateAuthz({
fleet: { all: true, setup: true },
integrations: { all: true, read: true },
isSuperuser: true,
});
} else if (await checkFleetSetupPrivilege(req)) {
// fleet-setup privilege only gets access to setup actions
return calculateAuthz({
fleet: { all: false, setup: true },
integrations: { all: false, read: false },
isSuperuser: false,
});
} else {
// All other users only get access to read integrations if they have the read privilege
const checkPrivileges = security.authz.checkPrivilegesDynamicallyWithRequest(req);
const { privileges } = await checkPrivileges({
kibana: [security.authz.actions.api.get('integrations-read')],
kibana: [
security.authz.actions.api.get(`${PLUGIN_ID}-all`),
security.authz.actions.api.get(`${INTEGRATIONS_PLUGIN_ID}-all`),
security.authz.actions.api.get(`${INTEGRATIONS_PLUGIN_ID}-read`),
security.authz.actions.api.get('fleet-setup'),
],
});
const fleetAllAuth = getAuthorizationFromPrivileges(privileges.kibana, `${PLUGIN_ID}-all`);
const intAllAuth = getAuthorizationFromPrivileges(
privileges.kibana,
`${INTEGRATIONS_PLUGIN_ID}-all`
);
const intReadAuth = getAuthorizationFromPrivileges(
privileges.kibana,
`${INTEGRATIONS_PLUGIN_ID}-read`
);
const fleetSetupAuth = getAuthorizationFromPrivileges(privileges.kibana, 'fleet-setup');
const [intRead] = privileges.kibana;
// Once we implement Kibana RBAC, use `checkPrivileges` for all privileges instead of only integrations.read
return calculateAuthz({
fleet: { all: false, setup: false },
integrations: { all: false, read: intRead.authorized },
isSuperuser: false,
fleet: { all: fleetAllAuth, setup: fleetSetupAuth },
integrations: { all: intAllAuth, read: intReadAuth },
isSuperuser: checkSuperuser(req),
});
}
}
return calculateAuthz({
fleet: { all: false, setup: false },

View file

@ -5,17 +5,17 @@
* 2.0.
*/
import type { RequestHandler } from 'src/core/server';
import type { TypeOf } from '@kbn/config-schema';
import { SETTINGS_API_ROUTES } from '../../constants';
import type { FleetRequestHandler } from '../../types';
import { PutSettingsRequestSchema, GetSettingsRequestSchema } from '../../types';
import { defaultIngestErrorHandler } from '../../errors';
import { settingsService, agentPolicyService, appContextService } from '../../services';
import type { FleetAuthzRouter } from '../security';
export const getSettingsHandler: RequestHandler = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
export const getSettingsHandler: FleetRequestHandler = async (context, request, response) => {
const soClient = context.fleet.epm.internalSoClient;
try {
const settings = await settingsService.getSettings(soClient);
@ -26,7 +26,7 @@ export const getSettingsHandler: RequestHandler = async (context, request, respo
} catch (error) {
if (error.isBoom && error.output.statusCode === 404) {
return response.notFound({
body: { message: `Setings not found` },
body: { message: `Settings not found` },
});
}
@ -34,12 +34,12 @@ export const getSettingsHandler: RequestHandler = async (context, request, respo
}
};
export const putSettingsHandler: RequestHandler<
export const putSettingsHandler: FleetRequestHandler<
undefined,
undefined,
TypeOf<typeof PutSettingsRequestSchema.body>
> = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
const soClient = context.fleet.epm.internalSoClient;
const esClient = context.core.elasticsearch.client.asInternalUser;
const user = await appContextService.getSecurity()?.authc.getCurrentUser(request);
@ -55,7 +55,7 @@ export const putSettingsHandler: RequestHandler<
} catch (error) {
if (error.isBoom && error.output.statusCode === 404) {
return response.notFound({
body: { message: `Setings not found` },
body: { message: `Settings not found` },
});
}

View file

@ -13,14 +13,15 @@ import type { ElasticsearchClient } from '../../../../../../src/core/server';
import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks';
import { FleetUnauthorizedError } from '../../errors';
import { checkSuperuser } from '../../routes/security';
import { getAuthzFromRequest } from '../../routes/security';
import type { FleetAuthz } from '../../../common';
import type { AgentClient } from './agent_service';
import { AgentServiceImpl } from './agent_service';
import { getAgentsByKuery, getAgentById } from './crud';
import { getAgentStatusById, getAgentStatusForAgentPolicy } from './status';
const mockCheckSuperuser = checkSuperuser as jest.Mock<boolean>;
const mockGetAuthzFromRequest = getAuthzFromRequest as jest.Mock<Promise<FleetAuthz>>;
const mockGetAgentsByKuery = getAgentsByKuery as jest.Mock;
const mockGetAgentById = getAgentById as jest.Mock;
const mockGetAgentStatusById = getAgentStatusById as jest.Mock;
@ -37,7 +38,30 @@ describe('AgentService', () => {
elasticsearchServiceMock.createElasticsearchClient()
).asScoped(httpServerMock.createKibanaRequest());
beforeEach(() => mockCheckSuperuser.mockReturnValue(false));
beforeEach(() =>
mockGetAuthzFromRequest.mockReturnValue(
Promise.resolve({
fleet: {
all: false,
setup: false,
readEnrollmentTokens: false,
readAgentPolicies: false,
},
integrations: {
readPackageInfo: false,
readInstalledPackages: false,
installPackages: false,
upgradePackages: false,
uploadPackages: false,
removePackages: false,
readPackageSettings: false,
writePackageSettings: false,
readIntegrationPolicies: false,
writeIntegrationPolicies: false,
},
})
)
);
it('rejects on listAgents', async () => {
await expect(agentClient.listAgents({ showInactive: true })).rejects.toThrowError(
@ -78,7 +102,30 @@ describe('AgentService', () => {
httpServerMock.createKibanaRequest()
);
beforeEach(() => mockCheckSuperuser.mockReturnValue(true));
beforeEach(() =>
mockGetAuthzFromRequest.mockReturnValue(
Promise.resolve({
fleet: {
all: true,
setup: true,
readEnrollmentTokens: true,
readAgentPolicies: true,
},
integrations: {
readPackageInfo: true,
readInstalledPackages: true,
installPackages: true,
upgradePackages: true,
uploadPackages: true,
removePackages: true,
readPackageSettings: true,
writePackageSettings: true,
readIntegrationPolicies: true,
writeIntegrationPolicies: true,
},
})
)
);
expectApisToCallServicesSuccessfully(mockEsClient, agentClient);
});

View file

@ -12,7 +12,7 @@ import type { ElasticsearchClient, KibanaRequest } from 'kibana/server';
import type { AgentStatus, ListWithKuery } from '../../types';
import type { Agent, GetAgentStatusResponse } from '../../../common';
import { checkSuperuser } from '../../routes/security';
import { getAuthzFromRequest } from '../../routes/security';
import { FleetUnauthorizedError } from '../../errors';
@ -123,8 +123,9 @@ export class AgentServiceImpl implements AgentService {
constructor(private readonly internalEsClient: ElasticsearchClient) {}
public asScoped(req: KibanaRequest) {
const preflightCheck = () => {
if (!checkSuperuser(req)) {
const preflightCheck = async () => {
const authz = await getAuthzFromRequest(req);
if (!authz.fleet.all) {
throw new FleetUnauthorizedError(
`User does not have adequate permissions to access Fleet agents.`
);

View file

@ -0,0 +1,14 @@
/*
* 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 { schema } from '@kbn/config-schema';
export const CheckPermissionsRequestSchema = {
query: schema.object({
fleetServerSetup: schema.maybe(schema.boolean()),
}),
};

View file

@ -15,3 +15,4 @@ export * from './output';
export * from './preconfiguration';
export * from './settings';
export * from './setup';
export * from './check_permissions';

View file

@ -34,7 +34,6 @@ describe('When using useEndpointPrivileges hook', () => {
let authenticatedUser: AuthenticatedUser;
let result: RenderResult<EndpointPrivileges>;
let unmount: ReturnType<typeof renderHook>['unmount'];
let releaseFleetAuthz: () => void;
let render: () => RenderHookResult<void, EndpointPrivileges>;
beforeEach(() => {
@ -46,14 +45,6 @@ describe('When using useEndpointPrivileges hook', () => {
licenseServiceMock.isPlatinumPlus.mockReturnValue(true);
// Add a daly to fleet service that provides authz information
const fleetAuthz = useKibana().services.fleet!.authz;
// Add a delay to the fleet Authz promise to test out the `loading` property
useKibana().services.fleet!.authz = new Promise((resolve) => {
releaseFleetAuthz = () => resolve(fleetAuthz);
});
render = () => {
const hookRenderResponse = renderHook(() => useEndpointPrivileges());
({ result, unmount } = hookRenderResponse);
@ -78,7 +69,6 @@ describe('When using useEndpointPrivileges hook', () => {
// Release the API response
await act(async () => {
releaseFleetAuthz();
await useKibana().services.fleet!.authz;
});

View file

@ -11653,7 +11653,6 @@
"xpack.fleet.packagePolicyValidation.nameRequiredErrorMessage": "名前が必要です",
"xpack.fleet.packagePolicyValidation.quoteStringErrorMessage": "*や&amp;などの特殊YAML文字で始まる文字列は二重引用符で囲む必要があります。",
"xpack.fleet.packagePolicyValidation.requiredErrorMessage": "{fieldName}が必要です",
"xpack.fleet.permissionDeniedErrorMessage": "Fleet へのアクセスが許可されていません。Fleet には{roleName}権限が必要です。",
"xpack.fleet.permissionDeniedErrorTitle": "パーミッションが拒否されました",
"xpack.fleet.permissionsRequestErrorMessageDescription": "Fleet アクセス権の確認中に問題が発生しました",
"xpack.fleet.permissionsRequestErrorMessageTitle": "アクセス権を確認できません",

View file

@ -11777,7 +11777,6 @@
"xpack.fleet.packagePolicyValidation.nameRequiredErrorMessage": "“名称”必填",
"xpack.fleet.packagePolicyValidation.quoteStringErrorMessage": "以特殊 YAML 字符(* 或 &amp;)开头的字符串需要使用双引号引起。",
"xpack.fleet.packagePolicyValidation.requiredErrorMessage": "“{fieldName}”必填",
"xpack.fleet.permissionDeniedErrorMessage": "您无权访问 Fleet。Fleet 需要 {roleName} 权限。",
"xpack.fleet.permissionDeniedErrorTitle": "权限被拒绝",
"xpack.fleet.permissionsRequestErrorMessageDescription": "检查 Fleet 权限时遇到问题",
"xpack.fleet.permissionsRequestErrorMessageTitle": "无法检查权限",

View file

@ -119,6 +119,7 @@ export default function ({ getService }: FtrProviderContext) {
'siem',
'securitySolutionCases',
'fleet',
'fleetv2',
].sort()
);
});

View file

@ -29,6 +29,7 @@ export default function ({ getService }: FtrProviderContext) {
canvas: ['all', 'read', 'minimal_all', 'minimal_read'],
maps: ['all', 'read', 'minimal_all', 'minimal_read'],
observabilityCases: ['all', 'read', 'minimal_all', 'minimal_read'],
fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'],
fleet: ['all', 'read', 'minimal_all', 'minimal_read'],
actions: ['all', 'read', 'minimal_all', 'minimal_read'],
stackAlerts: ['all', 'read', 'minimal_all', 'minimal_read'],

View file

@ -40,6 +40,7 @@ export default function ({ getService }: FtrProviderContext) {
ml: ['all', 'read', 'minimal_all', 'minimal_read'],
siem: ['all', 'read', 'minimal_all', 'minimal_read'],
securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read'],
fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'],
fleet: ['all', 'read', 'minimal_all', 'minimal_read'],
stackAlerts: ['all', 'read', 'minimal_all', 'minimal_read'],
actions: ['all', 'read', 'minimal_all', 'minimal_read'],