Allow users with read access to view Integrations app (#113925)
This commit is contained in:
parent
9f07f71500
commit
3ab04b67f8
|
@ -7,12 +7,10 @@
|
|||
|
||||
import React, { memo, useEffect, useState } from 'react';
|
||||
import type { AppMountParameters } from 'kibana/public';
|
||||
import { EuiCode, EuiEmptyPrompt, EuiErrorBoundary, EuiPanel, EuiPortal } from '@elastic/eui';
|
||||
import { EuiErrorBoundary, EuiPortal } from '@elastic/eui';
|
||||
import type { History } from 'history';
|
||||
import { Router, Redirect, Route, Switch } from 'react-router-dom';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import styled from 'styled-components';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
|
||||
import {
|
||||
|
@ -49,29 +47,23 @@ const ErrorLayout = ({ children }: { children: JSX.Element }) => (
|
|||
</EuiErrorBoundary>
|
||||
);
|
||||
|
||||
const Panel = styled(EuiPanel)`
|
||||
max-width: 500px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
export const WithPermissionsAndSetup: React.FC = memo(({ children }) => {
|
||||
useBreadcrumbs('integrations');
|
||||
|
||||
const [isPermissionsLoading, setIsPermissionsLoading] = useState<boolean>(false);
|
||||
const [permissionsError, setPermissionsError] = useState<string>();
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [initializationError, setInitializationError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setPermissionsError(undefined);
|
||||
setIsInitialized(false);
|
||||
setInitializationError(null);
|
||||
try {
|
||||
// Attempt Fleet Setup if user has permissions, otherwise skip
|
||||
setIsPermissionsLoading(true);
|
||||
const permissionsResponse = await sendGetPermissionsCheck();
|
||||
setIsPermissionsLoading(false);
|
||||
|
||||
if (permissionsResponse.data?.success) {
|
||||
try {
|
||||
const setupResponse = await sendSetup();
|
||||
|
@ -83,69 +75,20 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => {
|
|||
}
|
||||
setIsInitialized(true);
|
||||
} else {
|
||||
setPermissionsError(permissionsResponse.data?.error || 'REQUEST_ERROR');
|
||||
setIsInitialized(true);
|
||||
}
|
||||
} catch (err) {
|
||||
setPermissionsError('REQUEST_ERROR');
|
||||
} catch {
|
||||
// If there's an error checking permissions, default to proceeding without running setup
|
||||
// User will only have access to EPM endpoints if they actually have permission
|
||||
setIsInitialized(true);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
if (isPermissionsLoading || permissionsError) {
|
||||
if (isPermissionsLoading) {
|
||||
return (
|
||||
<ErrorLayout>
|
||||
{isPermissionsLoading ? (
|
||||
<Loading />
|
||||
) : permissionsError === 'REQUEST_ERROR' ? (
|
||||
<Error
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.permissionsRequestErrorMessageTitle"
|
||||
defaultMessage="Unable to check permissions"
|
||||
/>
|
||||
}
|
||||
error={i18n.translate('xpack.fleet.permissionsRequestErrorMessageDescription', {
|
||||
defaultMessage: 'There was a problem checking Fleet permissions',
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<Panel>
|
||||
<EuiEmptyPrompt
|
||||
iconType="securityApp"
|
||||
title={
|
||||
<h2>
|
||||
{permissionsError === 'MISSING_SUPERUSER_ROLE' ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.permissionDeniedErrorTitle"
|
||||
defaultMessage="Permission denied"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.securityRequiredErrorTitle"
|
||||
defaultMessage="Security is not enabled"
|
||||
/>
|
||||
)}
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
{permissionsError === 'MISSING_SUPERUSER_ROLE' ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrationsPermissionDeniedErrorMessage"
|
||||
defaultMessage="You are not authorized to access Integrations. Integrations requires {roleName} privileges."
|
||||
values={{ roleName: <EuiCode>superuser</EuiCode> }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrationsSecurityRequiredErrorMessage"
|
||||
defaultMessage="You must enable security in Kibana and Elasticsearch to use Integrations."
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</Panel>
|
||||
)}
|
||||
<Loading />
|
||||
</ErrorLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import React, { useEffect, useState } from 'react';
|
|||
import { Redirect } from 'react-router-dom';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui';
|
||||
import { groupBy } from 'lodash';
|
||||
|
||||
import { Loading, Error, ExtensionWrapper } from '../../../../../components';
|
||||
|
||||
|
@ -67,8 +68,26 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => {
|
|||
id,
|
||||
type,
|
||||
}));
|
||||
const { savedObjects } = await savedObjectsClient.bulkGet(objectsToGet);
|
||||
setAssetsSavedObjects(savedObjects as AssetSavedObject[]);
|
||||
|
||||
// We don't have an API to know which SO types a user has access to, so instead we make a request for each
|
||||
// SO type and ignore the 403 errors
|
||||
const objectsByType = await Promise.all(
|
||||
Object.entries(groupBy(objectsToGet, 'type')).map(([type, objects]) =>
|
||||
savedObjectsClient
|
||||
.bulkGet(objects)
|
||||
// Ignore privilege errors
|
||||
.catch((e: any) => {
|
||||
if (e?.body?.statusCode === 403) {
|
||||
return { savedObjects: [] };
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
})
|
||||
.then(({ savedObjects }) => savedObjects as AssetSavedObject[])
|
||||
)
|
||||
);
|
||||
|
||||
setAssetsSavedObjects(objectsByType.flat());
|
||||
} catch (e) {
|
||||
setFetchError(e);
|
||||
} finally {
|
||||
|
|
|
@ -11,6 +11,7 @@ import { act, cleanup } from '@testing-library/react';
|
|||
|
||||
import { INTEGRATIONS_ROUTING_PATHS, pagePathGetters } from '../../../../constants';
|
||||
import type {
|
||||
CheckPermissionsResponse,
|
||||
GetAgentPoliciesResponse,
|
||||
GetFleetStatusResponse,
|
||||
GetInfoResponse,
|
||||
|
@ -23,6 +24,7 @@ import type {
|
|||
} from '../../../../../../../common/types/models';
|
||||
import {
|
||||
agentPolicyRouteService,
|
||||
appRoutesService,
|
||||
epmRouteService,
|
||||
fleetSetupRouteService,
|
||||
packagePolicyRouteService,
|
||||
|
@ -260,6 +262,7 @@ interface EpmPackageDetailsResponseProvidersMock {
|
|||
fleetSetup: jest.MockedFunction<() => GetFleetStatusResponse>;
|
||||
packagePolicyList: jest.MockedFunction<() => GetPackagePoliciesResponse>;
|
||||
agentPolicyList: jest.MockedFunction<() => GetAgentPoliciesResponse>;
|
||||
appCheckPermissions: jest.MockedFunction<() => CheckPermissionsResponse>;
|
||||
}
|
||||
|
||||
const mockApiCalls = (
|
||||
|
@ -740,6 +743,10 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos
|
|||
},
|
||||
};
|
||||
|
||||
const appCheckPermissionsResponse: CheckPermissionsResponse = {
|
||||
success: true,
|
||||
};
|
||||
|
||||
const mockedApiInterface: MockedApi<EpmPackageDetailsResponseProvidersMock> = {
|
||||
waitForApi() {
|
||||
return new Promise((resolve) => {
|
||||
|
@ -757,6 +764,7 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos
|
|||
fleetSetup: jest.fn().mockReturnValue(agentsSetupResponse),
|
||||
packagePolicyList: jest.fn().mockReturnValue(packagePoliciesResponse),
|
||||
agentPolicyList: jest.fn().mockReturnValue(agentPoliciesResponse),
|
||||
appCheckPermissions: jest.fn().mockReturnValue(appCheckPermissionsResponse),
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -792,6 +800,11 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos
|
|||
return mockedApiInterface.responseProvider.epmGetStats();
|
||||
}
|
||||
|
||||
if (path === appRoutesService.getCheckPermissionsPath()) {
|
||||
markApiCallAsHandled();
|
||||
return mockedApiInterface.responseProvider.appCheckPermissions();
|
||||
}
|
||||
|
||||
const err = new Error(`API [GET ${path}] is not MOCKED!`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
|
|
|
@ -8,10 +8,12 @@ import type { ReactEventHandler } from 'react';
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Redirect, Route, Switch, useLocation, useParams, useHistory } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import type { EuiToolTipProps } from '@elastic/eui';
|
||||
import {
|
||||
EuiBetaBadge,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListDescription,
|
||||
EuiDescriptionListTitle,
|
||||
|
@ -19,6 +21,7 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
@ -30,6 +33,7 @@ import {
|
|||
useUIExtension,
|
||||
useBreadcrumbs,
|
||||
useStartServices,
|
||||
usePermissionCheck,
|
||||
} from '../../../../hooks';
|
||||
import {
|
||||
PLUGIN_ID,
|
||||
|
@ -61,6 +65,7 @@ import { OverviewPage } from './overview';
|
|||
import { PackagePoliciesPage } from './policies';
|
||||
import { SettingsPage } from './settings';
|
||||
import { CustomViewPage } from './custom';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
export interface DetailParams {
|
||||
|
@ -95,7 +100,11 @@ export function Detail() {
|
|||
const { getId: getAgentPolicyId } = useAgentPolicyContext();
|
||||
const { pkgkey, panel } = useParams<DetailParams>();
|
||||
const { getHref } = useLink();
|
||||
const hasWriteCapabilites = useCapabilities().write;
|
||||
const hasWriteCapabilities = useCapabilities().write;
|
||||
const permissionCheck = usePermissionCheck();
|
||||
const missingSecurityConfiguration =
|
||||
!permissionCheck.data?.success && permissionCheck.data?.error === 'MISSING_SECURITY';
|
||||
const userCanInstallIntegrations = hasWriteCapabilities && permissionCheck.data?.success;
|
||||
const history = useHistory();
|
||||
const { pathname, search, hash } = useLocation();
|
||||
const queryParams = useMemo(() => new URLSearchParams(search), [search]);
|
||||
|
@ -127,9 +136,11 @@ export function Detail() {
|
|||
const {
|
||||
data: packageInfoData,
|
||||
error: packageInfoError,
|
||||
isLoading,
|
||||
isLoading: packageInfoLoading,
|
||||
} = useGetPackageInfoByKey(pkgkey);
|
||||
|
||||
const isLoading = packageInfoLoading || permissionCheck.isLoading;
|
||||
|
||||
const showCustomTab =
|
||||
useUIExtension(packageInfoData?.response.name ?? '', 'package-detail-custom') !== undefined;
|
||||
|
||||
|
@ -327,10 +338,9 @@ export function Detail() {
|
|||
{ isDivider: true },
|
||||
{
|
||||
content: (
|
||||
// eslint-disable-next-line @elastic/eui/href-or-on-click
|
||||
<EuiButton
|
||||
<EuiButtonWithTooltip
|
||||
fill
|
||||
isDisabled={!hasWriteCapabilites}
|
||||
isDisabled={!userCanInstallIntegrations}
|
||||
iconType="plusInCircle"
|
||||
href={getHref('add_integration_to_policy', {
|
||||
pkgkey,
|
||||
|
@ -341,6 +351,23 @@ export function Detail() {
|
|||
})}
|
||||
onClick={handleAddIntegrationPolicyClick}
|
||||
data-test-subj="addIntegrationPolicyButton"
|
||||
tooltip={
|
||||
!userCanInstallIntegrations
|
||||
? {
|
||||
content: missingSecurityConfiguration ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epm.addPackagePolicyButtonSecurityRequiredTooltip"
|
||||
defaultMessage="To add Elastic Agent Integrations, you must have security enabled and have the minimum required privileges. Contact your administrator."
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epm.addPackagePolicyButtonPrivilegesRequiredTooltip"
|
||||
defaultMessage="To add Elastic Agent integrations, you must have the minimum required privileges. Contact your adminstrator."
|
||||
/>
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epm.addPackagePolicyButtonText"
|
||||
|
@ -349,7 +376,7 @@ export function Detail() {
|
|||
packageName: integrationInfo?.title || packageInfo.title,
|
||||
}}
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiButtonWithTooltip>
|
||||
),
|
||||
},
|
||||
].map((item, index) => (
|
||||
|
@ -370,16 +397,17 @@ export function Detail() {
|
|||
</>
|
||||
) : undefined,
|
||||
[
|
||||
getHref,
|
||||
handleAddIntegrationPolicyClick,
|
||||
hasWriteCapabilites,
|
||||
integration,
|
||||
integrationInfo,
|
||||
packageInfo,
|
||||
packageInstallStatus,
|
||||
pkgkey,
|
||||
updateAvailable,
|
||||
packageInstallStatus,
|
||||
userCanInstallIntegrations,
|
||||
getHref,
|
||||
pkgkey,
|
||||
integration,
|
||||
agentPolicyIdFromContext,
|
||||
handleAddIntegrationPolicyClick,
|
||||
missingSecurityConfiguration,
|
||||
integrationInfo?.title,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -407,7 +435,7 @@ export function Detail() {
|
|||
},
|
||||
];
|
||||
|
||||
if (packageInstallStatus === InstallStatus.installed) {
|
||||
if (userCanInstallIntegrations && packageInstallStatus === InstallStatus.installed) {
|
||||
tabs.push({
|
||||
id: 'policies',
|
||||
name: (
|
||||
|
@ -443,21 +471,23 @@ export function Detail() {
|
|||
});
|
||||
}
|
||||
|
||||
tabs.push({
|
||||
id: 'settings',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epm.packageDetailsNav.settingsLinkText"
|
||||
defaultMessage="Settings"
|
||||
/>
|
||||
),
|
||||
isSelected: panel === 'settings',
|
||||
'data-test-subj': `tab-settings`,
|
||||
href: getHref('integration_details_settings', {
|
||||
pkgkey: packageInfoKey,
|
||||
...(integration ? { integration } : {}),
|
||||
}),
|
||||
});
|
||||
if (userCanInstallIntegrations) {
|
||||
tabs.push({
|
||||
id: 'settings',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epm.packageDetailsNav.settingsLinkText"
|
||||
defaultMessage="Settings"
|
||||
/>
|
||||
),
|
||||
isSelected: panel === 'settings',
|
||||
'data-test-subj': `tab-settings`,
|
||||
href: getHref('integration_details_settings', {
|
||||
pkgkey: packageInfoKey,
|
||||
...(integration ? { integration } : {}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (showCustomTab) {
|
||||
tabs.push({
|
||||
|
@ -478,13 +508,55 @@ export function Detail() {
|
|||
}
|
||||
|
||||
return tabs;
|
||||
}, [packageInfo, panel, getHref, integration, packageInstallStatus, showCustomTab, CustomAssets]);
|
||||
}, [
|
||||
packageInfo,
|
||||
panel,
|
||||
getHref,
|
||||
integration,
|
||||
userCanInstallIntegrations,
|
||||
packageInstallStatus,
|
||||
CustomAssets,
|
||||
showCustomTab,
|
||||
]);
|
||||
|
||||
const securityCallout = missingSecurityConfiguration ? (
|
||||
<>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
iconType="lock"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epm.packageDetailsSecurityRequiredCalloutTitle"
|
||||
defaultMessage="Security needs to be enabled in order to add Elastic Agent integrations"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epm.packageDetailsSecurityRequiredCalloutDescription"
|
||||
defaultMessage="In order to fully use Fleet, you must enable Elasticsearch and Kibana security features.
|
||||
Follow the {guideLink} to enable security."
|
||||
values={{
|
||||
guideLink: (
|
||||
<a href={services.http.basePath.prepend('/app/fleet')}>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epm.packageDetailsSecurityRequiredCalloutDescriptionGuideLink"
|
||||
defaultMessage="steps in this guide"
|
||||
/>
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<WithHeaderLayout
|
||||
leftColumn={headerLeftContent}
|
||||
rightColumn={headerRightContent}
|
||||
rightColumnGrow={false}
|
||||
topContent={securityCallout}
|
||||
tabs={headerTabs}
|
||||
tabsClassName="fleet__epm__shiftNavTabs"
|
||||
>
|
||||
|
@ -526,3 +598,16 @@ export function Detail() {
|
|||
</WithHeaderLayout>
|
||||
);
|
||||
}
|
||||
|
||||
type EuiButtonPropsFull = Parameters<typeof EuiButton>[0];
|
||||
|
||||
const EuiButtonWithTooltip: React.FC<EuiButtonPropsFull & { tooltip?: Partial<EuiToolTipProps> }> =
|
||||
({ tooltip: tooltipProps, ...buttonProps }) => {
|
||||
return tooltipProps ? (
|
||||
<EuiToolTip {...tooltipProps}>
|
||||
<EuiButton {...buttonProps} />
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<EuiButton {...buttonProps} />
|
||||
);
|
||||
};
|
||||
|
|
|
@ -43,6 +43,7 @@ export interface HeaderProps {
|
|||
leftColumn?: JSX.Element;
|
||||
rightColumn?: JSX.Element;
|
||||
rightColumnGrow?: EuiFlexItemProps['grow'];
|
||||
topContent?: JSX.Element;
|
||||
tabs?: Array<Omit<EuiTabProps, 'name'> & { name?: JSX.Element | string }>;
|
||||
tabsClassName?: string;
|
||||
'data-test-subj'?: string;
|
||||
|
@ -61,6 +62,7 @@ export const Header: React.FC<HeaderProps> = ({
|
|||
leftColumn,
|
||||
rightColumn,
|
||||
rightColumnGrow,
|
||||
topContent,
|
||||
tabs,
|
||||
maxWidth,
|
||||
tabsClassName,
|
||||
|
@ -68,6 +70,7 @@ export const Header: React.FC<HeaderProps> = ({
|
|||
}) => (
|
||||
<Container data-test-subj={dataTestSubj}>
|
||||
<Wrapper maxWidth={maxWidth}>
|
||||
{topContent}
|
||||
<HeaderColumns
|
||||
leftColumn={leftColumn}
|
||||
rightColumn={rightColumn}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { appRoutesService } from '../../services';
|
||||
import type { CheckPermissionsResponse, GenerateServiceTokenResponse } from '../../types';
|
||||
|
||||
import { sendRequest } from './use_request';
|
||||
import { sendRequest, useRequest } from './use_request';
|
||||
|
||||
export const sendGetPermissionsCheck = () => {
|
||||
return sendRequest<CheckPermissionsResponse>({
|
||||
|
@ -23,3 +23,10 @@ export const sendGenerateServiceToken = () => {
|
|||
method: 'post',
|
||||
});
|
||||
};
|
||||
|
||||
export const usePermissionCheck = () => {
|
||||
return useRequest<CheckPermissionsResponse>({
|
||||
path: appRoutesService.getCheckPermissionsPath(),
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
|
|
|
@ -37,7 +37,8 @@ export const createAppContextStartContractMock = (): FleetAppContext => {
|
|||
data: dataPluginMock.createStartContract(),
|
||||
encryptedSavedObjectsStart: encryptedSavedObjectsMock.createStart(),
|
||||
savedObjects: savedObjectsServiceMock.createStartContract(),
|
||||
security: securityMock.createStart(),
|
||||
securitySetup: securityMock.createSetup(),
|
||||
securityStart: securityMock.createStart(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
isProductionMode: true,
|
||||
configInitialValue: {
|
||||
|
|
|
@ -37,6 +37,7 @@ import {
|
|||
AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
PACKAGES_SAVED_OBJECT_TYPE,
|
||||
ASSETS_SAVED_OBJECT_TYPE,
|
||||
AGENT_SAVED_OBJECT_TYPE,
|
||||
ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
|
||||
PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE,
|
||||
|
@ -103,7 +104,8 @@ export interface FleetAppContext {
|
|||
data: DataPluginStart;
|
||||
encryptedSavedObjectsStart?: EncryptedSavedObjectsPluginStart;
|
||||
encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup;
|
||||
security?: SecurityPluginStart;
|
||||
securitySetup?: SecurityPluginSetup;
|
||||
securityStart?: SecurityPluginStart;
|
||||
config$?: Observable<FleetConfigType>;
|
||||
configInitialValue: FleetConfigType;
|
||||
savedObjects: SavedObjectsServiceStart;
|
||||
|
@ -122,6 +124,7 @@ const allSavedObjectTypes = [
|
|||
AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
PACKAGES_SAVED_OBJECT_TYPE,
|
||||
ASSETS_SAVED_OBJECT_TYPE,
|
||||
AGENT_SAVED_OBJECT_TYPE,
|
||||
ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
|
||||
PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE,
|
||||
|
@ -164,14 +167,15 @@ export class FleetPlugin
|
|||
private licensing$!: Observable<ILicense>;
|
||||
private config$: Observable<FleetConfigType>;
|
||||
private configInitialValue: FleetConfigType;
|
||||
private cloud: CloudSetup | undefined;
|
||||
private logger: Logger | undefined;
|
||||
private cloud?: CloudSetup;
|
||||
private logger?: Logger;
|
||||
|
||||
private isProductionMode: FleetAppContext['isProductionMode'];
|
||||
private kibanaVersion: FleetAppContext['kibanaVersion'];
|
||||
private kibanaBranch: FleetAppContext['kibanaBranch'];
|
||||
private httpSetup: HttpServiceSetup | undefined;
|
||||
private encryptedSavedObjectsSetup: EncryptedSavedObjectsPluginSetup | undefined;
|
||||
private httpSetup?: HttpServiceSetup;
|
||||
private securitySetup?: SecurityPluginSetup;
|
||||
private encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup;
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.config$ = this.initializerContext.config.create<FleetConfigType>();
|
||||
|
@ -187,6 +191,7 @@ export class FleetPlugin
|
|||
this.licensing$ = deps.licensing.license$;
|
||||
this.encryptedSavedObjectsSetup = deps.encryptedSavedObjects;
|
||||
this.cloud = deps.cloud;
|
||||
this.securitySetup = deps.security;
|
||||
const config = this.configInitialValue;
|
||||
|
||||
registerSavedObjects(core.savedObjects, deps.encryptedSavedObjects);
|
||||
|
@ -233,6 +238,10 @@ export class FleetPlugin
|
|||
|
||||
// Always register app routes for permissions checking
|
||||
registerAppRoutes(router);
|
||||
// Allow read-only users access to endpoints necessary for Integrations UI
|
||||
// Only some endpoints require superuser so we pass a raw IRouter here
|
||||
registerEPMRoutes(router);
|
||||
|
||||
// For all the routes we enforce the user to have role superuser
|
||||
const routerSuperuserOnly = makeRouterEnforcingSuperuser(router);
|
||||
// Register rest of routes only if security is enabled
|
||||
|
@ -243,7 +252,6 @@ export class FleetPlugin
|
|||
registerOutputRoutes(routerSuperuserOnly);
|
||||
registerSettingsRoutes(routerSuperuserOnly);
|
||||
registerDataStreamRoutes(routerSuperuserOnly);
|
||||
registerEPMRoutes(routerSuperuserOnly);
|
||||
registerPreconfigurationRoutes(routerSuperuserOnly);
|
||||
|
||||
// Conditional config routes
|
||||
|
@ -260,7 +268,8 @@ export class FleetPlugin
|
|||
data: plugins.data,
|
||||
encryptedSavedObjectsStart: plugins.encryptedSavedObjects,
|
||||
encryptedSavedObjectsSetup: this.encryptedSavedObjectsSetup,
|
||||
security: plugins.security,
|
||||
securitySetup: this.securitySetup,
|
||||
securityStart: plugins.security,
|
||||
configInitialValue: this.configInitialValue,
|
||||
config$: this.config$,
|
||||
savedObjects: core.savedObjects,
|
||||
|
|
|
@ -17,13 +17,15 @@ export const getCheckPermissionsHandler: RequestHandler = async (context, reques
|
|||
success: false,
|
||||
error: 'MISSING_SECURITY',
|
||||
};
|
||||
const body: CheckPermissionsResponse = { success: true };
|
||||
try {
|
||||
const security = await appContextService.getSecurity();
|
||||
|
||||
if (!appContextService.hasSecurity() || !appContextService.getSecurityLicense().isEnabled()) {
|
||||
return response.ok({ body: missingSecurityBody });
|
||||
} else {
|
||||
const security = appContextService.getSecurity();
|
||||
const user = security.authc.getCurrentUser(request);
|
||||
|
||||
// when ES security is disabled, but Kibana security plugin is not explicitly disabled,
|
||||
// `authc.getCurrentUser()` does not error, instead it comes back as `null`
|
||||
// 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,
|
||||
|
@ -31,20 +33,15 @@ export const getCheckPermissionsHandler: RequestHandler = async (context, reques
|
|||
}
|
||||
|
||||
if (!user?.roles.includes('superuser')) {
|
||||
body.success = false;
|
||||
body.error = 'MISSING_SUPERUSER_ROLE';
|
||||
return response.ok({
|
||||
body,
|
||||
body: {
|
||||
success: false,
|
||||
error: 'MISSING_SUPERUSER_ROLE',
|
||||
} as CheckPermissionsResponse,
|
||||
});
|
||||
}
|
||||
|
||||
return response.ok({ body: { success: true } });
|
||||
} catch (e) {
|
||||
// when Kibana security plugin is explicitly disabled,
|
||||
// `appContextService.getSecurity()` returns an error, so we catch it here
|
||||
return response.ok({
|
||||
body: missingSecurityBody,
|
||||
});
|
||||
return response.ok({ body: { success: true } as CheckPermissionsResponse });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
GetStatsRequestSchema,
|
||||
UpdatePackageRequestSchema,
|
||||
} from '../../types';
|
||||
import { enforceSuperUser } from '../security';
|
||||
|
||||
import {
|
||||
getCategoriesHandler,
|
||||
|
@ -60,7 +61,7 @@ export const registerRoutes = (router: IRouter) => {
|
|||
{
|
||||
path: EPM_API_ROUTES.LIMITED_LIST_PATTERN,
|
||||
validate: false,
|
||||
options: { tags: [`access:${PLUGIN_ID}`] },
|
||||
options: { tags: [`access:${PLUGIN_ID}-read`] },
|
||||
},
|
||||
getLimitedListHandler
|
||||
);
|
||||
|
@ -69,7 +70,7 @@ export const registerRoutes = (router: IRouter) => {
|
|||
{
|
||||
path: EPM_API_ROUTES.STATS_PATTERN,
|
||||
validate: GetStatsRequestSchema,
|
||||
options: { tags: [`access:${PLUGIN_ID}`] },
|
||||
options: { tags: [`access:${PLUGIN_ID}-read`] },
|
||||
},
|
||||
getStatsHandler
|
||||
);
|
||||
|
@ -98,7 +99,7 @@ export const registerRoutes = (router: IRouter) => {
|
|||
validate: UpdatePackageRequestSchema,
|
||||
options: { tags: [`access:${PLUGIN_ID}-all`] },
|
||||
},
|
||||
updatePackageHandler
|
||||
enforceSuperUser(updatePackageHandler)
|
||||
);
|
||||
|
||||
router.post(
|
||||
|
@ -107,7 +108,7 @@ export const registerRoutes = (router: IRouter) => {
|
|||
validate: InstallPackageFromRegistryRequestSchema,
|
||||
options: { tags: [`access:${PLUGIN_ID}-all`] },
|
||||
},
|
||||
installPackageFromRegistryHandler
|
||||
enforceSuperUser(installPackageFromRegistryHandler)
|
||||
);
|
||||
|
||||
router.post(
|
||||
|
@ -116,7 +117,7 @@ export const registerRoutes = (router: IRouter) => {
|
|||
validate: BulkUpgradePackagesFromRegistryRequestSchema,
|
||||
options: { tags: [`access:${PLUGIN_ID}-all`] },
|
||||
},
|
||||
bulkInstallPackagesFromRegistryHandler
|
||||
enforceSuperUser(bulkInstallPackagesFromRegistryHandler)
|
||||
);
|
||||
|
||||
router.post(
|
||||
|
@ -132,7 +133,7 @@ export const registerRoutes = (router: IRouter) => {
|
|||
},
|
||||
},
|
||||
},
|
||||
installPackageByUploadHandler
|
||||
enforceSuperUser(installPackageByUploadHandler)
|
||||
);
|
||||
|
||||
router.delete(
|
||||
|
@ -141,6 +142,6 @@ export const registerRoutes = (router: IRouter) => {
|
|||
validate: DeletePackageRequestSchema,
|
||||
options: { tags: [`access:${PLUGIN_ID}-all`] },
|
||||
},
|
||||
deletePackageHandler
|
||||
enforceSuperUser(deletePackageHandler)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -13,6 +13,14 @@ export function enforceSuperUser<T1, T2, T3>(
|
|||
handler: RequestHandler<T1, T2, T3>
|
||||
): RequestHandler<T1, T2, T3> {
|
||||
return function enforceSuperHandler(context, req, res) {
|
||||
if (!appContextService.hasSecurity() || !appContextService.getSecurityLicense().isEnabled()) {
|
||||
return res.forbidden({
|
||||
body: {
|
||||
message: `Access to this API requires that security is enabled`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const security = appContextService.getSecurity();
|
||||
const user = security.authc.getCurrentUser(req);
|
||||
if (!user) {
|
||||
|
|
|
@ -22,7 +22,7 @@ import type {
|
|||
EncryptedSavedObjectsPluginSetup,
|
||||
} from '../../../encrypted_saved_objects/server';
|
||||
|
||||
import type { SecurityPluginStart } from '../../../security/server';
|
||||
import type { SecurityPluginStart, SecurityPluginSetup } from '../../../security/server';
|
||||
import type { FleetConfigType } from '../../common';
|
||||
import type {
|
||||
ExternalCallback,
|
||||
|
@ -39,7 +39,8 @@ class AppContextService {
|
|||
private encryptedSavedObjectsSetup: EncryptedSavedObjectsPluginSetup | undefined;
|
||||
private data: DataPluginStart | undefined;
|
||||
private esClient: ElasticsearchClient | undefined;
|
||||
private security: SecurityPluginStart | undefined;
|
||||
private securitySetup: SecurityPluginSetup | undefined;
|
||||
private securityStart: SecurityPluginStart | undefined;
|
||||
private config$?: Observable<FleetConfigType>;
|
||||
private configSubject$?: BehaviorSubject<FleetConfigType>;
|
||||
private savedObjects: SavedObjectsServiceStart | undefined;
|
||||
|
@ -56,7 +57,8 @@ class AppContextService {
|
|||
this.esClient = appContext.elasticsearch.client.asInternalUser;
|
||||
this.encryptedSavedObjects = appContext.encryptedSavedObjectsStart?.getClient();
|
||||
this.encryptedSavedObjectsSetup = appContext.encryptedSavedObjectsSetup;
|
||||
this.security = appContext.security;
|
||||
this.securitySetup = appContext.securitySetup;
|
||||
this.securityStart = appContext.securityStart;
|
||||
this.savedObjects = appContext.savedObjects;
|
||||
this.isProductionMode = appContext.isProductionMode;
|
||||
this.cloud = appContext.cloud;
|
||||
|
@ -92,14 +94,21 @@ class AppContextService {
|
|||
}
|
||||
|
||||
public getSecurity() {
|
||||
if (!this.security) {
|
||||
if (!this.hasSecurity()) {
|
||||
throw new Error('Security service not set.');
|
||||
}
|
||||
return this.security;
|
||||
return this.securityStart!;
|
||||
}
|
||||
|
||||
public getSecurityLicense() {
|
||||
if (!this.hasSecurity()) {
|
||||
throw new Error('Security service not set.');
|
||||
}
|
||||
return this.securitySetup!.license;
|
||||
}
|
||||
|
||||
public hasSecurity() {
|
||||
return !!this.security;
|
||||
return !!this.securitySetup && !!this.securityStart;
|
||||
}
|
||||
|
||||
public getCloud() {
|
||||
|
|
|
@ -67,3 +67,11 @@ export const setPackageInfo = ({
|
|||
};
|
||||
|
||||
export const deletePackageInfo = (args: SharedKey) => packageInfoCache.delete(sharedKey(args));
|
||||
|
||||
export const clearPackageFileCache = (args: SharedKey) => {
|
||||
const fileList = getArchiveFilelist(args) ?? [];
|
||||
fileList.forEach((filePath) => {
|
||||
deleteArchiveEntry(filePath);
|
||||
});
|
||||
deleteArchiveFilelist(args);
|
||||
};
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
setArchiveFilelist,
|
||||
deleteArchiveFilelist,
|
||||
deletePackageInfo,
|
||||
clearPackageFileCache,
|
||||
} from './cache';
|
||||
import type { SharedKey } from './cache';
|
||||
import { getBufferExtractor } from './extract';
|
||||
|
@ -42,6 +43,9 @@ export async function unpackBufferToCache({
|
|||
archiveBuffer: Buffer;
|
||||
installSource: InstallSource;
|
||||
}): Promise<string[]> {
|
||||
// Make sure any buffers from previous installations from registry or upload are deleted first
|
||||
clearPackageFileCache({ name, version });
|
||||
|
||||
const entries = await unpackBufferEntries(archiveBuffer, contentType);
|
||||
const paths: string[] = [];
|
||||
entries.forEach((entry) => {
|
||||
|
|
|
@ -75,6 +75,10 @@ export const getHttp = (basepath = BASE_PATH) => {
|
|||
return await import('./fixtures/integration.okta');
|
||||
}
|
||||
|
||||
if (path.startsWith('/api/fleet/check-permissions')) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
action(path)('KP: UNSUPPORTED ROUTE');
|
||||
return {};
|
||||
}) as HttpHandler,
|
||||
|
|
|
@ -10873,8 +10873,6 @@
|
|||
"xpack.fleet.integrations.updatePackage.updatePackageButtonLabel": "最新バージョンに更新",
|
||||
"xpack.fleet.integrationsAppTitle": "統合",
|
||||
"xpack.fleet.integrationsHeaderTitle": "Elasticエージェント統合",
|
||||
"xpack.fleet.integrationsPermissionDeniedErrorMessage": "統合にアクセスする権限がありません。統合には{roleName}権限が必要です。",
|
||||
"xpack.fleet.integrationsSecurityRequiredErrorMessage": "統合を使用するには、KibanaとElasticsearchでセキュリティを有効にする必要があります。",
|
||||
"xpack.fleet.invalidLicenseDescription": "現在のライセンスは期限切れです。登録されたビートエージェントは引き続き動作しますが、Elastic Fleet インターフェイスにアクセスするには有効なライセンスが必要です。",
|
||||
"xpack.fleet.invalidLicenseTitle": "ライセンスの期限切れ",
|
||||
"xpack.fleet.multiTextInput.addRow": "行の追加",
|
||||
|
@ -10946,7 +10944,6 @@
|
|||
"xpack.fleet.preconfiguration.missingIDError": "{agentPolicyName}には「id」フィールドがありません。ポリシーのis_defaultまたはis_default_fleet_serverに設定されている場合をのぞき、「id」は必須です。",
|
||||
"xpack.fleet.preconfiguration.packageMissingError": "{agentPolicyName}を追加できませんでした。{pkgName}がインストールされていません。{pkgName}を`{packagesConfigValue}`に追加するか、{packagePolicyName}から削除してください。",
|
||||
"xpack.fleet.preconfiguration.policyDeleted": "構成済みのポリシー{id}が削除されました。作成をスキップしています",
|
||||
"xpack.fleet.securityRequiredErrorTitle": "セキュリティが有効ではありません",
|
||||
"xpack.fleet.serverError.agentPolicyDoesNotExist": "エージェントポリシー{agentPolicyId}が存在しません",
|
||||
"xpack.fleet.serverError.enrollmentKeyDuplicate": "エージェントポリシーの{agentPolicyId}登録キー{providedKeyName}はすでに存在します",
|
||||
"xpack.fleet.serverError.returnedIncorrectKey": "find enrollmentKeyByIdで正しくないキーが返されました",
|
||||
|
|
|
@ -10987,8 +10987,6 @@
|
|||
"xpack.fleet.integrations.updatePackage.updatePackageButtonLabel": "更新到最新版本",
|
||||
"xpack.fleet.integrationsAppTitle": "集成",
|
||||
"xpack.fleet.integrationsHeaderTitle": "Elastic 代理集成",
|
||||
"xpack.fleet.integrationsPermissionDeniedErrorMessage": "您无权访问“集成”。“集成”需要 {roleName} 权限。",
|
||||
"xpack.fleet.integrationsSecurityRequiredErrorMessage": "必须在 Kibana 和 Elasticsearch 中启用安全性,才能使用“集成”。",
|
||||
"xpack.fleet.invalidLicenseDescription": "您当前的许可证已过期。已注册 Beats 代理将继续工作,但您需要有效的许可证,才能访问 Elastic Fleet 界面。",
|
||||
"xpack.fleet.invalidLicenseTitle": "已过期许可证",
|
||||
"xpack.fleet.multiTextInput.addRow": "添加行",
|
||||
|
@ -11060,7 +11058,6 @@
|
|||
"xpack.fleet.preconfiguration.missingIDError": "{agentPolicyName} 缺失 `id` 字段。`id` 是必需的,但标记为 is_default 或 is_default_fleet_server 的策略除外。",
|
||||
"xpack.fleet.preconfiguration.packageMissingError": "{agentPolicyName} 无法添加。{pkgName} 未安装,请将 {pkgName} 添加到 `{packagesConfigValue}` 或将其从 {packagePolicyName} 中移除。",
|
||||
"xpack.fleet.preconfiguration.policyDeleted": "预配置的策略 {id} 已删除;将跳过创建",
|
||||
"xpack.fleet.securityRequiredErrorTitle": "安全性未启用",
|
||||
"xpack.fleet.serverError.agentPolicyDoesNotExist": "代理策略 {agentPolicyId} 不存在",
|
||||
"xpack.fleet.serverError.enrollmentKeyDuplicate": "称作 {providedKeyName} 的注册密钥对于代理策略 {agentPolicyId} 已存在",
|
||||
"xpack.fleet.serverError.returnedIncorrectKey": "find enrollmentKeyById 返回错误的密钥",
|
||||
|
|
|
@ -8,66 +8,15 @@
|
|||
import expect from '@kbn/expect';
|
||||
|
||||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
import { testUsers } from '../test_users';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const supertest = getService('supertest');
|
||||
const security = getService('security');
|
||||
const users: { [rollName: string]: { username: string; password: string; permissions?: any } } = {
|
||||
kibana_basic_user: {
|
||||
permissions: {
|
||||
feature: {
|
||||
dashboards: ['read'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
username: 'kibana_basic_user',
|
||||
password: 'changeme',
|
||||
},
|
||||
fleet_user: {
|
||||
permissions: {
|
||||
feature: {
|
||||
fleet: ['read'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
username: 'fleet_user',
|
||||
password: 'changeme',
|
||||
},
|
||||
fleet_admin: {
|
||||
permissions: {
|
||||
feature: {
|
||||
fleet: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
username: 'fleet_admin',
|
||||
password: 'changeme',
|
||||
},
|
||||
};
|
||||
|
||||
describe('fleet_list_agent', () => {
|
||||
before(async () => {
|
||||
for (const roleName in users) {
|
||||
if (users.hasOwnProperty(roleName)) {
|
||||
const user = users[roleName];
|
||||
|
||||
if (user.permissions) {
|
||||
await security.role.create(roleName, {
|
||||
kibana: [user.permissions],
|
||||
});
|
||||
}
|
||||
|
||||
// Import a repository first
|
||||
await security.user.create(user.username, {
|
||||
password: user.password,
|
||||
roles: [roleName],
|
||||
full_name: user.username,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/fleet/agents');
|
||||
});
|
||||
after(async () => {
|
||||
|
@ -77,13 +26,13 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
it('should return a 403 if a user without the superuser role try to access the APU', async () => {
|
||||
await supertestWithoutAuth
|
||||
.get(`/api/fleet/agents`)
|
||||
.auth(users.fleet_admin.username, users.fleet_admin.password)
|
||||
.auth(testUsers.fleet_all.username, testUsers.fleet_all.password)
|
||||
.expect(403);
|
||||
});
|
||||
it('should not return the list of agents when requesting as a user without fleet permissions', async () => {
|
||||
await supertestWithoutAuth
|
||||
.get(`/api/fleet/agents`)
|
||||
.auth(users.kibana_basic_user.username, users.kibana_basic_user.password)
|
||||
.auth(testUsers.fleet_no_access.username, testUsers.fleet_no_access.password)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
|
|
|
@ -14,10 +14,12 @@ import {
|
|||
IBulkInstallPackageHTTPError,
|
||||
} from '../../../../plugins/fleet/common';
|
||||
import { setupFleetAndAgents } from '../agents/services';
|
||||
import { testUsers } from '../test_users';
|
||||
|
||||
export default function (providerContext: FtrProviderContext) {
|
||||
const { getService } = providerContext;
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
const deletePackage = async (pkgkey: string) => {
|
||||
await supertest.delete(`/api/fleet/epm/packages/${pkgkey}`).set('kbn-xsrf', 'xxxx');
|
||||
|
@ -44,6 +46,13 @@ export default function (providerContext: FtrProviderContext) {
|
|||
it('should return 400 if no packages are requested for upgrade', async function () {
|
||||
await supertest.post(`/api/fleet/epm/packages/_bulk`).set('kbn-xsrf', 'xxxx').expect(400);
|
||||
});
|
||||
it('should return 403 if read only user requests upgrade', async function () {
|
||||
await supertestWithoutAuth
|
||||
.post(`/api/fleet/epm/packages/_bulk`)
|
||||
.auth(testUsers.fleet_read_only.username, testUsers.fleet_read_only.password)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.expect(403);
|
||||
});
|
||||
it('should return 200 and an array for upgrading a package', async function () {
|
||||
const { body }: { body: BulkInstallPackagesResponse } = await supertest
|
||||
.post(`/api/fleet/epm/packages/_bulk`)
|
||||
|
|
|
@ -8,10 +8,12 @@
|
|||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
import { skipIfNoDockerRegistry } from '../../helpers';
|
||||
import { setupFleetAndAgents } from '../agents/services';
|
||||
import { testUsers } from '../test_users';
|
||||
|
||||
export default function (providerContext: FtrProviderContext) {
|
||||
const { getService } = providerContext;
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const requiredPackage = 'elastic_agent-0.0.7';
|
||||
|
||||
const installPackage = async (pkgkey: string) => {
|
||||
|
@ -52,5 +54,13 @@ export default function (providerContext: FtrProviderContext) {
|
|||
.send({ force: true })
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should return 403 for read-only users', async () => {
|
||||
await supertestWithoutAuth
|
||||
.delete(`/api/fleet/epm/packages/${requiredPackage}`)
|
||||
.auth(testUsers.fleet_read_only.username, testUsers.fleet_read_only.password)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -11,11 +11,13 @@ import path from 'path';
|
|||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
import { skipIfNoDockerRegistry } from '../../helpers';
|
||||
import { setupFleetAndAgents } from '../agents/services';
|
||||
import { testUsers } from '../test_users';
|
||||
|
||||
export default function (providerContext: FtrProviderContext) {
|
||||
const { getService } = providerContext;
|
||||
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
const testPkgKey = 'apache-0.1.4';
|
||||
|
||||
|
@ -91,5 +93,12 @@ export default function (providerContext: FtrProviderContext) {
|
|||
it('returns a 400 for a package key without a proper semver version', async function () {
|
||||
await supertest.get('/api/fleet/epm/packages/endpoint-0.1.0.1.2.3').expect(400);
|
||||
});
|
||||
|
||||
it('allows user with only read permission to access', async () => {
|
||||
await supertestWithoutAuth
|
||||
.get(`/api/fleet/epm/packages/${testPkgKey}`)
|
||||
.auth(testUsers.fleet_read_only.username, testUsers.fleet_read_only.password)
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -12,10 +12,12 @@ import expect from '@kbn/expect';
|
|||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
import { skipIfNoDockerRegistry } from '../../helpers';
|
||||
import { setupFleetAndAgents } from '../agents/services';
|
||||
import { testUsers } from '../test_users';
|
||||
|
||||
export default function (providerContext: FtrProviderContext) {
|
||||
const { getService } = providerContext;
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const dockerServers = getService('dockerServers');
|
||||
|
||||
const testPkgArchiveTgz = path.join(
|
||||
|
@ -190,5 +192,16 @@ export default function (providerContext: FtrProviderContext) {
|
|||
'{"statusCode":400,"error":"Bad Request","message":"Name thisIsATypo and version 0.1.4 do not match top-level directory apache-0.1.4"}'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not allow users without all access', async () => {
|
||||
const buf = fs.readFileSync(testPkgArchiveTgz);
|
||||
await supertestWithoutAuth
|
||||
.post(`/api/fleet/epm/packages`)
|
||||
.auth(testUsers.fleet_read_only.username, testUsers.fleet_read_only.password)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.type('application/gzip')
|
||||
.send(buf)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,10 +9,12 @@ import expect from '@kbn/expect';
|
|||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
import { skipIfNoDockerRegistry } from '../../helpers';
|
||||
import { setupFleetAndAgents } from '../agents/services';
|
||||
import { testUsers } from '../test_users';
|
||||
|
||||
export default function (providerContext: FtrProviderContext) {
|
||||
const { getService } = providerContext;
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
// use function () {} and not () => {} here
|
||||
|
@ -54,6 +56,13 @@ export default function (providerContext: FtrProviderContext) {
|
|||
|
||||
expect(listResponse.response).to.eql(['endpoint']);
|
||||
});
|
||||
|
||||
it('allows user with only read permission to access', async () => {
|
||||
await supertestWithoutAuth
|
||||
.get('/api/fleet/epm/packages')
|
||||
.auth(testUsers.fleet_read_only.username, testUsers.fleet_read_only.password)
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,10 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export default function ({ loadTestFile }) {
|
||||
import { setupTestUsers } from './test_users';
|
||||
|
||||
export default function ({ loadTestFile, getService }) {
|
||||
describe('Fleet Endpoints', function () {
|
||||
before(async () => {
|
||||
await setupTestUsers(getService('security'));
|
||||
});
|
||||
|
||||
// EPM
|
||||
loadTestFile(require.resolve('./epm/index'));
|
||||
loadTestFile(require.resolve('./epm'));
|
||||
|
||||
// Fleet setup
|
||||
loadTestFile(require.resolve('./fleet_setup'));
|
||||
|
|
63
x-pack/test/fleet_api_integration/apis/test_users.ts
Normal file
63
x-pack/test/fleet_api_integration/apis/test_users.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 type { SecurityService } from '../../../../test/common/services/security/security';
|
||||
|
||||
export const testUsers: {
|
||||
[rollName: string]: { username: string; password: string; permissions?: any };
|
||||
} = {
|
||||
fleet_no_access: {
|
||||
permissions: {
|
||||
feature: {
|
||||
dashboards: ['read'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
username: 'fleet_no_access',
|
||||
password: 'changeme',
|
||||
},
|
||||
fleet_read_only: {
|
||||
permissions: {
|
||||
feature: {
|
||||
fleet: ['read'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
username: 'fleet_read_only',
|
||||
password: 'changeme',
|
||||
},
|
||||
fleet_all: {
|
||||
permissions: {
|
||||
feature: {
|
||||
fleet: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
username: 'fleet_all',
|
||||
password: 'changeme',
|
||||
},
|
||||
};
|
||||
|
||||
export const setupTestUsers = async (security: SecurityService) => {
|
||||
for (const roleName in testUsers) {
|
||||
if (testUsers.hasOwnProperty(roleName)) {
|
||||
const user = testUsers[roleName];
|
||||
|
||||
if (user.permissions) {
|
||||
await security.role.create(roleName, {
|
||||
kibana: [user.permissions],
|
||||
});
|
||||
}
|
||||
|
||||
await security.user.create(user.username, {
|
||||
password: user.password,
|
||||
roles: [roleName],
|
||||
full_name: user.username,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
Loading…
Reference in a new issue