[Fleet] Allow to move agent policy to another space. (#189663)

This commit is contained in:
Nicolas Chaulet 2024-08-15 13:19:25 -04:00 committed by GitHub
parent 727c9b46ae
commit c2fc468638
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 892 additions and 110 deletions

View file

@ -132,6 +132,7 @@ export const APP_API_ROUTES = {
HEALTH_CHECK_PATTERN: `${API_ROOT}/health_check`,
CHECK_PERMISSIONS_PATTERN: `${API_ROOT}/check-permissions`,
GENERATE_SERVICE_TOKEN_PATTERN: `${API_ROOT}/service_tokens`,
AGENT_POLICIES_SPACES: `${INTERNAL_ROOT}/agent_policies_spaces`,
// deprecated since 8.0
GENERATE_SERVICE_TOKEN_PATTERN_DEPRECATED: `${API_ROOT}/service-tokens`,
};

View file

@ -294,6 +294,7 @@ export const appRoutesService = {
getCheckPermissionsPath: () => APP_API_ROUTES.CHECK_PERMISSIONS_PATTERN,
getRegenerateServiceTokenPath: () => APP_API_ROUTES.GENERATE_SERVICE_TOKEN_PATTERN,
postHealthCheckPath: () => APP_API_ROUTES.HEALTH_CHECK_PATTERN,
getAgentPoliciesSpacesPath: () => APP_API_ROUTES.AGENT_POLICIES_SPACES,
};
export const enrollmentAPIKeyRouteService = {

View file

@ -21,6 +21,7 @@ export interface NewAgentPolicy {
id?: string;
name: string;
namespace: string;
space_ids?: string[];
description?: string;
is_default?: boolean;
is_default_fleet_server?: boolean; // Optional when creating a policy
@ -53,7 +54,7 @@ export interface GlobalDataTag {
// SO definition for this type is declared in server/types/interfaces
export interface AgentPolicy extends Omit<NewAgentPolicy, 'id'> {
id: string;
space_id?: string | undefined;
space_ids?: string[] | undefined;
status: ValueOf<AgentPolicyStatus>;
package_policies?: PackagePolicy[];
is_managed: boolean; // required for created policy

View file

@ -101,7 +101,7 @@ export interface UpdatePackagePolicy extends NewPackagePolicy {
// SO definition for this type is declared in server/types/interfaces
export interface PackagePolicy extends Omit<NewPackagePolicy, 'inputs'> {
id: string;
spaceId?: string;
spaceIds?: string[];
inputs: PackagePolicyInput[];
version?: string;
agents?: number;

View file

@ -34,7 +34,7 @@ export type EnrollmentSettingsFleetServerPolicy = Pick<
| 'has_fleet_server'
| 'fleet_server_host_id'
| 'download_source_id'
| 'space_id'
| 'space_ids'
>;
export interface GetEnrollmentSettingsResponse {

View file

@ -41,6 +41,8 @@ import {
useGetAgentPolicies,
useLicense,
useUIExtension,
useLink,
useFleetStatus,
} from '../../../../hooks';
import { AgentPolicyPackageBadge } from '../../../../components';
@ -60,6 +62,7 @@ import {
} from './hooks';
import { CustomFields } from './custom_fields';
import { SpaceSelector } from './space_selector';
interface Props {
agentPolicy: Partial<NewAgentPolicy | AgentPolicy>;
@ -75,7 +78,11 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent<Props> =
validation,
disabled = false,
}) => {
const useSpaceAwareness = ExperimentalFeaturesService.get()?.useSpaceAwareness ?? false;
const { docLinks } = useStartServices();
const { spaceId } = useFleetStatus();
const { getAbsolutePath } = useLink();
const AgentTamperProtectionWrapper = useUIExtension(
'endpoint',
'endpoint-agent-tamper-protection'
@ -257,6 +264,63 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent<Props> =
/>
</EuiFormRow>
</EuiDescribedFormGroup>
{useSpaceAwareness ? (
<EuiDescribedFormGroup
fullWidth
title={
<h3>
<FormattedMessage
id="xpack.fleet.agentPolicyForm.spaceFieldLabel"
defaultMessage="Space"
/>
</h3>
}
description={
<FormattedMessage
id="xpack.fleet.agentPolicyForm.spaceDescription"
defaultMessage="Select a space for this policy or create a new one. {link}"
values={{
link: (
<EuiLink
target="_blank"
href={getAbsolutePath('/app/management/kibana/spaces/create')}
external
>
<FormattedMessage
id="xpack.fleet.agentPolicyForm.createSpaceLink"
defaultMessage="Create space"
/>
</EuiLink>
),
}}
/>
}
>
<EuiFormRow
fullWidth
key="space"
error={
touchedFields.description && validation.description ? validation.description : null
}
isDisabled={disabled}
isInvalid={Boolean(touchedFields.description && validation.description)}
>
<SpaceSelector
isDisabled={disabled}
value={
'space_ids' in agentPolicy && agentPolicy.space_ids
? agentPolicy.space_ids
: [spaceId || 'default']
}
onChange={(newValue) => {
updateAgentPolicy({
space_ids: newValue,
});
}}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
) : null}
<EuiDescribedFormGroup
fullWidth
title={

View file

@ -0,0 +1,76 @@
/*
* 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 EuiComboBoxOptionOption, EuiHealth } from '@elastic/eui';
import { EuiComboBox } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import { useAgentPoliciesSpaces } from '../../../../../../hooks';
export interface SpaceSelectorProps {
value: string[];
onChange: (newVal: string[]) => void;
isDisabled?: boolean;
}
export const SpaceSelector: React.FC<SpaceSelectorProps> = ({ value, onChange, isDisabled }) => {
const res = useAgentPoliciesSpaces();
const renderOption = React.useCallback(
(option: any, searchValue: string, contentClassName: string) => (
<EuiHealth color={option.color}>
<span className={contentClassName}>{option.label}</span>
</EuiHealth>
),
[]
);
const options: Array<EuiComboBoxOptionOption<string>> = useMemo(() => {
return (
res.data?.items.map((item: any) => ({
label: item.name,
key: item.id,
...item,
})) ?? []
);
}, [res.data]);
const selectedOptions: Array<EuiComboBoxOptionOption<string>> = useMemo(() => {
if (res.isInitialLoading) {
return [];
}
return value.map((v) => {
const existingOption = options.find((opt) => opt.key === v);
return existingOption
? existingOption
: {
label: v,
key: v,
};
});
}, [options, value, res.isInitialLoading]);
return (
<EuiComboBox
data-test-subj={'spaceSelectorComboBox'}
aria-label={i18n.translate('xpack.fleet.agentPolicies.spaceSelectorLabel', {
defaultMessage: 'Spaces',
})}
fullWidth
options={options}
renderOption={renderOption}
selectedOptions={selectedOptions}
isDisabled={res.isInitialLoading || isDisabled}
isClearable={false}
onChange={(newOptions) => {
onChange(newOptions.map(({ key }) => key as string));
}}
/>
);
};

View file

@ -45,6 +45,7 @@ const pickAgentPolicyKeysToSend = (agentPolicy: AgentPolicy) =>
'name',
'description',
'namespace',
'space_ids',
'monitoring_enabled',
'unenroll_timeout',
'inactivity_timeout',

View file

@ -22,3 +22,4 @@ export * from './download_source';
export * from './fleet_server_hosts';
export * from './fleet_proxies';
export * from './health_check';
export * from './spaces';

View file

@ -0,0 +1,22 @@
/*
* 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 { useQuery } from '@tanstack/react-query';
import { API_VERSIONS, appRoutesService } from '../../../common';
import { sendRequestForRq } from './use_request';
export function useAgentPoliciesSpaces() {
return useQuery(['fleet-get-spaces'], async () => {
return sendRequestForRq({
method: 'get',
path: appRoutesService.getAgentPoliciesSpacesPath(),
version: API_VERSIONS.internal.v1,
});
});
}

View file

@ -19,6 +19,8 @@ import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
import { securityMock } from '@kbn/security-plugin/server/mocks';
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
import { SPACES_EXTENSION_ID } from '@kbn/core-saved-objects-server';
import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import type { PackagePolicyClient } from '../services/package_policy_service';
import type { AgentPolicyServiceInterface } from '../services';
@ -59,7 +61,11 @@ export interface MockedFleetAppContext extends FleetAppContext {
export const createAppContextStartContractMock = (
configOverrides: Partial<FleetConfigType> = {},
isServerless: boolean = false
isServerless: boolean = false,
soClients: Partial<{
internal?: SavedObjectsClientContract;
withoutSpaceExtensions?: SavedObjectsClientContract;
}> = {}
): MockedFleetAppContext => {
const config = {
agents: { enabled: true, elasticsearch: {} },
@ -70,12 +76,26 @@ export const createAppContextStartContractMock = (
const config$ = of(config);
const mockedSavedObject = savedObjectsServiceMock.createStartContract();
const internalSoClient = soClients.internal ?? savedObjectsClientMock.create();
const internalSoClientWithoutSpaceExtension =
soClients.withoutSpaceExtensions ?? savedObjectsClientMock.create();
mockedSavedObject.getScopedClient.mockImplementation((request, options) => {
if (options?.excludedExtensions?.includes(SPACES_EXTENSION_ID)) {
return internalSoClientWithoutSpaceExtension;
}
return internalSoClient;
});
return {
elasticsearch: elasticsearchServiceMock.createStart(),
data: dataPluginMock.createStartContract(),
encryptedSavedObjectsStart: encryptedSavedObjectsMock.createStart(),
encryptedSavedObjectsSetup: encryptedSavedObjectsMock.createSetup({ canEncrypt: true }),
savedObjects: savedObjectsServiceMock.createStartContract(),
savedObjects: mockedSavedObject,
securityCoreStart: securityServiceMock.createStart(),
securitySetup: securityMock.createSetup(),
securityStart: securityMock.createStart(),
@ -116,6 +136,7 @@ export const createFleetRequestHandlerContextMock = (): jest.Mocked<
> => {
return {
authz: createFleetAuthzMock(),
getAllSpaces: jest.fn(),
agentClient: {
asCurrentUser: agentServiceMock.createClient(),
asInternalUser: agentServiceMock.createClient(),

View file

@ -150,6 +150,7 @@ export interface FleetStartDeps {
telemetry?: TelemetryPluginStart;
savedObjectsTagging: SavedObjectTaggingStart;
taskManager: TaskManagerStartContract;
spaces: SpacesPluginStart;
}
export interface FleetAppContext {
@ -257,6 +258,7 @@ export class FleetPlugin
private kibanaInstanceId: FleetAppContext['kibanaInstanceId'];
private httpSetup?: HttpServiceSetup;
private securitySetup!: SecurityPluginSetup;
private spacesPluginsStart?: SpacesPluginStart;
private encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup;
private readonly telemetryEventsSender: TelemetryEventsSender;
private readonly fleetStatus$: BehaviorSubject<ServiceStatus>;
@ -517,6 +519,7 @@ export class FleetPlugin
.getSavedObjects()
.getScopedClient(request, { excludedExtensions: [SECURITY_EXTENSION_ID] });
const spacesPluginsStart = this.spacesPluginsStart;
return {
get agentClient() {
const agentService = plugin.setupAgentService(esClient.asInternalUser, soClient);
@ -554,7 +557,9 @@ export class FleetPlugin
get spaceId() {
return deps.spaces?.spacesService?.getSpaceId(request) ?? DEFAULT_SPACE_ID;
},
getAllSpaces() {
return spacesPluginsStart!.spacesService.createSpacesClient(request).getAll();
},
get limitedToPackages() {
if (routeAuthz && routeAuthz.granted) {
return routeAuthz.scopeDataToPackages;
@ -600,6 +605,7 @@ export class FleetPlugin
}
public start(core: CoreStart, plugins: FleetStartDeps): FleetStartContract {
this.spacesPluginsStart = plugins.spaces;
const messageSigningService = new MessageSigningService(
this.initializerContext.logger,
plugins.encryptedSavedObjects.getClient({

View file

@ -6,7 +6,7 @@
*/
import type { TypeOf } from '@kbn/config-schema';
import type { RequestHandler, ResponseHeaders } from '@kbn/core/server';
import type { KibanaRequest, RequestHandler, ResponseHeaders } from '@kbn/core/server';
import pMap from 'p-map';
import { safeDump } from 'js-yaml';
@ -28,6 +28,7 @@ import type {
FleetRequestHandler,
BulkGetAgentPoliciesRequestSchema,
AgentPolicy,
FleetRequestHandlerContext,
} from '../../types';
import type {
@ -47,8 +48,10 @@ import {
defaultFleetErrorHandler,
AgentPolicyNotFoundError,
FleetUnauthorizedError,
FleetError,
} from '../../errors';
import { createAgentPolicyWithPackages } from '../../services/agent_policy_create';
import { updateAgentPolicySpaces } from '../../services/spaces/agent_policy';
export async function populateAssignedAgentsCount(
agentClient: AgentClient,
@ -97,6 +100,24 @@ function sanitizeItemForReadAgentOnly(item: AgentPolicy): AgentPolicy {
};
}
export async function checkAgentPoliciesAllPrivilegesForSpaces(
request: KibanaRequest,
context: FleetRequestHandlerContext,
spaceIds: string[]
) {
const security = appContextService.getSecurity();
const spaces = await (await context.fleet).getAllSpaces();
const allSpaceId = spaces.map((s) => s.id);
const res = await security.authz.checkPrivilegesWithRequest(request).atSpaces(allSpaceId, {
kibana: [security.authz.actions.api.get(`fleet-agent-policies-all`)],
});
return allSpaceId.filter(
(id) =>
res.privileges.kibana.find((privilege) => privilege.resource === id)?.authorized ?? false
);
}
export const getAgentPoliciesHandler: FleetRequestHandler<
undefined,
TypeOf<typeof GetAgentPoliciesRequestSchema.query>
@ -218,26 +239,54 @@ export const createAgentPolicyHandler: FleetRequestHandler<
const user = appContextService.getSecurityCore().authc.getCurrentUser(request) || undefined;
const withSysMonitoring = request.query.sys_monitoring ?? false;
const monitoringEnabled = request.body.monitoring_enabled;
const { has_fleet_server: hasFleetServer, force, ...newPolicy } = request.body;
const {
has_fleet_server: hasFleetServer,
force,
space_ids: spaceIds,
...newPolicy
} = request.body;
const spaceId = fleetContext.spaceId;
const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request, user?.username);
try {
let authorizedSpaces: string[] | undefined;
if (spaceIds?.length) {
authorizedSpaces = await checkAgentPoliciesAllPrivilegesForSpaces(request, context, spaceIds);
for (const requestedSpaceId of spaceIds) {
if (!authorizedSpaces.includes(requestedSpaceId)) {
throw new FleetError(
`No enough permissions to create policies in space ${requestedSpaceId}`
);
}
}
}
const agentPolicy = await createAgentPolicyWithPackages({
soClient,
esClient,
newPolicy,
hasFleetServer,
withSysMonitoring,
monitoringEnabled,
spaceId,
user,
authorizationHeader,
force,
});
const body: CreateAgentPolicyResponse = {
item: await createAgentPolicyWithPackages({
soClient,
esClient,
newPolicy,
hasFleetServer,
withSysMonitoring,
monitoringEnabled,
spaceId,
user,
authorizationHeader,
force,
}),
item: agentPolicy,
};
if (spaceIds && spaceIds.length > 1 && authorizedSpaces) {
await updateAgentPolicySpaces({
agentPolicyId: agentPolicy.id,
currentSpaceId: spaceId,
newSpaceIds: spaceIds,
authorizedSpaces,
});
}
return response.ok({
body,
});
@ -259,20 +308,36 @@ export const updateAgentPolicyHandler: FleetRequestHandler<
> = async (context, request, response) => {
const coreContext = await context.core;
const fleetContext = await context.fleet;
const soClient = coreContext.savedObjects.client;
const esClient = coreContext.elasticsearch.client.asInternalUser;
const user = appContextService.getSecurityCore().authc.getCurrentUser(request) || undefined;
const { force, ...data } = request.body;
const { force, space_ids: spaceIds, ...data } = request.body;
let spaceId = fleetContext.spaceId;
const spaceId = fleetContext.spaceId;
try {
if (spaceIds?.length) {
const authorizedSpaces = await checkAgentPoliciesAllPrivilegesForSpaces(
request,
context,
spaceIds
);
await updateAgentPolicySpaces({
agentPolicyId: request.params.agentPolicyId,
currentSpaceId: spaceId,
newSpaceIds: spaceIds,
authorizedSpaces,
});
spaceId = spaceIds[0];
}
const agentPolicy = await agentPolicyService.update(
soClient,
appContextService.getInternalUserSOClientForSpaceId(spaceId),
esClient,
request.params.agentPolicyId,
data,
{ force, user, spaceId }
);
const body: UpdateAgentPolicyResponse = { item: agentPolicy };
return response.ok({
body,

View file

@ -151,6 +151,35 @@ export const generateServiceTokenHandler: RequestHandler<
}
};
export const getAgentPoliciesSpacesHandler: FleetRequestHandler<
null,
null,
TypeOf<typeof GenerateServiceTokenRequestSchema.body>
> = async (context, request, response) => {
try {
const spaces = await (await context.fleet).getAllSpaces();
const security = appContextService.getSecurity();
const spaceIds = spaces.map(({ id }) => id);
const res = await security.authz.checkPrivilegesWithRequest(request).atSpaces(spaceIds, {
kibana: [security.authz.actions.api.get(`fleet-agent-policies-all`)],
});
const authorizedSpaces = spaces.filter(
(space) =>
res.privileges.kibana.find((privilege) => privilege.resource === space.id)?.authorized ??
false
);
return response.ok({
body: {
items: authorizedSpaces,
},
});
} catch (error) {
return defaultFleetErrorHandler({ error, response });
}
};
const serviceTokenBodyValidation = (data: any, validationResult: RouteValidationResultFactory) => {
const { ok } = validationResult;
if (!data) {
@ -192,6 +221,22 @@ export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType
getCheckPermissionsHandler
);
router.versioned
.get({
path: APP_API_ROUTES.AGENT_POLICIES_SPACES,
access: 'internal',
fleetAuthz: {
fleet: { allAgentPolicies: true },
},
})
.addVersion(
{
version: API_VERSIONS.internal.v1,
validate: {},
},
getAgentPoliciesSpacesHandler
);
router.versioned
.post({
path: APP_API_ROUTES.GENERATE_SERVICE_TOKEN_PATTERN,

View file

@ -115,7 +115,7 @@ export const getFleetServerOrAgentPolicies = async (
has_fleet_server: policy.has_fleet_server,
fleet_server_host_id: policy.fleet_server_host_id,
download_source_id: policy.download_source_id,
space_id: policy.space_id,
space_ids: policy.space_ids,
});
// If an agent policy is specified, return only that policy

View file

@ -49,6 +49,7 @@ describe('FleetSetupHandler', () => {
uninstallTokenService: {
asCurrentUser: createUninstallTokenServiceMock(),
},
getAllSpaces: jest.fn(),
agentClient: {
asCurrentUser: agentServiceMock.createClient(),
asInternalUser: agentServiceMock.createClient(),
@ -136,6 +137,7 @@ describe('FleetStatusHandler', () => {
uninstallTokenService: {
asCurrentUser: createUninstallTokenServiceMock(),
},
getAllSpaces: jest.fn(),
agentClient: {
asCurrentUser: agentServiceMock.createClient(),
asInternalUser: agentServiceMock.createClient(),

View file

@ -199,8 +199,8 @@ export async function getFullAgentPolicy(
},
};
if (agentPolicy.space_id) {
fullAgentPolicy.namespaces = [agentPolicy.space_id];
if (agentPolicy.space_ids) {
fullAgentPolicy.namespaces = agentPolicy.space_ids;
}
const packagePoliciesByOutputId = Object.keys(fullAgentPolicy.outputs).reduce(

View file

@ -40,7 +40,7 @@ export const mapAgentPolicySavedObjectToAgentPolicy = ({
return {
id,
version,
space_id: namespaces?.[0] ? namespaces?.[0] : undefined,
space_ids: namespaces,
description,
is_default,
is_default_fleet_server,

View file

@ -1502,12 +1502,17 @@ describe('Agent policy', () => {
it('should return empty array if no policies with inactivity timeouts', async () => {
const mockSoClient = createMockSoClientThatReturns([]);
expect(await agentPolicyService.getInactivityTimeouts(mockSoClient)).toEqual([]);
mockedAppContextService.getInternalUserSOClientWithoutSpaceExtension.mockReturnValueOnce(
mockSoClient
);
expect(await agentPolicyService.getInactivityTimeouts()).toEqual([]);
});
it('should return single inactivity timeout', async () => {
const mockSoClient = createMockSoClientThatReturns([createPolicySO('policy1', 1000)]);
expect(await agentPolicyService.getInactivityTimeouts(mockSoClient)).toEqual([
mockedAppContextService.getInternalUserSOClientWithoutSpaceExtension.mockReturnValueOnce(
mockSoClient
);
expect(await agentPolicyService.getInactivityTimeouts()).toEqual([
{ inactivityTimeout: 1000, policyIds: ['policy1'] },
]);
});
@ -1516,8 +1521,11 @@ describe('Agent policy', () => {
createPolicySO('policy1', 1000),
createPolicySO('policy2', 1000),
]);
mockedAppContextService.getInternalUserSOClientWithoutSpaceExtension.mockReturnValueOnce(
mockSoClient
);
expect(await agentPolicyService.getInactivityTimeouts(mockSoClient)).toEqual([
expect(await agentPolicyService.getInactivityTimeouts()).toEqual([
{ inactivityTimeout: 1000, policyIds: ['policy1', 'policy2'] },
]);
});
@ -1527,8 +1535,10 @@ describe('Agent policy', () => {
createPolicySO('policy2', 1000),
createPolicySO('policy3', 2000),
]);
expect(await agentPolicyService.getInactivityTimeouts(mockSoClient)).toEqual([
mockedAppContextService.getInternalUserSOClientWithoutSpaceExtension.mockReturnValueOnce(
mockSoClient
);
expect(await agentPolicyService.getInactivityTimeouts()).toEqual([
{ inactivityTimeout: 1000, policyIds: ['policy1', 'policy2'] },
{ inactivityTimeout: 2000, policyIds: ['policy3'] },
]);

View file

@ -478,7 +478,10 @@ class AgentPolicyService {
return {
...options,
id: id.id,
namespaces: id.spaceId ? [id.spaceId] : undefined,
namespaces:
savedObjectType === AGENT_POLICY_SAVED_OBJECT_TYPE && id.spaceId
? [id.spaceId]
: undefined,
type: savedObjectType,
};
});
@ -591,16 +594,12 @@ class AgentPolicyService {
(await packagePolicyService.findAllForAgentPolicy(soClient, agentPolicySO.id)) || [];
}
if (options.withAgentCount) {
await getAgentsByKuery(
appContextService.getInternalUserESClient(),
appContextService.getInternalUserSOClientForSpaceId(agentPolicy.space_id),
{
showInactive: true,
perPage: 0,
page: 1,
kuery: `${AGENTS_PREFIX}.policy_id:${agentPolicy.id}`,
}
).then(({ total }) => (agentPolicy.agents = total));
await getAgentsByKuery(appContextService.getInternalUserESClient(), soClient, {
showInactive: true,
perPage: 0,
page: 1,
kuery: `${AGENTS_PREFIX}.policy_id:${agentPolicy.id}`,
}).then(({ total }) => (agentPolicy.agents = total));
} else {
agentPolicy.agents = 0;
}
@ -1519,16 +1518,19 @@ class AgentPolicyService {
);
}
public async getInactivityTimeouts(
soClient: SavedObjectsClientContract
): Promise<Array<{ policyIds: string[]; inactivityTimeout: number }>> {
public async getInactivityTimeouts(): Promise<
Array<{ policyIds: string[]; inactivityTimeout: number }>
> {
const savedObjectType = await getAgentPolicySavedObjectType();
const findRes = await soClient.find<AgentPolicySOAttributes>({
const internalSoClientWithoutSpaceExtension =
appContextService.getInternalUserSOClientWithoutSpaceExtension();
const findRes = await internalSoClientWithoutSpaceExtension.find<AgentPolicySOAttributes>({
type: savedObjectType,
page: 1,
perPage: SO_SEARCH_LIMIT,
filter: `${savedObjectType}.attributes.inactivity_timeout > 0`,
fields: [`inactivity_timeout`],
namespaces: ['*'],
});
const groupedResults = groupBy(findRes.saved_objects, (so) => so.attributes.inactivity_timeout);

View file

@ -168,7 +168,7 @@ export function _buildStatusRuntimeField(opts: {
// pathPrefix is used by the endpoint team currently to run
// agent queries against the endpoint metadata index
export async function buildAgentStatusRuntimeField(
soClient: SavedObjectsClientContract,
soClient: SavedObjectsClientContract, // Deprecated, it's now using an internal client
pathPrefix?: string
) {
const config = appContextService.getConfig();
@ -182,7 +182,7 @@ export async function buildAgentStatusRuntimeField(
}
const maxAgentPoliciesWithInactivityTimeout =
config?.developer?.maxAgentPoliciesWithInactivityTimeout;
const inactivityTimeouts = await agentPolicyService.getInactivityTimeouts(soClient);
const inactivityTimeouts = await agentPolicyService.getInactivityTimeouts();
return _buildStatusRuntimeField({
inactivityTimeouts,

View file

@ -57,7 +57,9 @@ describe('Agents CRUD test', () => {
closePointInTime: jest.fn(),
} as unknown as ElasticsearchClient;
mockContract = createAppContextStartContractMock();
mockContract = createAppContextStartContractMock({}, false, {
withoutSpaceExtensions: soClientMock,
});
appContextService.start(mockContract);
});

View file

@ -16,7 +16,12 @@ import { createClientMock } from './action.mock';
describe('reassignAgent', () => {
beforeEach(async () => {
appContextService.start(createAppContextStartContractMock());
const { soClient } = createClientMock();
appContextService.start(
createAppContextStartContractMock({}, false, {
withoutSpaceExtensions: soClient,
})
);
});
afterEach(() => {

View file

@ -14,7 +14,12 @@ import { bulkRequestDiagnostics, requestDiagnostics } from './request_diagnostic
describe('requestDiagnostics', () => {
beforeEach(async () => {
appContextService.start(createAppContextStartContractMock());
const { soClient } = createClientMock();
appContextService.start(
createAppContextStartContractMock({}, false, {
withoutSpaceExtensions: soClient,
})
);
});
afterEach(() => {

View file

@ -15,7 +15,23 @@ import { getAgentStatusForAgentPolicy } from './status';
describe('getAgentStatusForAgentPolicy', () => {
beforeEach(async () => {
appContextService.start(createAppContextStartContractMock());
const soClient = {
find: jest.fn().mockResolvedValue({
saved_objects: [
{
id: 'agentPolicyId',
attributes: {
name: 'Policy 1',
},
},
],
}),
};
appContextService.start(
createAppContextStartContractMock({}, false, {
withoutSpaceExtensions: soClient as any,
})
);
});
afterEach(() => {

View file

@ -27,7 +27,12 @@ const mockedInvalidateAPIKeys = invalidateAPIKeys as jest.MockedFunction<typeof
describe('unenroll', () => {
beforeEach(async () => {
appContextService.start(createAppContextStartContractMock());
const { soClient } = createClientMock();
appContextService.start(
createAppContextStartContractMock({}, false, {
withoutSpaceExtensions: soClient,
})
);
});
afterEach(() => {

View file

@ -36,7 +36,12 @@ jest.mock('./action_status', () => {
describe('sendUpgradeAgentsActions (plural)', () => {
beforeEach(async () => {
appContextService.start(createAppContextStartContractMock());
const { soClient } = createClientMock();
appContextService.start(
createAppContextStartContractMock({}, false, {
withoutSpaceExtensions: soClient,
})
);
});
afterEach(() => {

View file

@ -11,8 +11,8 @@ import { loggerMock } from '@kbn/logging-mocks';
import type { Logger } from '@kbn/core/server';
import { securityMock } from '@kbn/security-plugin/server/mocks';
import type { AgentPolicy } from '../../../common';
import { ENROLLMENT_API_KEYS_INDEX } from '../../constants';
import { agentPolicyService } from '../agent_policy';
import { auditLoggingService } from '../audit_logging';
import { appContextService } from '../app_context';
@ -99,8 +99,8 @@ describe('enrollment api keys', () => {
mockedAgentPolicyService.get.mockResolvedValue({
id: 'test-agent-policy',
space_id: 'test123',
} as any);
space_ids: ['test123'],
} as AgentPolicy);
await generateEnrollmentAPIKey(soClient, esClient, {
name: 'test-api-key',
@ -117,6 +117,40 @@ describe('enrollment api keys', () => {
})
);
});
it('should set namespaces if agent policy specify mulitple space IDs', async () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
esClient.create.mockResolvedValue({
_id: 'test-enrollment-api-key-id',
} as any);
esClient.security.createApiKey.mockResolvedValue({
api_key: 'test-api-key-value',
id: 'test-api-key-id',
} as any);
mockedAgentPolicyService.get.mockResolvedValue({
id: 'test-agent-policy',
space_ids: ['test123', 'test456'],
} as AgentPolicy);
await generateEnrollmentAPIKey(soClient, esClient, {
name: 'test-api-key',
expiration: '7d',
agentPolicyId: 'test-agent-policy',
forceRecreate: true,
});
expect(esClient.create).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.objectContaining({
namespaces: ['test123', 'test456'],
}),
})
);
});
});
describe('deleteEnrollmentApiKey', () => {

View file

@ -313,7 +313,7 @@ export async function generateEnrollmentAPIKey(
api_key: apiKey,
name,
policy_id: agentPolicyId,
namespaces: agentPolicy?.space_id ? [agentPolicy?.space_id] : undefined,
namespaces: agentPolicy?.space_ids,
created_at: new Date().toISOString(),
};

View file

@ -35,15 +35,15 @@ export const getFleetServerPolicies = async (
});
// Extract associated fleet server agent policy IDs
const fleetServerAgentPolicyIds = fleetServerPackagePolicies.items.flatMap((p) =>
p.policy_ids?.map((id) => ({ id, spaceId: p.spaceId } ?? []))
);
const fleetServerAgentPolicyIds = fleetServerPackagePolicies.items.flatMap((p) => {
return p.policy_ids?.map((id) => ({ id, spaceId: p.spaceIds?.[0] ?? DEFAULT_SPACE_ID } ?? []));
});
// Retrieve associated agent policies
const fleetServerAgentPolicies = fleetServerAgentPolicyIds.length
? await agentPolicyService.getByIDs(
soClient,
uniqBy(fleetServerAgentPolicyIds, (p) => `${p?.spaceId ?? ''}:${p.id}`)
uniqBy(fleetServerAgentPolicyIds, (p) => p.id)
)
: [];
@ -58,7 +58,7 @@ export const getFleetServerPolicies = async (
export const hasFleetServersForPolicies = async (
esClient: ElasticsearchClient,
soClient: SavedObjectsClientContract,
agentPolicies: Array<Pick<AgentPolicy, 'id' | 'space_id'>>,
agentPolicies: Array<Pick<AgentPolicy, 'id' | 'space_ids'>>,
activeOnly: boolean = false
): Promise<boolean> => {
if (agentPolicies.length > 0) {
@ -67,10 +67,10 @@ export const hasFleetServersForPolicies = async (
soClient,
undefined,
agentPolicies
.map(({ id, space_id: spaceId }) => {
.map(({ id, space_ids: spaceIds }) => {
const space =
spaceId && spaceId !== DEFAULT_SPACE_ID
? `namespaces:"${spaceId}"`
spaceIds?.[0] && spaceIds?.[0] !== DEFAULT_SPACE_ID
? `namespaces:"${spaceIds?.[0]}"`
: `not namespaces:* or namespaces:"${DEFAULT_SPACE_ID}"`;
return `(policy_id:${id} and (${space}))`;

View file

@ -804,7 +804,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
id: packagePolicySO.id,
version: packagePolicySO.version,
...packagePolicySO.attributes,
spaceId: packagePolicySO.namespaces?.[0],
spaceIds: packagePolicySO.namespaces,
})),
total: packagePolicies?.total,
page,

View file

@ -0,0 +1,111 @@
/*
* 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 deepEqual from 'fast-deep-equal';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import {
AGENTS_INDEX,
AGENT_POLICY_SAVED_OBJECT_TYPE,
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
} from '../../../common/constants';
import { appContextService } from '../app_context';
import { agentPolicyService } from '../agent_policy';
import { ENROLLMENT_API_KEYS_INDEX } from '../../constants';
import { packagePolicyService } from '../package_policy';
import { FleetError } from '../../errors';
import { isSpaceAwarenessEnabled } from './helpers';
export async function updateAgentPolicySpaces({
agentPolicyId,
currentSpaceId,
newSpaceIds,
authorizedSpaces,
}: {
agentPolicyId: string;
currentSpaceId: string;
newSpaceIds: string[];
authorizedSpaces: string[];
}) {
const useSpaceAwareness = await isSpaceAwarenessEnabled();
if (!useSpaceAwareness || !newSpaceIds || newSpaceIds.length === 0) {
return;
}
const esClient = appContextService.getInternalUserESClient();
const soClient = appContextService.getInternalUserSOClientWithoutSpaceExtension();
const currentSpaceSoClient = appContextService.getInternalUserSOClientForSpaceId(currentSpaceId);
const existingPolicy = await agentPolicyService.get(currentSpaceSoClient, agentPolicyId);
const existingPackagePolicies = await packagePolicyService.findAllForAgentPolicy(
currentSpaceSoClient,
agentPolicyId
);
if (deepEqual(existingPolicy?.space_ids?.sort() ?? [DEFAULT_SPACE_ID], newSpaceIds.sort())) {
return;
}
const spacesToAdd = newSpaceIds.filter(
(spaceId) => !existingPolicy?.space_ids?.includes(spaceId) ?? true
);
const spacesToRemove =
existingPolicy?.space_ids?.filter((spaceId) => !newSpaceIds.includes(spaceId) ?? true) ?? [];
// Privileges check
for (const spaceId of spacesToAdd) {
if (!authorizedSpaces.includes(spaceId)) {
throw new FleetError(`No enough permissions to create policies in space ${spaceId}`);
}
}
for (const spaceId of spacesToRemove) {
if (!authorizedSpaces.includes(spaceId)) {
throw new FleetError(`No enough permissions to remove policies from space ${spaceId}`);
}
}
const res = await soClient.updateObjectsSpaces(
[
{
id: agentPolicyId,
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
},
...existingPackagePolicies.map(({ id }) => ({
id,
type: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
})),
],
spacesToAdd,
spacesToRemove,
{ refresh: 'wait_for', namespace: currentSpaceId }
);
for (const soRes of res.objects) {
if (soRes.error) {
throw soRes.error;
}
}
// Update fleet server index agents, enrollment api keys
await esClient.updateByQuery({
index: ENROLLMENT_API_KEYS_INDEX,
script: `ctx._source.namespaces = [${newSpaceIds.map((spaceId) => `"${spaceId}"`).join(',')}]`,
ignore_unavailable: true,
refresh: true,
});
await esClient.updateByQuery({
index: AGENTS_INDEX,
script: `ctx._source.namespaces = [${newSpaceIds.map((spaceId) => `"${spaceId}"`).join(',')}]`,
ignore_unavailable: true,
refresh: true,
});
}

View file

@ -41,6 +41,11 @@ function isInteger(n: number) {
export const AgentPolicyBaseSchema = {
id: schema.maybe(schema.string()),
space_ids: schema.maybe(
schema.arrayOf(schema.string(), {
minSize: 1,
})
),
name: schema.string({ minLength: 1, validate: validateNonEmptyString }),
namespace: AgentPolicyNamespaceSchema,
description: schema.maybe(schema.string()),

View file

@ -13,6 +13,7 @@ import type {
SavedObjectsClientContract,
IRouter,
} from '@kbn/core/server';
import type { GetSpaceResult } from '@kbn/spaces-plugin/common';
import type { FleetAuthz } from '../../common/authz';
import type { AgentClient } from '../services';
@ -43,6 +44,7 @@ export type FleetRequestHandlerContext = CustomRequestHandlerContext<{
readonly internalSoClient: SavedObjectsClientContract;
spaceId: string;
getAllSpaces(): Promise<GetSpaceResult[]>;
/**
* If data is to be limited to the list of integration package names. This will be set when
* authz to the API was granted only based on Package Privileges.

View file

@ -17,6 +17,8 @@ import {
httpServiceMock,
savedObjectsClientMock,
} from '@kbn/core/server/mocks';
import { createAppContextStartContractMock as fleetCreateAppContextStartContractMock } from '@kbn/fleet-plugin/server/mocks';
import { appContextService as fleetAppContextService } from '@kbn/fleet-plugin/server/services';
import type { HostInfo, MetadataListResponse } from '../../../../common/endpoint/types';
import { HostStatus } from '../../../../common/endpoint/types';
import { registerEndpointRoutes } from '.';
@ -135,6 +137,11 @@ describe('test endpoint routes', () => {
page: 1,
per_page: 10,
});
fleetAppContextService.start(
fleetCreateAppContextStartContractMock({}, false, {
withoutSpaceExtensions: mockSavedObjectClient,
})
);
mockAgentClient.getAgentStatusById.mockResolvedValue('error');
mockAgentClient.listAgents.mockResolvedValue(noUnenrolledAgent);
mockAgentPolicyService.getByIds = jest.fn().mockResolvedValueOnce([]);

View file

@ -5,6 +5,9 @@
* 2.0.
*/
import { createAppContextStartContractMock as fleetCreateAppContextStartContractMock } from '@kbn/fleet-plugin/server/mocks';
import { appContextService as fleetAppContextService } from '@kbn/fleet-plugin/server/services';
import { getESQueryHostMetadataByID, buildUnitedIndexQuery } from './query_builders';
import { metadataCurrentIndexPattern } from '../../../../common/endpoint/constants';
import { get } from 'lodash';
@ -50,6 +53,11 @@ describe('query builder', () => {
per_page: 0,
page: 0,
});
fleetAppContextService.start(
fleetCreateAppContextStartContractMock({}, false, {
withoutSpaceExtensions: soClient,
})
);
});
it('correctly builds empty query', async () => {

View file

@ -21,6 +21,8 @@ import {
import type { HostMetadata } from '../../../../common/endpoint/types';
import type { Agent, PackagePolicy } from '@kbn/fleet-plugin/common';
import type { AgentPolicyServiceInterface } from '@kbn/fleet-plugin/server/services';
import { createAppContextStartContractMock as fleetCreateAppContextStartContractMock } from '@kbn/fleet-plugin/server/mocks';
import { appContextService as fleetAppContextService } from '@kbn/fleet-plugin/server/services';
import { EndpointError } from '../../../../common/endpoint/errors';
import type { SavedObjectsClientContract } from '@kbn/core/server';
@ -38,6 +40,11 @@ describe('EndpointMetadataService', () => {
esClient = elasticsearchServiceMock.createScopedClusterClient().asInternalUser;
soClient = savedObjectsClientMock.create();
soClient.find = jest.fn().mockResolvedValue({ saved_objects: [] });
fleetAppContextService.start(
fleetCreateAppContextStartContractMock({}, false, {
withoutSpaceExtensions: soClient,
})
);
});
describe('#findHostMetadataForFleetAgents()', () => {

View file

@ -531,6 +531,7 @@ export default function (providerContext: FtrProviderContext) {
updated_by: 'elastic',
package_policies: [],
is_protected: false,
space_ids: [],
});
});
@ -962,6 +963,7 @@ export default function (providerContext: FtrProviderContext) {
inactivity_timeout: 1209600,
package_policies: [],
is_protected: false,
space_ids: [],
});
});
@ -1125,6 +1127,7 @@ export default function (providerContext: FtrProviderContext) {
package_policies: [],
monitoring_enabled: ['logs', 'metrics'],
inactivity_timeout: 1209600,
space_ids: [],
});
const listResponseAfterUpdate = await fetchPackageList();
@ -1183,6 +1186,7 @@ export default function (providerContext: FtrProviderContext) {
inactivity_timeout: 1209600,
package_policies: [],
is_protected: false,
space_ids: [],
overrides: {
agent: {
logging: {
@ -1473,6 +1477,7 @@ export default function (providerContext: FtrProviderContext) {
const {
package_policies: packagePolicies,
id,
space_ids: spaceIds,
updated_at: updatedAt,
version: policyVersion,
...rest

View file

@ -67,12 +67,14 @@ export default function (providerContext: FtrProviderContext) {
is_default_fleet_server: true,
is_managed: false,
name: 'Fleet Server Policy',
space_ids: [],
},
{
id: 'fleet-server-policy-2',
is_default_fleet_server: false,
is_managed: false,
name: 'Fleet Server Policy 2',
space_ids: [],
},
],
has_active: true,
@ -117,6 +119,7 @@ export default function (providerContext: FtrProviderContext) {
is_default_fleet_server: false,
is_managed: false,
name: 'Fleet Server Policy 2',
space_ids: [],
},
],
has_active: true,

View file

@ -10,7 +10,7 @@ import { CreateAgentPolicyResponse } from '@kbn/fleet-plugin/common';
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
import { skipIfNoDockerRegistry } from '../../helpers';
import { SpaceTestApiClient } from './api_helper';
import { cleanFleetIndices } from './helpers';
import { cleanFleetIndices, expectToRejectWithNotFound } from './helpers';
import { setupTestSpaces, TEST_SPACE_1 } from './space_helpers';
export default function (providerContext: FtrProviderContext) {
@ -81,27 +81,13 @@ export default function (providerContext: FtrProviderContext) {
await apiClient.getAgentPolicy(spaceTest1Policy1.item.id, TEST_SPACE_1);
});
it('should not allow to get a policy from a different space from the default space', async () => {
let err: Error | undefined;
try {
await apiClient.getAgentPolicy(spaceTest1Policy1.item.id);
} catch (_err) {
err = _err;
}
expect(err).to.be.an(Error);
expect(err?.message).to.match(/404 "Not Found"/);
await expectToRejectWithNotFound(() => apiClient.getAgentPolicy(spaceTest1Policy1.item.id));
});
it('should not allow to get an default space policy from a different space', async () => {
let err: Error | undefined;
try {
await apiClient.getAgentPolicy(defaultSpacePolicy1.item.id, TEST_SPACE_1);
} catch (_err) {
err = _err;
}
expect(err).to.be.an(Error);
expect(err?.message).to.match(/404 "Not Found"/);
await expectToRejectWithNotFound(() =>
apiClient.getAgentPolicy(defaultSpacePolicy1.item.id, TEST_SPACE_1)
);
});
});
});

View file

@ -15,6 +15,7 @@ import {
GetAgentsResponse,
GetOneAgentPolicyResponse,
GetOneAgentResponse,
GetOnePackagePolicyResponse,
GetPackagePoliciesResponse,
} from '@kbn/fleet-plugin/common';
import {
@ -28,21 +29,28 @@ import {
PutSpaceSettingsRequest,
GetActionStatusResponse,
PostNewAgentActionResponse,
UpdateAgentPolicyResponse,
UpdateAgentPolicyRequest,
} from '@kbn/fleet-plugin/common/types';
import {
GetUninstallTokenResponse,
GetUninstallTokensMetadataResponse,
} from '@kbn/fleet-plugin/common/types/rest_spec/uninstall_token';
import { SimplifiedPackagePolicy } from '@kbn/fleet-plugin/common/services/simplified_package_policy_helper';
import { testUsers } from '../test_users';
export class SpaceTestApiClient {
constructor(private readonly supertest: Agent) {}
constructor(
private readonly supertest: Agent,
private readonly auth = testUsers.fleet_all_int_all
) {}
private getBaseUrl(spaceId?: string) {
return spaceId ? `/s/${spaceId}` : '';
}
async setup(spaceId?: string): Promise<CreateAgentPolicyResponse> {
const { body: res } = await this.supertest
.post(`${this.getBaseUrl(spaceId)}/api/fleet/setup`)
.auth(this.auth.username, this.auth.password)
.set('kbn-xsrf', 'xxxx')
.send({})
.expect(200);
@ -57,6 +65,7 @@ export class SpaceTestApiClient {
): Promise<CreateAgentPolicyResponse> {
const { body: res } = await this.supertest
.post(`${this.getBaseUrl(spaceId)}/api/fleet/agent_policies`)
.auth(this.auth.username, this.auth.password)
.set('kbn-xsrf', 'xxxx')
.send({
name: `test ${uuidV4()}`,
@ -82,6 +91,17 @@ export class SpaceTestApiClient {
return res;
}
async getPackagePolicy(
packagePolicyId: string,
spaceId?: string
): Promise<GetOnePackagePolicyResponse> {
const { body: res } = await this.supertest
.get(`${this.getBaseUrl(spaceId)}/api/fleet/package_policies/${packagePolicyId}`)
.expect(200);
return res;
}
async getPackagePolicies(spaceId?: string): Promise<GetPackagePoliciesResponse> {
const { body: res } = await this.supertest
.get(`${this.getBaseUrl(spaceId)}/api/fleet/package_policies`)
@ -89,6 +109,7 @@ export class SpaceTestApiClient {
return res;
}
async createFleetServerPolicy(spaceId?: string): Promise<CreateAgentPolicyResponse> {
const { body: res } = await this.supertest
.post(`${this.getBaseUrl(spaceId)}/api/fleet/agent_policies`)
@ -121,6 +142,29 @@ export class SpaceTestApiClient {
return res;
}
async putAgentPolicy(
policyId: string,
data: Partial<UpdateAgentPolicyRequest['body']>,
spaceId?: string
): Promise<UpdateAgentPolicyResponse> {
const { body: res, statusCode } = await this.supertest
.put(`${this.getBaseUrl(spaceId)}/api/fleet/agent_policies/${policyId}`)
.auth(this.auth.username, this.auth.password)
.send({
...data,
})
.set('kbn-xsrf', 'xxxx');
if (statusCode === 200) {
return res;
}
if (statusCode === 404) {
throw new Error('404 "Not Found"');
} else {
throw new Error(`${statusCode} ${res?.error} ${res.message}`);
}
}
async getAgentPolicies(spaceId?: string): Promise<GetAgentPoliciesResponse> {
const { body: res } = await this.supertest
.get(`${this.getBaseUrl(spaceId)}/api/fleet/agent_policies`)

View file

@ -0,0 +1,197 @@
/*
* 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 expect from '@kbn/expect';
import { CreateAgentPolicyResponse, GetOnePackagePolicyResponse } from '@kbn/fleet-plugin/common';
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
import { skipIfNoDockerRegistry } from '../../helpers';
import { SpaceTestApiClient } from './api_helper';
import {
cleanFleetIndices,
createFleetAgent,
expectToRejectWithError,
expectToRejectWithNotFound,
} from './helpers';
import { setupTestSpaces, TEST_SPACE_1 } from './space_helpers';
import { testUsers, setupTestUsers } from '../test_users';
export default function (providerContext: FtrProviderContext) {
const { getService } = providerContext;
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const esClient = getService('es');
const kibanaServer = getService('kibanaServer');
describe('change space agent policies', async function () {
skipIfNoDockerRegistry(providerContext);
const apiClient = new SpaceTestApiClient(supertest);
before(async () => {
await setupTestUsers(getService('security'), true);
await kibanaServer.savedObjects.cleanStandardList();
await kibanaServer.savedObjects.cleanStandardList({
space: TEST_SPACE_1,
});
await cleanFleetIndices(esClient);
});
after(async () => {
await kibanaServer.savedObjects.cleanStandardList();
await kibanaServer.savedObjects.cleanStandardList({
space: TEST_SPACE_1,
});
await cleanFleetIndices(esClient);
});
setupTestSpaces(providerContext);
let defaultSpacePolicy1: CreateAgentPolicyResponse;
let defaultPackagePolicy1: GetOnePackagePolicyResponse;
before(async () => {
await apiClient.postEnableSpaceAwareness();
const _policyRes = await apiClient.createAgentPolicy();
defaultSpacePolicy1 = _policyRes;
await apiClient.installPackage({
pkgName: 'nginx',
pkgVersion: '1.20.0',
force: true, // To avoid package verification
});
await createFleetAgent(esClient, defaultSpacePolicy1.item.id);
const packagePolicyRes = await apiClient.createPackagePolicy(undefined, {
policy_ids: [defaultSpacePolicy1.item.id],
name: `test-nginx-${Date.now()}`,
description: 'test',
package: {
name: 'nginx',
version: '1.20.0',
},
inputs: {},
});
defaultPackagePolicy1 = packagePolicyRes;
});
describe('PUT /agent_policies/{id}', () => {
beforeEach(async () => {
// Reset policy in default space
await apiClient
.putAgentPolicy(
defaultSpacePolicy1.item.id,
{
name: 'tata',
namespace: 'default',
description: 'tata',
space_ids: ['default'],
},
TEST_SPACE_1
)
.catch(() => {});
await apiClient
.putAgentPolicy(defaultSpacePolicy1.item.id, {
name: 'tata',
namespace: 'default',
description: 'tata',
space_ids: ['default'],
})
.catch(() => {});
});
async function assertPolicyAvailableInSpace(spaceId?: string) {
await apiClient.getAgentPolicy(defaultSpacePolicy1.item.id, spaceId);
await apiClient.getPackagePolicy(defaultPackagePolicy1.item.id, spaceId);
const enrollmentApiKeys = await apiClient.getEnrollmentApiKeys(spaceId);
expect(
enrollmentApiKeys.items.find((item) => item.policy_id === defaultSpacePolicy1.item.id)
).not.to.be(undefined);
const agents = await apiClient.getAgents(spaceId);
expect(agents.total).to.be(1);
}
async function assertPolicyNotAvailableInSpace(spaceId?: string) {
await expectToRejectWithNotFound(() =>
apiClient.getPackagePolicy(defaultPackagePolicy1.item.id, spaceId)
);
await expectToRejectWithNotFound(() =>
apiClient.getAgentPolicy(defaultSpacePolicy1.item.id, spaceId)
);
const enrollmentApiKeys = await apiClient.getEnrollmentApiKeys(spaceId);
expect(
enrollmentApiKeys.items.find((item) => item.policy_id === defaultSpacePolicy1.item.id)
).to.be(undefined);
const agents = await apiClient.getAgents(spaceId);
expect(agents.total).to.be(0);
}
it('should allow set policy in multiple space', async () => {
await apiClient.putAgentPolicy(defaultSpacePolicy1.item.id, {
name: 'tata',
namespace: 'default',
description: 'tata',
space_ids: ['default', TEST_SPACE_1],
});
await assertPolicyAvailableInSpace();
await assertPolicyAvailableInSpace(TEST_SPACE_1);
});
it('should allow set policy in test space only', async () => {
await apiClient.putAgentPolicy(defaultSpacePolicy1.item.id, {
name: 'tata',
namespace: 'default',
description: 'tata',
space_ids: [TEST_SPACE_1],
});
await assertPolicyNotAvailableInSpace();
await assertPolicyAvailableInSpace(TEST_SPACE_1);
});
it('should not allow add policy to a space where user do not have access', async () => {
const testApiClient = new SpaceTestApiClient(
supertestWithoutAuth,
testUsers.fleet_all_int_all_default_space_only
);
await expectToRejectWithError(
() =>
testApiClient.putAgentPolicy(defaultSpacePolicy1.item.id, {
name: 'tata',
namespace: 'default',
description: 'tata',
space_ids: ['default', TEST_SPACE_1],
}),
/400 Bad Request No enough permissions to create policies in space test1/
);
});
it('should not allow to remove policy from a space where user do not have access', async () => {
await apiClient.putAgentPolicy(defaultSpacePolicy1.item.id, {
name: 'tata',
namespace: 'default',
description: 'tata',
space_ids: ['default', TEST_SPACE_1],
});
const testApiClient = new SpaceTestApiClient(
supertestWithoutAuth,
testUsers.fleet_all_int_all_default_space_only
);
await expectToRejectWithError(
() =>
testApiClient.putAgentPolicy(defaultSpacePolicy1.item.id, {
name: 'tata',
namespace: 'default',
description: 'tata',
space_ids: ['default'],
}),
/400 Bad Request No enough permissions to remove policies from space test1/
);
});
});
});
}

View file

@ -6,6 +6,7 @@
*/
import { Client } from '@elastic/elasticsearch';
import expect from '@kbn/expect';
import {
AGENT_ACTIONS_INDEX,
@ -17,6 +18,21 @@ import { ENROLLMENT_API_KEYS_INDEX } from '@kbn/fleet-plugin/common/constants';
const ES_INDEX_OPTIONS = { headers: { 'X-elastic-product-origin': 'fleet' } };
export async function expectToRejectWithNotFound(fn: any) {
await expectToRejectWithError(fn, /404 "Not Found"/);
}
export async function expectToRejectWithError(fn: any, errRegexp: RegExp) {
let err: Error | undefined;
try {
await fn();
} catch (_err) {
err = _err;
}
expect(err).to.be.an(Error);
expect(err?.message).to.match(errRegexp);
}
export async function cleanFleetIndices(esClient: Client) {
await Promise.all([
esClient.deleteByQuery({

View file

@ -15,6 +15,7 @@ export default function loadTests({ loadTestFile }) {
loadTestFile(require.resolve('./package_install'));
loadTestFile(require.resolve('./space_settings'));
loadTestFile(require.resolve('./actions'));
loadTestFile(require.resolve('./change_space_agent_policies'));
loadTestFile(require.resolve('./space_awareness_migration'));
});
}

View file

@ -11,7 +11,7 @@ import { UninstallTokenMetadata } from '@kbn/fleet-plugin/common/types/models/un
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
import { skipIfNoDockerRegistry } from '../../helpers';
import { SpaceTestApiClient } from './api_helper';
import { cleanFleetIndices } from './helpers';
import { cleanFleetIndices, expectToRejectWithNotFound } from './helpers';
import { setupTestSpaces, TEST_SPACE_1 } from './space_helpers';
export default function (providerContext: FtrProviderContext) {
@ -89,27 +89,13 @@ export default function (providerContext: FtrProviderContext) {
await apiClient.getUninstallToken(spaceTest1Token.id, TEST_SPACE_1);
});
it('should not allow to get an uninstall token from a different space from the default space', async () => {
let err: Error | undefined;
try {
await apiClient.getUninstallToken(spaceTest1Token.id);
} catch (_err) {
err = _err;
}
expect(err).to.be.an(Error);
expect(err?.message).to.match(/404 "Not Found"/);
await expectToRejectWithNotFound(() => apiClient.getUninstallToken(spaceTest1Token.id));
});
it('should not allow to get an default space uninstall token from a different space', async () => {
let err: Error | undefined;
try {
await apiClient.getUninstallToken(defaultSpaceToken.id, TEST_SPACE_1);
} catch (_err) {
err = _err;
}
expect(err).to.be.an(Error);
expect(err?.message).to.match(/404 "Not Found"/);
await expectToRejectWithNotFound(() =>
apiClient.getUninstallToken(defaultSpaceToken.id, TEST_SPACE_1)
);
});
});
});

View file

@ -21,6 +21,17 @@ export const testUsers: {
username: 'fleet_all_int_all',
password: 'changeme',
},
fleet_all_int_all_default_space_only: {
permissions: {
feature: {
fleetv2: ['all'],
fleet: ['all'],
},
spaces: ['default'],
},
username: 'fleet_all_int_all_default_space_only',
password: 'changeme',
},
fleet_read_only: {
permissions: {
feature: {
@ -231,8 +242,11 @@ export const testUsers: {
},
};
export const setupTestUsers = async (security: SecurityService) => {
export const setupTestUsers = async (security: SecurityService, spaceAwarenessEnabled = false) => {
for (const roleName in testUsers) {
if (!spaceAwarenessEnabled && roleName === 'fleet_all_int_all_default_space_only') {
continue;
}
if (Object.hasOwn(testUsers, roleName)) {
const user = testUsers[roleName];