diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index 59cd87288e8ec..31a4b111b69d0 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -480,6 +480,7 @@ "status", "uuid" ], + "fleet-space-settings": [], "fleet-uninstall-tokens": [ "policy_id", "token_plain" diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index 47602ca181b5e..6a9e57fa1a05b 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -1612,6 +1612,10 @@ } } }, + "fleet-space-settings": { + "dynamic": false, + "properties": {} + }, "fleet-uninstall-tokens": { "dynamic": false, "properties": { diff --git a/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts b/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts index 12e3bfc56d980..f15fb0035d670 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts @@ -128,6 +128,7 @@ const STANDARD_LIST_TYPES = [ 'fleet-fleet-server-host', 'fleet-proxy', 'fleet-uninstall-tokens', + 'fleet-space-settings', ]; /** diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index 51c06947d224b..84cdfb9217adf 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -104,6 +104,7 @@ describe('checking migration metadata changes on all registered SO types', () => "fleet-preconfiguration-deletion-record": "c52ea1e13c919afe8a5e8e3adbb7080980ecc08e", "fleet-proxy": "6cb688f0d2dd856400c1dbc998b28704ff70363d", "fleet-setup-lock": "0dc784792c79b5af5a6e6b5dcac06b0dbaa90bde", + "fleet-space-settings": "b278e82a33978900e53a1253884b5bdbd929c9bb", "fleet-uninstall-tokens": "ed8aa37e3cdd69e4360709e64944bb81cae0c025", "graph-workspace": "5cc6bb1455b078fd848c37324672163f09b5e376", "guided-onboarding-guide-state": "d338972ed887ac480c09a1a7fbf582d6a3827c91", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts index 2228b19956bb3..8d0670b977091 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts @@ -72,6 +72,7 @@ const previouslyRegisteredTypes = [ 'fleet-proxy', 'fleet-uninstall-tokens', 'fleet-setup-lock', + 'fleet-space-settings', 'graph-workspace', 'guided-setup-state', 'guided-onboarding-guide-state', diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts index 0ff598fc0dd47..59ae42239db20 100644 --- a/x-pack/plugins/fleet/common/constants/routes.ts +++ b/x-pack/plugins/fleet/common/constants/routes.ts @@ -123,6 +123,8 @@ export const SETTINGS_API_ROUTES = { INFO_PATTERN: `${API_ROOT}/settings`, UPDATE_PATTERN: `${API_ROOT}/settings`, ENROLLMENT_INFO_PATTERN: `${INTERNAL_ROOT}/settings/enrollment`, + SPACE_INFO_PATTERN: `${API_ROOT}/space_settings`, + SPACE_UPDATE_PATTERN: `${API_ROOT}/space_settings`, }; // App API routes diff --git a/x-pack/plugins/fleet/common/constants/settings.ts b/x-pack/plugins/fleet/common/constants/settings.ts index 423e71edf10e6..b95052e66bedb 100644 --- a/x-pack/plugins/fleet/common/constants/settings.ts +++ b/x-pack/plugins/fleet/common/constants/settings.ts @@ -7,4 +7,8 @@ export const GLOBAL_SETTINGS_SAVED_OBJECT_TYPE = 'ingest_manager_settings'; +export const SPACE_SETTINGS_SAVED_OBJECT_TYPE = 'fleet-space-settings'; + +export const SPACE_SETTINGS_ID_SUFFIX = '-default-settings'; + export const GLOBAL_SETTINGS_ID = 'fleet-default-settings'; diff --git a/x-pack/plugins/fleet/common/errors.ts b/x-pack/plugins/fleet/common/errors.ts index c43e4a6284869..c41f6238f8647 100644 --- a/x-pack/plugins/fleet/common/errors.ts +++ b/x-pack/plugins/fleet/common/errors.ts @@ -16,6 +16,7 @@ export class FleetError extends Error { } } +export class PolicyNamespaceValidationError extends FleetError {} export class PackagePolicyValidationError extends FleetError {} export class MessageSigningError extends FleetError {} diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index e0b6fe6c03e7e..4ec0647480165 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -35,6 +35,10 @@ export interface FleetConfigType { url: string; }; }; + spaceSettings?: Array<{ + space_id: string; + allowed_namespace_prefixes: string[] | null; + }>; agentPolicies?: PreconfiguredAgentPolicy[]; packages?: PreconfiguredPackage[]; outputs?: PreconfiguredOutput[]; diff --git a/x-pack/plugins/fleet/common/types/rest_spec/settings.ts b/x-pack/plugins/fleet/common/types/rest_spec/settings.ts index 73ad6a3a219fc..889da89b130c7 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/settings.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/settings.ts @@ -46,3 +46,14 @@ export interface GetEnrollmentSettingsResponse { }; download_source?: DownloadSource; } +export interface PutSpaceSettingsRequest { + body: { + allowed_namespace_prefixes?: string[]; + }; +} + +export interface GetSpaceSettingsResponse { + item: { + allowed_namespace_prefixes?: string[]; + }; +} diff --git a/x-pack/plugins/fleet/server/config.ts b/x-pack/plugins/fleet/server/config.ts index 87df985be7a51..628abfb38d718 100644 --- a/x-pack/plugins/fleet/server/config.ts +++ b/x-pack/plugins/fleet/server/config.ts @@ -19,6 +19,7 @@ import { PreconfiguredOutputsSchema, PreconfiguredFleetServerHostsSchema, PreconfiguredFleetProxiesSchema, + PreconfiguredSpaceSettingsSchema, } from './types'; import { BULK_CREATE_MAX_ARTIFACTS_BYTES } from './services/artifacts/artifacts'; @@ -154,6 +155,7 @@ export const config: PluginConfigDescriptor = { outputs: PreconfiguredOutputsSchema, fleetServerHosts: PreconfiguredFleetServerHostsSchema, proxies: PreconfiguredFleetProxiesSchema, + spaceSettings: PreconfiguredSpaceSettingsSchema, agentIdVerificationEnabled: schema.boolean({ defaultValue: true }), setup: schema.maybe( schema.object({ diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index d727cd30c6385..8ea5297ecd59b 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -50,6 +50,7 @@ export { OUTPUT_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE, ASSETS_SAVED_OBJECT_TYPE, + SPACE_SETTINGS_SAVED_OBJECT_TYPE, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, UNINSTALL_TOKENS_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 4256a4a90e537..fa3eefd90c227 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -88,6 +88,7 @@ import { PLUGIN_ID, PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, FLEET_PROXY_SAVED_OBJECT_TYPE, + SPACE_SETTINGS_SAVED_OBJECT_TYPE, } from './constants'; import { registerEncryptedSavedObjects, registerSavedObjects } from './saved_objects'; import { registerRoutes } from './routes'; @@ -190,6 +191,7 @@ const allSavedObjectTypes = [ DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE, FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, FLEET_PROXY_SAVED_OBJECT_TYPE, + SPACE_SETTINGS_SAVED_OBJECT_TYPE, ]; /** diff --git a/x-pack/plugins/fleet/server/routes/index.ts b/x-pack/plugins/fleet/server/routes/index.ts index 29efa03967ea0..9257d672848a7 100644 --- a/x-pack/plugins/fleet/server/routes/index.ts +++ b/x-pack/plugins/fleet/server/routes/index.ts @@ -40,7 +40,7 @@ export function registerRoutes(fleetAuthzRouter: FleetAuthzRouter, config: Fleet registerAgentPolicyRoutes(fleetAuthzRouter); registerPackagePolicyRoutes(fleetAuthzRouter); registerOutputRoutes(fleetAuthzRouter); - registerSettingsRoutes(fleetAuthzRouter); + registerSettingsRoutes(fleetAuthzRouter, config); registerDataStreamRoutes(fleetAuthzRouter); registerPreconfigurationRoutes(fleetAuthzRouter); registerFleetServerHostRoutes(fleetAuthzRouter); diff --git a/x-pack/plugins/fleet/server/routes/settings/index.ts b/x-pack/plugins/fleet/server/routes/settings/index.ts index a72d2da7805d5..b9f672627daa7 100644 --- a/x-pack/plugins/fleet/server/routes/settings/index.ts +++ b/x-pack/plugins/fleet/server/routes/settings/index.ts @@ -5,71 +5,68 @@ * 2.0. */ -import type { TypeOf } from '@kbn/config-schema'; - +import { parseExperimentalConfigValue } from '../../../common/experimental_features'; import { API_VERSIONS } from '../../../common/constants'; import type { FleetAuthzRouter } from '../../services/security'; - import { SETTINGS_API_ROUTES } from '../../constants'; -import type { FleetRequestHandler } from '../../types'; import { PutSettingsRequestSchema, GetSettingsRequestSchema, GetEnrollmentSettingsRequestSchema, + GetSpaceSettingsRequestSchema, + PutSpaceSettingsRequestSchema, } from '../../types'; -import { defaultFleetErrorHandler } from '../../errors'; -import { settingsService, agentPolicyService, appContextService } from '../../services'; +import type { FleetConfigType } from '../../config'; import { getEnrollmentSettingsHandler } from './enrollment_settings_handler'; -export const getSettingsHandler: FleetRequestHandler = async (context, request, response) => { - const soClient = (await context.fleet).internalSoClient; - - try { - const settings = await settingsService.getSettings(soClient); - const body = { - item: settings, - }; - return response.ok({ body }); - } catch (error) { - if (error.isBoom && error.output.statusCode === 404) { - return response.notFound({ - body: { message: `Settings not found` }, - }); - } - - return defaultFleetErrorHandler({ error, response }); - } -}; - -export const putSettingsHandler: FleetRequestHandler< - undefined, - undefined, - TypeOf -> = async (context, request, response) => { - const soClient = (await context.fleet).internalSoClient; - const esClient = (await context.core).elasticsearch.client.asInternalUser; - const user = appContextService.getSecurityCore().authc.getCurrentUser(request) || undefined; +import { + getSettingsHandler, + getSpaceSettingsHandler, + putSettingsHandler, + putSpaceSettingsHandler, +} from './settings_handler'; - try { - const settings = await settingsService.saveSettings(soClient, request.body); - await agentPolicyService.bumpAllAgentPolicies(esClient, { user }); - const body = { - item: settings, - }; - return response.ok({ body }); - } catch (error) { - if (error.isBoom && error.output.statusCode === 404) { - return response.notFound({ - body: { message: `Settings not found` }, - }); - } +export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType) => { + const experimentalFeatures = parseExperimentalConfigValue(config.enableExperimental); + if (experimentalFeatures.useSpaceAwareness) { + router.versioned + .get({ + path: SETTINGS_API_ROUTES.SPACE_INFO_PATTERN, + fleetAuthz: (authz) => { + return ( + authz.fleet.readSettings || + authz.integrations.writeIntegrationPolicies || + authz.fleet.allAgentPolicies + ); + }, + description: `Get space settings`, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { request: GetSpaceSettingsRequestSchema }, + }, + getSpaceSettingsHandler + ); - return defaultFleetErrorHandler({ error, response }); + router.versioned + .put({ + path: SETTINGS_API_ROUTES.SPACE_UPDATE_PATTERN, + fleetAuthz: { + fleet: { allSettings: true }, + }, + description: `Put space settings`, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { request: PutSpaceSettingsRequestSchema }, + }, + putSpaceSettingsHandler + ); } -}; -export const registerRoutes = (router: FleetAuthzRouter) => { router.versioned .get({ path: SETTINGS_API_ROUTES.INFO_PATTERN, diff --git a/x-pack/plugins/fleet/server/routes/settings/settings_handler.ts b/x-pack/plugins/fleet/server/routes/settings/settings_handler.ts new file mode 100644 index 0000000000000..c959638d0fc6b --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/settings/settings_handler.ts @@ -0,0 +1,100 @@ +/* + * 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 { TypeOf } from '@kbn/config-schema'; + +import type { + FleetRequestHandler, + PutSettingsRequestSchema, + PutSpaceSettingsRequestSchema, +} from '../../types'; +import { defaultFleetErrorHandler } from '../../errors'; +import { settingsService, agentPolicyService, appContextService } from '../../services'; +import { getSpaceSettings, saveSpaceSettings } from '../../services/spaces/space_settings'; + +export const getSpaceSettingsHandler: FleetRequestHandler = async (context, request, response) => { + try { + const soClient = (await context.fleet).internalSoClient; + const settings = await getSpaceSettings(soClient.getCurrentNamespace()); + const body = { + item: settings, + }; + return response.ok({ body }); + } catch (error) { + return defaultFleetErrorHandler({ error, response }); + } +}; + +export const putSpaceSettingsHandler: FleetRequestHandler< + undefined, + undefined, + TypeOf +> = async (context, request, response) => { + try { + const soClient = (await context.fleet).internalSoClient; + await saveSpaceSettings({ + settings: { + allowed_namespace_prefixes: request.body.allowed_namespace_prefixes, + }, + spaceId: soClient.getCurrentNamespace(), + }); + const settings = await settingsService.getSettings(soClient); + const body = { + item: settings, + }; + return response.ok({ body }); + } catch (error) { + return defaultFleetErrorHandler({ error, response }); + } +}; + +export const getSettingsHandler: FleetRequestHandler = async (context, request, response) => { + const soClient = (await context.fleet).internalSoClient; + + try { + const settings = await settingsService.getSettings(soClient); + const body = { + item: settings, + }; + return response.ok({ body }); + } catch (error) { + if (error.isBoom && error.output.statusCode === 404) { + return response.notFound({ + body: { message: `Settings not found` }, + }); + } + + return defaultFleetErrorHandler({ error, response }); + } +}; + +export const putSettingsHandler: FleetRequestHandler< + undefined, + undefined, + TypeOf +> = async (context, request, response) => { + const soClient = (await context.fleet).internalSoClient; + const esClient = (await context.core).elasticsearch.client.asInternalUser; + const user = appContextService.getSecurityCore().authc.getCurrentUser(request) || undefined; + + try { + const settings = await settingsService.saveSettings(soClient, request.body); + await agentPolicyService.bumpAllAgentPolicies(esClient, { user }); + const body = { + item: settings, + }; + return response.ok({ body }); + } catch (error) { + if (error.isBoom && error.output.statusCode === 404) { + return response.notFound({ + body: { message: `Settings not found` }, + }); + } + + return defaultFleetErrorHandler({ error, response }); + } +}; diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 3f91146536a9d..4b76a952a0f3b 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -24,6 +24,7 @@ import { INGEST_SAVED_OBJECT_INDEX, UNINSTALL_TOKENS_SAVED_OBJECT_TYPE, FLEET_SETUP_LOCK_TYPE, + SPACE_SETTINGS_SAVED_OBJECT_TYPE, } from '../constants'; import { migrateSyntheticsPackagePolicyToV8120 } from './migrations/synthetics/to_v8_12_0'; @@ -123,6 +124,22 @@ export const getSavedObjectTypes = ( }, }, }, + [SPACE_SETTINGS_SAVED_OBJECT_TYPE]: { + name: SPACE_SETTINGS_SAVED_OBJECT_TYPE, + indexPattern: INGEST_SAVED_OBJECT_INDEX, + hidden: false, + namespaceType: 'single', + management: { + importableAndExportable: false, + }, + mappings: { + dynamic: false, + properties: { + // allowed_namespace_prefixes: { enabled: false }, + // managed_by: { type: 'keyword', index: false }, + }, + }, + }, // Deprecated [GLOBAL_SETTINGS_SAVED_OBJECT_TYPE]: { name: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index ed3cefb2faf94..9ddbdef52ce73 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -107,6 +107,7 @@ import { getFullAgentPolicy, validateOutputForPolicy } from './agent_policies'; import { auditLoggingService } from './audit_logging'; import { licenseService } from './license'; import { createSoFindIterable } from './utils/create_so_find_iterable'; +import { validatePolicyNamespaceForSpace } from './spaces/policy_namespaces'; const SAVED_OBJECT_TYPE = AGENT_POLICY_SAVED_OBJECT_TYPE; @@ -329,7 +330,10 @@ class AgentPolicyService { this.checkAgentless(agentPolicy); await this.requireUniqueName(soClient, agentPolicy); - + await validatePolicyNamespaceForSpace({ + spaceId: soClient.getCurrentNamespace(), + namespace: agentPolicy.namespace, + }); await validateOutputForPolicy(soClient, agentPolicy); const newSo = await soClient.create( @@ -592,6 +596,12 @@ class AgentPolicyService { name: agentPolicy.name, }); } + if (agentPolicy.namespace) { + await validatePolicyNamespaceForSpace({ + spaceId: soClient.getCurrentNamespace(), + namespace: agentPolicy.namespace, + }); + } const existingAgentPolicy = await this.get(soClient, id, true); diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index e244be59b8012..794269ba78ca6 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -132,6 +132,7 @@ import { import { getPackageAssetsMap } from './epm/packages/get'; import { validateOutputForNewPackagePolicy } from './agent_policies/outputs_helpers'; import type { PackagePolicyClientFetchAllItemIdsOptions } from './package_policy_service'; +import { validatePolicyNamespaceForSpace } from './spaces/policy_namespaces'; export type InputsOverride = Partial & { vars?: Array; @@ -242,6 +243,12 @@ class PackagePolicyClientImpl implements PackagePolicyClient { if (!options?.skipUniqueNameVerification) { await requireUniqueName(soClient, enrichedPackagePolicy); } + if (enrichedPackagePolicy.namespace) { + await validatePolicyNamespaceForSpace({ + namespace: enrichedPackagePolicy.namespace, + spaceId: soClient.getCurrentNamespace(), + }); + } let elasticsearchPrivileges: NonNullable['privileges']; let inputs = getInputsWithStreamIds(enrichedPackagePolicy, packagePolicyId); @@ -847,6 +854,13 @@ class PackagePolicyClientImpl implements PackagePolicyClient { await requireUniqueName(soClient, enrichedPackagePolicy, id); } + if (packagePolicy.namespace) { + await validatePolicyNamespaceForSpace({ + namespace: packagePolicy.namespace, + spaceId: soClient.getCurrentNamespace(), + }); + } + // eslint-disable-next-line prefer-const let { version, ...restOfPackagePolicy } = packagePolicy; let inputs = getInputsWithStreamIds(restOfPackagePolicy, oldPackagePolicy.id); diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/space_settings.ts b/x-pack/plugins/fleet/server/services/preconfiguration/space_settings.ts new file mode 100644 index 0000000000000..f3cdb4de81d1b --- /dev/null +++ b/x-pack/plugins/fleet/server/services/preconfiguration/space_settings.ts @@ -0,0 +1,89 @@ +/* + * 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 { TypeOf } from '@kbn/config-schema'; +import type { SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; +import deepEqual from 'fast-deep-equal'; + +import { appContextService } from '..'; +import { SO_SEARCH_LIMIT, SPACE_SETTINGS_SAVED_OBJECT_TYPE } from '../../constants'; +import type { PreconfiguredSpaceSettingsSchema, SpaceSettingsSOAttributes } from '../../types'; +import { saveSpaceSettings } from '../spaces/space_settings'; + +export async function ensureSpaceSettings( + configSpaceSettingsArray: TypeOf +) { + const soClient = appContextService.getInternalUserSOClientWithoutSpaceExtension(); + + // Get all existing config space + const existingConfigSpaceSettingsSOs = await soClient.find({ + type: SPACE_SETTINGS_SAVED_OBJECT_TYPE, + perPage: SO_SEARCH_LIMIT, + namespaces: ['*'], + }); + + const existingConfigSpaceSettingsSOMap = existingConfigSpaceSettingsSOs.saved_objects.reduce( + (acc, so) => { + acc.set(so.namespaces?.[0] ?? DEFAULT_SPACE_ID, so); + + return acc; + }, + new Map>() + ); + + for (const configSpaceSettings of configSpaceSettingsArray) { + // Check for existing + const existingConfigSpaceSettingsSO = existingConfigSpaceSettingsSOMap.get( + configSpaceSettings.space_id + ); + + if (!existingConfigSpaceSettingsSO) { + await saveSpaceSettings({ + spaceId: configSpaceSettings.space_id, + settings: { + allowed_namespace_prefixes: configSpaceSettings.allowed_namespace_prefixes, + managed_by: 'kibana_config', + }, + managedBy: 'kibana_config', + }); + } else if ( + !deepEqual( + existingConfigSpaceSettingsSO.attributes.allowed_namespace_prefixes, + configSpaceSettings.allowed_namespace_prefixes + ) || + !existingConfigSpaceSettingsSO.attributes.managed_by + ) { + await saveSpaceSettings({ + spaceId: configSpaceSettings.space_id, + settings: { + allowed_namespace_prefixes: configSpaceSettings.allowed_namespace_prefixes, + managed_by: 'kibana_config', + }, + managedBy: 'kibana_config', + }); + } + } + + for (const spaceId of existingConfigSpaceSettingsSOMap.keys()) { + if ( + !configSpaceSettingsArray.some( + (config) => + config.space_id === spaceId && + existingConfigSpaceSettingsSOMap.get(spaceId)?.attributes?.managed_by === 'kibana_config' + ) + ) { + await saveSpaceSettings({ + spaceId, + settings: { + managed_by: null, + }, + managedBy: 'kibana_config', + }); + } + } +} diff --git a/x-pack/plugins/fleet/server/services/setup.test.ts b/x-pack/plugins/fleet/server/services/setup.test.ts index 03a52c27abffe..5e88fac35e140 100644 --- a/x-pack/plugins/fleet/server/services/setup.test.ts +++ b/x-pack/plugins/fleet/server/services/setup.test.ts @@ -20,6 +20,7 @@ import { setupFleet } from './setup'; jest.mock('./preconfiguration'); jest.mock('./preconfiguration/outputs'); jest.mock('./preconfiguration/fleet_proxies'); +jest.mock('./preconfiguration/space_settings'); jest.mock('./settings'); jest.mock('./output'); jest.mock('./download_source'); diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index a1df9c7d9d609..e59eb229ad8e5 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -52,6 +52,7 @@ import { import { cleanUpOldFileIndices } from './setup/clean_old_fleet_indices'; import type { UninstallTokenInvalidError } from './security/uninstall_token_service'; import { ensureAgentPoliciesFleetServerKeysAndPolicies } from './setup/fleet_server_policies_enrollment_keys'; +import { ensureSpaceSettings } from './preconfiguration/space_settings'; export interface SetupStatus { isInitialized: boolean; @@ -191,6 +192,9 @@ async function createSetupSideEffects( getPreconfiguredFleetServerHostFromConfig(appContextService.getConfig()) ); + logger.debug('Setting up Space settings'); + await ensureSpaceSettings(appContextService.getConfig()?.spaceSettings ?? []); + logger.debug('Setting up Fleet outputs'); await Promise.all([ ensurePreconfiguredOutputs( diff --git a/x-pack/plugins/fleet/server/services/spaces/policy_namespaces.test.ts b/x-pack/plugins/fleet/server/services/spaces/policy_namespaces.test.ts new file mode 100644 index 0000000000000..8cee5b4e618be --- /dev/null +++ b/x-pack/plugins/fleet/server/services/spaces/policy_namespaces.test.ts @@ -0,0 +1,118 @@ +/* + * 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 { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks'; +import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; + +import { appContextService } from '../app_context'; + +import { validatePolicyNamespaceForSpace } from './policy_namespaces'; + +jest.mock('../app_context'); + +describe('validatePolicyNamespaceForSpace', () => { + function createSavedsClientMock(settingsAttributes?: any) { + const client = savedObjectsClientMock.create(); + + if (settingsAttributes) { + client.get.mockResolvedValue({ + attributes: settingsAttributes, + } as any); + } else { + client.get.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError('Not found') + ); + } + + jest.mocked(appContextService.getInternalUserSOClientForSpaceId).mockReturnValue(client); + + return client; + } + + beforeEach(() => { + jest + .mocked(appContextService.getExperimentalFeatures) + .mockReturnValue({ useSpaceAwareness: true } as any); + }); + + it('should retrieve settings based on given spaceId', async () => { + const soClient = createSavedsClientMock(); + await validatePolicyNamespaceForSpace({ + spaceId: 'test1', + namespace: 'test', + }); + + expect(soClient.get).toBeCalledWith('fleet-space-settings', 'test1-default-settings'); + }); + + it('should retrieve default space settings based if no spaceId is provided', async () => { + const soClient = createSavedsClientMock(); + await validatePolicyNamespaceForSpace({ + namespace: 'test', + }); + + expect(soClient.get).toBeCalledWith('fleet-space-settings', 'default-default-settings'); + }); + + it('should accept valid namespace if there is some allowed_namespace_prefixes configured', async () => { + createSavedsClientMock({ + allowed_namespace_prefixes: ['tata', 'test', 'toto'], + }); + await validatePolicyNamespaceForSpace({ + spaceId: 'test1', + namespace: 'test', + }); + }); + + it('should accept valid namespace matching prefix if there is some allowed_namespace_prefixes configured', async () => { + createSavedsClientMock({ + allowed_namespace_prefixes: ['tata', 'test', 'toto'], + }); + await validatePolicyNamespaceForSpace({ + spaceId: 'test1', + namespace: 'testvalid', + }); + }); + + it('should accept any namespace if there is no settings configured', async () => { + createSavedsClientMock(); + await validatePolicyNamespaceForSpace({ + spaceId: 'test1', + namespace: 'testvalid', + }); + }); + + it('should accept any namespace if there is no allowed_namespace_prefixes configured', async () => { + createSavedsClientMock(); + await validatePolicyNamespaceForSpace({ + spaceId: 'test1', + namespace: 'testvalid', + }); + }); + + it('should throw if the namespace is not matching allowed_namespace_prefixes', async () => { + createSavedsClientMock({ allowed_namespace_prefixes: ['tata', 'test', 'toto'] }); + await expect( + validatePolicyNamespaceForSpace({ + spaceId: 'test1', + namespace: 'notvalid', + }) + ).rejects.toThrowError(/Invalid namespace, supported namespace prefixes: tata, test, toto/); + }); + + it('should not validate if feature flag is off', async () => { + jest + .mocked(appContextService.getExperimentalFeatures) + .mockReturnValue({ useSpaceAwareness: false } as any); + createSavedsClientMock({ allowed_namespace_prefixes: ['tata', 'test', 'toto'] }); + + await validatePolicyNamespaceForSpace({ + spaceId: 'test1', + namespace: 'notvalid', + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/spaces/policy_namespaces.ts b/x-pack/plugins/fleet/server/services/spaces/policy_namespaces.ts new file mode 100644 index 0000000000000..f3d7ee303985c --- /dev/null +++ b/x-pack/plugins/fleet/server/services/spaces/policy_namespaces.ts @@ -0,0 +1,44 @@ +/* + * 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 { appContextService } from '../app_context'; +import { PolicyNamespaceValidationError } from '../../../common/errors'; + +import { getSpaceSettings } from './space_settings'; + +export async function validatePolicyNamespaceForSpace({ + namespace, + spaceId, +}: { + namespace: string; + spaceId?: string; +}) { + const experimentalFeature = appContextService.getExperimentalFeatures(); + if (!experimentalFeature.useSpaceAwareness) { + return; + } + const settings = await getSpaceSettings(spaceId); + if (!settings.allowed_namespace_prefixes || settings.allowed_namespace_prefixes.length === 0) { + return; + } + + let valid = false; + for (const allowedNamespacePrefix of settings.allowed_namespace_prefixes) { + if (namespace.startsWith(allowedNamespacePrefix)) { + valid = true; + break; + } + } + + if (!valid) { + throw new PolicyNamespaceValidationError( + `Invalid namespace, supported namespace prefixes: ${settings.allowed_namespace_prefixes.join( + ', ' + )}` + ); + } +} diff --git a/x-pack/plugins/fleet/server/services/spaces/space_settings.test.ts b/x-pack/plugins/fleet/server/services/spaces/space_settings.test.ts new file mode 100644 index 0000000000000..a0db98c84e972 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/spaces/space_settings.test.ts @@ -0,0 +1,104 @@ +/* + * 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 { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks'; +import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; + +import { appContextService } from '../app_context'; + +import { saveSpaceSettings } from './space_settings'; + +jest.mock('../app_context'); + +describe('saveSpaceSettings', () => { + function createSavedsClientMock(settingsAttributes?: any) { + const client = savedObjectsClientMock.create(); + + if (settingsAttributes) { + client.get.mockResolvedValue({ + attributes: settingsAttributes, + } as any); + } else { + client.get.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError('Not found') + ); + } + + jest.mocked(appContextService.getInternalUserSOClientForSpaceId).mockReturnValue(client); + + return client; + } + describe('saved managedBy settings', () => { + it('should work if saved with managedBy:kibana_config and previous settings did not exists', async () => { + const soClient = createSavedsClientMock(); + await saveSpaceSettings({ + spaceId: 'test', + managedBy: 'kibana_config', + settings: { + allowed_namespace_prefixes: ['test'], + managed_by: 'kibana_config', + }, + }); + expect(soClient.update).toBeCalledWith( + 'fleet-space-settings', + 'test-default-settings', + { allowed_namespace_prefixes: ['test'], managed_by: 'kibana_config' }, + expect.anything() + ); + }); + + it('should work if saved with managedBy:kibana_config and previous settings is managedBy:kibana_config', async () => { + const soClient = createSavedsClientMock({ + managed_by: 'kibana_config', + }); + await saveSpaceSettings({ + spaceId: 'test', + managedBy: 'kibana_config', + settings: { + allowed_namespace_prefixes: ['test'], + managed_by: 'kibana_config', + }, + }); + expect(soClient.update).toBeCalledWith( + 'fleet-space-settings', + 'test-default-settings', + { allowed_namespace_prefixes: ['test'], managed_by: 'kibana_config' }, + expect.anything() + ); + }); + + it('should work if saved with managedBy:kibana_config and previous settings is not managedBy:kibana_config', async () => { + const so = createSavedsClientMock({}); + await saveSpaceSettings({ + spaceId: 'test', + managedBy: 'kibana_config', + settings: { + allowed_namespace_prefixes: ['test'], + managed_by: 'kibana_config', + }, + }); + expect(so.update).toBeCalledWith( + 'fleet-space-settings', + 'test-default-settings', + { allowed_namespace_prefixes: ['test'], managed_by: 'kibana_config' }, + expect.anything() + ); + }); + + it('should throw if called without managedBy:kibana_config and previous settings is managedBy:kibana_config', async () => { + createSavedsClientMock({ managed_by: 'kibana_config' }); + await expect( + saveSpaceSettings({ + spaceId: 'test', + settings: { + allowed_namespace_prefixes: ['test'], + }, + }) + ).rejects.toThrowError(/Settings are managed by: kibana_config and should be edited there/); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/spaces/space_settings.ts b/x-pack/plugins/fleet/server/services/spaces/space_settings.ts new file mode 100644 index 0000000000000..ece0291ff4f7c --- /dev/null +++ b/x-pack/plugins/fleet/server/services/spaces/space_settings.ts @@ -0,0 +1,69 @@ +/* + * 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 { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; +import { SavedObjectsErrorHelpers } from '@kbn/core/server'; + +import { SPACE_SETTINGS_ID_SUFFIX } from '../../../common/constants'; +import { appContextService } from '../app_context'; +import { SPACE_SETTINGS_SAVED_OBJECT_TYPE } from '../../constants'; +import type { SpaceSettingsSOAttributes } from '../../types'; +import { FleetUnauthorizedError } from '../..'; + +function _getSavedObjectId(spaceId?: string) { + if (!spaceId || spaceId === DEFAULT_SPACE_ID) { + return `${DEFAULT_SPACE_ID}${SPACE_SETTINGS_ID_SUFFIX}`; + } + + return `${spaceId}${SPACE_SETTINGS_ID_SUFFIX}`; +} + +export async function getSpaceSettings(spaceId?: string) { + const soClient = appContextService.getInternalUserSOClientForSpaceId(spaceId); + + const settings = await soClient + .get(SPACE_SETTINGS_SAVED_OBJECT_TYPE, _getSavedObjectId(spaceId)) + .catch((err) => { + if (SavedObjectsErrorHelpers.isNotFoundError(err)) { + return undefined; + } + throw err; + }); + + return { + allowed_namespace_prefixes: settings?.attributes?.allowed_namespace_prefixes ?? [], + managed_by: settings?.attributes?.managed_by, + }; +} + +export async function saveSpaceSettings({ + settings, + spaceId, + managedBy, +}: { + settings: Partial; + spaceId?: string; + managedBy?: 'kibana_config'; +}) { + const soClient = appContextService.getInternalUserSOClientForSpaceId(spaceId); + + const originalSettings = await getSpaceSettings(spaceId); + if (originalSettings.managed_by && originalSettings.managed_by !== managedBy) { + throw new FleetUnauthorizedError( + `Settings are managed by: ${originalSettings.managed_by} and should be edited there` + ); + } + + await soClient.update( + SPACE_SETTINGS_SAVED_OBJECT_TYPE, + _getSavedObjectId(spaceId), + settings, + { + upsert: settings, + } + ); +} diff --git a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts index 37786ab207249..6216eb4bbd326 100644 --- a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts @@ -195,3 +195,23 @@ export const PreconfiguredAgentPoliciesSchema = schema.arrayOf( defaultValue: [], } ); + +export const PreconfiguredSpaceSettingsSchema = schema.arrayOf( + schema.object({ + space_id: schema.string(), + allowed_namespace_prefixes: schema.nullable( + schema.arrayOf( + schema.string({ + validate: (v) => { + if (v.includes('-')) { + return 'Must not contain -'; + } + }, + }) + ) + ), + }), + { + defaultValue: [], + } +); diff --git a/x-pack/plugins/fleet/server/types/rest_spec/settings.test.ts b/x-pack/plugins/fleet/server/types/rest_spec/settings.test.ts new file mode 100644 index 0000000000000..703f01fe82d37 --- /dev/null +++ b/x-pack/plugins/fleet/server/types/rest_spec/settings.test.ts @@ -0,0 +1,24 @@ +/* + * 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 { PutSpaceSettingsRequestSchema } from './settings'; + +describe('PutSpaceSettingsRequestSchema', () => { + it('should work with valid allowed_namespace_prefixes', () => { + PutSpaceSettingsRequestSchema.body.validate({ + allowed_namespace_prefixes: ['test', 'test2'], + }); + }); + + it('should not accept allowed_namespace_prefixes with -', () => { + expect(() => + PutSpaceSettingsRequestSchema.body.validate({ + allowed_namespace_prefixes: ['test', 'test-'], + }) + ).toThrowErrorMatchingInlineSnapshot(`"[allowed_namespace_prefixes.1]: Must not contain -"`); + }); +}); diff --git a/x-pack/plugins/fleet/server/types/rest_spec/settings.ts b/x-pack/plugins/fleet/server/types/rest_spec/settings.ts index c6eb10e0cc52f..10db7b0f4def7 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/settings.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/settings.ts @@ -39,6 +39,24 @@ export const PutSettingsRequestSchema = { }), }; +export const GetSpaceSettingsRequestSchema = {}; + +export const PutSpaceSettingsRequestSchema = { + body: schema.object({ + allowed_namespace_prefixes: schema.maybe( + schema.arrayOf( + schema.string({ + validate: (v) => { + if (v.includes('-')) { + return 'Must not contain -'; + } + }, + }) + ) + ), + }), +}; + export const GetEnrollmentSettingsRequestSchema = { query: schema.maybe( schema.object({ diff --git a/x-pack/plugins/fleet/server/types/so_attributes.ts b/x-pack/plugins/fleet/server/types/so_attributes.ts index 024718c6f4970..79640a0f90e12 100644 --- a/x-pack/plugins/fleet/server/types/so_attributes.ts +++ b/x-pack/plugins/fleet/server/types/so_attributes.ts @@ -239,6 +239,11 @@ export interface SettingsSOAttributes { output_secret_storage_requirements_met?: boolean; } +export interface SpaceSettingsSOAttributes { + allowed_namespace_prefixes?: string[] | null; + managed_by?: 'kibana_config' | null; +} + export interface DownloadSourceSOAttributes { name: string; host: string; diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts index c14f87447e154..633d34cfa2d15 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts @@ -8,6 +8,7 @@ import { v4 as uuidV4 } from 'uuid'; import type { Agent } from 'supertest'; import { + CreateAgentPolicyRequest, CreateAgentPolicyResponse, GetAgentPoliciesResponse, GetAgentsResponse, @@ -21,6 +22,8 @@ import { PostEnrollmentAPIKeyRequest, GetEnrollmentSettingsResponse, GetInfoResponse, + GetSpaceSettingsResponse, + PutSpaceSettingsRequest, } from '@kbn/fleet-plugin/common/types'; import { GetUninstallTokenResponse, @@ -42,7 +45,10 @@ export class SpaceTestApiClient { return res; } // Agent policies - async createAgentPolicy(spaceId?: string): Promise { + async createAgentPolicy( + spaceId?: string, + data: Partial = {} + ): Promise { const { body: res } = await this.supertest .post(`${this.getBaseUrl(spaceId)}/api/fleet/agent_policies`) .set('kbn-xsrf', 'xxxx') @@ -51,6 +57,7 @@ export class SpaceTestApiClient { description: '', namespace: 'default', inactivity_timeout: 24 * 1000, + ...data, }) .expect(200); @@ -174,6 +181,26 @@ export class SpaceTestApiClient { return res; } + // Space Settings + async getSpaceSettings(spaceId?: string): Promise { + const { body: res } = await this.supertest + .get(`${this.getBaseUrl(spaceId)}/api/fleet/space_settings`) + .expect(200); + + return res; + } + async putSpaceSettings( + data: PutSpaceSettingsRequest['body'], + spaceId?: string + ): Promise { + const { body: res } = await this.supertest + .put(`${this.getBaseUrl(spaceId)}/api/fleet/space_settings`) + .set('kbn-xsrf', 'xxxx') + .send(data) + .expect(200); + + return res; + } // Package install async getPackage( { pkgName, pkgVersion }: { pkgName: string; pkgVersion: string }, diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/index.js b/x-pack/test/fleet_api_integration/apis/space_awareness/index.js index 9733668cd913d..fd9bab0e091f2 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/index.js +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/index.js @@ -13,5 +13,6 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./agents')); loadTestFile(require.resolve('./enrollment_settings')); loadTestFile(require.resolve('./package_install')); + loadTestFile(require.resolve('./space_settings')); }); } diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/space_settings.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/space_settings.ts new file mode 100644 index 0000000000000..29bab0b86b0d0 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/space_settings.ts @@ -0,0 +1,115 @@ +/* + * 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 { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { SpaceTestApiClient } from './api_helper'; +import { setupTestSpaces, TEST_SPACE_1 } from './space_helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + describe('space_settings', async function () { + setupTestSpaces(providerContext); + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.savedObjects.cleanStandardList({ + space: TEST_SPACE_1, + }); + }); + after(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.savedObjects.cleanStandardList({ + space: TEST_SPACE_1, + }); + }); + + const apiClient = new SpaceTestApiClient(supertest); + + describe('In the default space', () => { + it('should allow to set and get space settings', async () => { + const settings = await apiClient.getSpaceSettings(); + expect(settings.item).to.eql({ + allowed_namespace_prefixes: [], + }); + // Update settings + await apiClient.putSpaceSettings({ + allowed_namespace_prefixes: ['test1', 'test2'], + }); + expect((await apiClient.getSpaceSettings()).item).to.eql({ + allowed_namespace_prefixes: ['test1', 'test2'], + }); + // Clear settings + await apiClient.putSpaceSettings({ + allowed_namespace_prefixes: [], + }); + expect((await apiClient.getSpaceSettings()).item).to.eql({ + allowed_namespace_prefixes: [], + }); + }); + }); + + describe('In a specific space', () => { + it('should allow to set and get space settings', async () => { + const settings = await apiClient.getSpaceSettings(TEST_SPACE_1); + expect(settings.item).to.eql({ + allowed_namespace_prefixes: [], + }); + // Update settings + await apiClient.putSpaceSettings( + { + allowed_namespace_prefixes: ['test1', 'test2'], + }, + TEST_SPACE_1 + ); + expect((await apiClient.getSpaceSettings(TEST_SPACE_1)).item).to.eql({ + allowed_namespace_prefixes: ['test1', 'test2'], + }); + // Clear settings + await apiClient.putSpaceSettings( + { + allowed_namespace_prefixes: [], + }, + TEST_SPACE_1 + ); + expect((await apiClient.getSpaceSettings(TEST_SPACE_1)).item).to.eql({ + allowed_namespace_prefixes: [], + }); + }); + + describe('with allowed_namespace_prefixes:["test"]', () => { + before(async () => { + await apiClient.putSpaceSettings( + { + allowed_namespace_prefixes: ['test'], + }, + TEST_SPACE_1 + ); + }); + it('should restrict non authorized agent policy namespace', async () => { + let err: Error | undefined; + try { + await apiClient.createAgentPolicy(TEST_SPACE_1, { + namespace: 'default', + }); + } catch (_err) { + err = _err; + } + + expect(err).to.be.an(Error); + expect(err?.message).to.match(/400 "Bad Request"/); + }); + it('should allow authorized agent policy namespace', async () => { + await apiClient.createAgentPolicy(TEST_SPACE_1, { + namespace: 'test_production', + }); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts b/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts index 25bdff734e379..90e5a73623c14 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts @@ -3746,6 +3746,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:fleet-proxy/delete", "saved_object:fleet-proxy/bulk_delete", "saved_object:fleet-proxy/share_to_space", + "saved_object:fleet-space-settings/bulk_get", + "saved_object:fleet-space-settings/get", + "saved_object:fleet-space-settings/find", + "saved_object:fleet-space-settings/open_point_in_time", + "saved_object:fleet-space-settings/close_point_in_time", + "saved_object:fleet-space-settings/create", + "saved_object:fleet-space-settings/bulk_create", + "saved_object:fleet-space-settings/update", + "saved_object:fleet-space-settings/bulk_update", + "saved_object:fleet-space-settings/delete", + "saved_object:fleet-space-settings/bulk_delete", + "saved_object:fleet-space-settings/share_to_space", "saved_object:telemetry/bulk_get", "saved_object:telemetry/get", "saved_object:telemetry/find", @@ -4061,6 +4073,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:fleet-proxy/delete", "saved_object:fleet-proxy/bulk_delete", "saved_object:fleet-proxy/share_to_space", + "saved_object:fleet-space-settings/bulk_get", + "saved_object:fleet-space-settings/get", + "saved_object:fleet-space-settings/find", + "saved_object:fleet-space-settings/open_point_in_time", + "saved_object:fleet-space-settings/close_point_in_time", + "saved_object:fleet-space-settings/create", + "saved_object:fleet-space-settings/bulk_create", + "saved_object:fleet-space-settings/update", + "saved_object:fleet-space-settings/bulk_update", + "saved_object:fleet-space-settings/delete", + "saved_object:fleet-space-settings/bulk_delete", + "saved_object:fleet-space-settings/share_to_space", "saved_object:telemetry/bulk_get", "saved_object:telemetry/get", "saved_object:telemetry/find", @@ -4312,6 +4336,11 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:fleet-proxy/find", "saved_object:fleet-proxy/open_point_in_time", "saved_object:fleet-proxy/close_point_in_time", + "saved_object:fleet-space-settings/bulk_get", + "saved_object:fleet-space-settings/get", + "saved_object:fleet-space-settings/find", + "saved_object:fleet-space-settings/open_point_in_time", + "saved_object:fleet-space-settings/close_point_in_time", "saved_object:config/bulk_get", "saved_object:config/get", "saved_object:config/find", @@ -4459,6 +4488,11 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:fleet-proxy/find", "saved_object:fleet-proxy/open_point_in_time", "saved_object:fleet-proxy/close_point_in_time", + "saved_object:fleet-space-settings/bulk_get", + "saved_object:fleet-space-settings/get", + "saved_object:fleet-space-settings/find", + "saved_object:fleet-space-settings/open_point_in_time", + "saved_object:fleet-space-settings/close_point_in_time", "saved_object:config/bulk_get", "saved_object:config/get", "saved_object:config/find",