mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 03:01:21 -04:00
[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:
parent
a88d4a8f6b
commit
b12f70800c
54 changed files with 461 additions and 294 deletions
|
@ -27,7 +27,12 @@ export const getApplication = () => {
|
|||
management: {},
|
||||
navLinks: {},
|
||||
fleet: {
|
||||
write: true,
|
||||
read: true,
|
||||
all: true,
|
||||
},
|
||||
fleetv2: {
|
||||
read: true,
|
||||
all: true,
|
||||
},
|
||||
},
|
||||
applications$: of(applications),
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>"All"</EuiCode>,
|
||||
roleName2: <EuiCode>"Read"</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 (
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 />
|
||||
)}
|
||||
|
|
|
@ -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>"manage_service_account"</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</Panel>
|
||||
);
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 />;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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;
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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], () => {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 },
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,34 +261,21 @@ export class FleetPlugin implements Plugin<FleetSetup, FleetStart, FleetSetupDep
|
|||
view: 'package-detail-assets',
|
||||
Component: LazyCustomLogsAssetsExtension,
|
||||
});
|
||||
const { capabilities } = core.application;
|
||||
|
||||
// capabilities.fleetv2 returns fleet privileges and capabilities.fleet returns integrations privileges
|
||||
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 },
|
||||
isSuperuser: false,
|
||||
});
|
||||
}
|
||||
}),
|
||||
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 () => {
|
||||
const permissionsResponse = await getPermissions();
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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,60 +45,47 @@ 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')],
|
||||
});
|
||||
const checkPrivileges = security.authz.checkPrivilegesDynamicallyWithRequest(req);
|
||||
const { privileges } = await checkPrivileges({
|
||||
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,
|
||||
});
|
||||
}
|
||||
return calculateAuthz({
|
||||
fleet: { all: fleetAllAuth, setup: fleetSetupAuth },
|
||||
integrations: { all: intAllAuth, read: intReadAuth },
|
||||
isSuperuser: checkSuperuser(req),
|
||||
});
|
||||
}
|
||||
|
||||
return calculateAuthz({
|
||||
|
|
|
@ -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` },
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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.`
|
||||
);
|
||||
|
|
|
@ -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()),
|
||||
}),
|
||||
};
|
|
@ -15,3 +15,4 @@ export * from './output';
|
|||
export * from './preconfiguration';
|
||||
export * from './settings';
|
||||
export * from './setup';
|
||||
export * from './check_permissions';
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
@ -11653,7 +11653,6 @@
|
|||
"xpack.fleet.packagePolicyValidation.nameRequiredErrorMessage": "名前が必要です",
|
||||
"xpack.fleet.packagePolicyValidation.quoteStringErrorMessage": "*や&などの特殊YAML文字で始まる文字列は二重引用符で囲む必要があります。",
|
||||
"xpack.fleet.packagePolicyValidation.requiredErrorMessage": "{fieldName}が必要です",
|
||||
"xpack.fleet.permissionDeniedErrorMessage": "Fleet へのアクセスが許可されていません。Fleet には{roleName}権限が必要です。",
|
||||
"xpack.fleet.permissionDeniedErrorTitle": "パーミッションが拒否されました",
|
||||
"xpack.fleet.permissionsRequestErrorMessageDescription": "Fleet アクセス権の確認中に問題が発生しました",
|
||||
"xpack.fleet.permissionsRequestErrorMessageTitle": "アクセス権を確認できません",
|
||||
|
|
|
@ -11777,7 +11777,6 @@
|
|||
"xpack.fleet.packagePolicyValidation.nameRequiredErrorMessage": "“名称”必填",
|
||||
"xpack.fleet.packagePolicyValidation.quoteStringErrorMessage": "以特殊 YAML 字符(* 或 &)开头的字符串需要使用双引号引起。",
|
||||
"xpack.fleet.packagePolicyValidation.requiredErrorMessage": "“{fieldName}”必填",
|
||||
"xpack.fleet.permissionDeniedErrorMessage": "您无权访问 Fleet。Fleet 需要 {roleName} 权限。",
|
||||
"xpack.fleet.permissionDeniedErrorTitle": "权限被拒绝",
|
||||
"xpack.fleet.permissionsRequestErrorMessageDescription": "检查 Fleet 权限时遇到问题",
|
||||
"xpack.fleet.permissionsRequestErrorMessageTitle": "无法检查权限",
|
||||
|
|
|
@ -119,6 +119,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'siem',
|
||||
'securitySolutionCases',
|
||||
'fleet',
|
||||
'fleetv2',
|
||||
].sort()
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue