diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts index e639b364343d9..5fa4085c5106f 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -71,7 +71,7 @@ export interface FullAgentPolicyInputStream { id: string; data_stream: { dataset: string; - type: string; + type?: string; }; [key: string]: any; } diff --git a/x-pack/plugins/fleet/common/types/models/package_policy.ts b/x-pack/plugins/fleet/common/types/models/package_policy.ts index 1e456f3e4d21d..354834d2571dc 100644 --- a/x-pack/plugins/fleet/common/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/package_policy.ts @@ -90,6 +90,7 @@ export interface NewPackagePolicy { privileges?: { cluster?: string[]; }; + [key: string]: any; }; overrides?: { inputs?: { [key: string]: any } } | null; } diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts index fbcf1ee80f206..05876c2bbdf26 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts @@ -9,9 +9,20 @@ import { httpServerMock, httpServiceMock } from '@kbn/core/server/mocks'; import type { KibanaRequest } from '@kbn/core/server'; import type { RouteConfig } from '@kbn/core/server'; +import type { + ListResult, + PostDeletePackagePoliciesResponse, + UpgradePackagePolicyResponse, +} from '../../../common'; + import type { FleetAuthzRouter } from '../../services/security'; import { PACKAGE_POLICY_API_ROUTES } from '../../../common/constants'; +import type { + DryRunPackagePolicy, + UpgradePackagePolicyDryRunResponse, + UpgradePackagePolicyDryRunResponseItem, +} from '../../../common/types'; import { agentPolicyService, appContextService, @@ -21,10 +32,33 @@ import { import { createAppContextStartContractMock, xpackMocks } from '../../mocks'; import type { PackagePolicyClient, FleetRequestHandlerContext } from '../..'; import type { UpdatePackagePolicyRequestSchema } from '../../types/rest_spec'; -import type { AgentPolicy, FleetRequestHandler } from '../../types'; +import { + PackagePolicyResponseSchema, + type AgentPolicy, + type FleetRequestHandler, + BulkGetPackagePoliciesResponseBodySchema, + DeletePackagePoliciesResponseBodySchema, + DeleteOnePackagePolicyResponseSchema, + UpgradePackagePoliciesResponseBodySchema, + DryRunPackagePoliciesResponseBodySchema, + OrphanedPackagePoliciesResponseSchema, + CreatePackagePolicyResponseSchema, +} from '../../types'; import type { PackagePolicy } from '../../types'; -import { getPackagePoliciesHandler } from './handlers'; +import { ListResponseSchema } from '../schema/utils'; + +import { + bulkGetPackagePoliciesHandler, + createPackagePolicyHandler, + deleteOnePackagePolicyHandler, + deletePackagePolicyHandler, + dryRunUpgradePackagePolicyHandler, + getOnePackagePolicyHandler, + getOrphanedPackagePolicies, + getPackagePoliciesHandler, + upgradePackagePolicyHandler, +} from './handlers'; import { registerRoutes } from '.'; const packagePolicyServiceMock = packagePolicyService as jest.Mocked; @@ -79,35 +113,7 @@ jest.mock( delete: jest.fn(), get: jest.fn(), getByIDs: jest.fn(), - list: jest.fn(async (_, __) => { - return { - total: 1, - perPage: 10, - page: 1, - items: [ - { - id: `123`, - name: `Package Policy 123`, - description: '', - created_at: '2022-12-19T20:43:45.879Z', - created_by: 'elastic', - updated_at: '2022-12-19T20:43:45.879Z', - updated_by: 'elastic', - policy_id: `agent-policy-id-a`, - policy_ids: [`agent-policy-id-a`], - enabled: true, - inputs: [], - namespace: 'default', - package: { - name: 'a-package', - title: 'package A', - version: '1.0.0', - }, - revision: 1, - }, - ], - }; - }), + list: jest.fn(), listIds: jest.fn(), update: jest.fn(), // @ts-ignore @@ -131,6 +137,7 @@ jest.mock('../../services/agent_policy', () => { agentPolicyService: { get: jest.fn(), update: jest.fn(), + list: jest.fn(), }, }; }); @@ -140,9 +147,18 @@ jest.mock('../../services/epm/packages', () => { ensureInstalledPackage: jest.fn(() => Promise.resolve()), getPackageInfo: jest.fn(() => Promise.resolve()), getInstallation: jest.fn(), + getInstallations: jest.fn().mockResolvedValue({ + saved_objects: [ + { + attributes: { name: 'a-package', version: '1.0.0' }, + }, + ], + }), }; }); +let testPackagePolicy: PackagePolicy; + describe('When calling package policy', () => { let routerMock: jest.Mocked; let routeHandler: FleetRequestHandler; @@ -160,6 +176,65 @@ describe('When calling package policy', () => { context = xpackMocks.createRequestHandlerContext() as unknown as FleetRequestHandlerContext; (await context.fleet).packagePolicyService.asCurrentUser as jest.Mocked; response = httpServerMock.createResponseFactory(); + testPackagePolicy = { + agents: 100, + created_at: '2022-12-19T20:43:45.879Z', + created_by: 'elastic', + description: '', + enabled: true, + id: '123', + inputs: [ + { + streams: [ + { + id: '1', + compiled_stream: {}, + enabled: true, + keep_enabled: false, + release: 'beta', + vars: { var: { type: 'text', value: 'value', frozen: false } }, + config: { config: { type: 'text', value: 'value', frozen: false } }, + data_stream: { dataset: 'apache.access', type: 'logs', elasticsearch: {} }, + }, + ], + compiled_input: '', + id: '1', + enabled: true, + type: 'logs', + policy_template: '', + keep_enabled: false, + vars: { var: { type: 'text', value: 'value', frozen: false } }, + config: { config: { type: 'text', value: 'value', frozen: false } }, + }, + ], + vars: { var: { type: 'text', value: 'value', frozen: false } }, + name: 'Package Policy 123', + namespace: 'default', + package: { + name: 'a-package', + title: 'package A', + version: '1.0.0', + experimental_data_stream_features: [{ data_stream: 'logs', features: { tsdb: true } }], + requires_root: false, + }, + policy_id: 'agent-policy-id-a', + policy_ids: ['agent-policy-id-a'], + revision: 1, + updated_at: '2022-12-19T20:43:45.879Z', + updated_by: 'elastic', + version: '1.0.0', + secret_references: [ + { + id: 'ref1', + }, + ], + spaceIds: ['space1'], + elasticsearch: { + 'index_template.mappings': { + dynamic_templates: [], + }, + }, + }; }); afterEach(() => { @@ -187,7 +262,14 @@ describe('When calling package policy', () => { }); }; - const existingPolicy = { + const existingPolicy: PackagePolicy = { + id: '1', + revision: 1, + created_at: '', + created_by: '', + updated_at: '', + updated_by: '', + policy_ids: ['2'], name: 'endpoint-1', description: 'desc', policy_id: '2', @@ -231,17 +313,10 @@ describe('When calling package policy', () => { beforeEach(() => { jest.spyOn(licenseService, 'hasAtLeast').mockClear(); packagePolicyServiceMock.update.mockImplementation((soClient, esClient, policyId, newData) => - Promise.resolve(newData as PackagePolicy) + Promise.resolve({ ...existingPolicy, ...newData } as PackagePolicy) ); packagePolicyServiceMock.get.mockResolvedValue({ - id: '1', - revision: 1, - created_at: '', - created_by: '', - updated_at: '', - updated_by: '', ...existingPolicy, - policy_ids: [existingPolicy.policy_id], inputs: [ { ...existingPolicy.inputs[0], @@ -264,6 +339,8 @@ describe('When calling package policy', () => { expect(response.ok).toHaveBeenCalledWith({ body: { item: existingPolicy }, }); + const validationResp = PackagePolicyResponseSchema.validate(existingPolicy); + expect(validationResp).toEqual(existingPolicy); }); it('should use request package policy props if provided by request', async () => { @@ -300,9 +377,13 @@ describe('When calling package policy', () => { }; const request = getUpdateKibanaRequest(newData as any); await routeHandler(context, request, response); + const responseItem = { ...existingPolicy, ...newData }; expect(response.ok).toHaveBeenCalledWith({ - body: { item: newData }, + body: { item: responseItem }, }); + + const validationResp = PackagePolicyResponseSchema.validate(responseItem); + expect(validationResp).toEqual(responseItem); }); it('should override props provided by request only', async () => { @@ -435,43 +516,43 @@ describe('When calling package policy', () => { inputs, } as any); await routeHandler(context, request, response); - expect(response.ok).toHaveBeenCalledWith({ - body: { - item: { - description: 'desc', - enabled: true, - inputs: [ + const responseItem = { + ...existingPolicy, + inputs: [ + { + type: 'input-logs', + enabled: false, + streams: [ { - type: 'input-logs', enabled: false, - streams: [ - { - enabled: false, - data_stream: { - type: 'logs', - dataset: 'test.some_logs', - }, - }, - ], + data_stream: { + type: 'logs', + dataset: 'test.some_logs', + }, }, ], - name: 'endpoint-1', - namespace: 'default', - package: { - name: 'endpoint', - title: 'Elastic Endpoint', - version: '0.5.0', - }, - vars: expect.any(Object), - policy_id: '2', }, + ], + }; + expect(response.ok).toHaveBeenCalledWith({ + body: { + item: responseItem, }, }); + + const validationResp = PackagePolicyResponseSchema.validate(responseItem); + expect(validationResp).toEqual(responseItem); }); }); describe('list api handler', () => { it('should return agent count when `withAgentCount` query param is used', async () => { + packagePolicyServiceMock.list.mockResolvedValue({ + total: 1, + perPage: 10, + page: 1, + items: [testPackagePolicy], + }); const request = httpServerMock.createKibanaRequest({ query: { withAgentCount: true, @@ -510,37 +591,334 @@ describe('When calling package policy', () => { }); await getPackagePoliciesHandler(context, request, response); + const responseBody: ListResult = { + page: 1, + perPage: 10, + total: 1, + items: [testPackagePolicy], + }; + expect(response.ok).toHaveBeenCalledWith({ + body: responseBody, + }); + + const validationResp = ListResponseSchema(PackagePolicyResponseSchema).validate(responseBody); + expect(validationResp).toEqual(responseBody); + }); + }); + + describe('bulk api handler', () => { + it('should return valid response', async () => { + const items: PackagePolicy[] = [testPackagePolicy]; + packagePolicyServiceMock.getByIDs.mockResolvedValue(items); + const request = httpServerMock.createKibanaRequest({ + query: {}, + body: { ids: ['1'] }, + }); + await bulkGetPackagePoliciesHandler(context, request, response); + expect(response.ok).toHaveBeenCalledWith({ + body: { items }, + }); + const validationResp = BulkGetPackagePoliciesResponseBodySchema.validate({ items }); + expect(validationResp).toEqual({ items }); + }); + }); + + describe('orphaned package policies api handler', () => { + it('should return valid response', async () => { + const items: PackagePolicy[] = [testPackagePolicy]; + const expectedResponse = { + items, + total: 1, + }; + packagePolicyServiceMock.list.mockResolvedValue({ + items: [testPackagePolicy], + total: 1, + page: 1, + perPage: 20, + }); + mockedAgentPolicyService.list.mockResolvedValue({ + items: [], + total: 0, + page: 1, + perPage: 20, + }); + await getOrphanedPackagePolicies(context, {} as any, response); + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = OrphanedPackagePoliciesResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); + }); + + describe('get api handler', () => { + it('should return valid response', async () => { + packagePolicyServiceMock.get.mockResolvedValue(testPackagePolicy); + const request = httpServerMock.createKibanaRequest({ + params: { + packagePolicyId: '1', + }, + }); + await getOnePackagePolicyHandler(context, request, response); + expect(response.ok).toHaveBeenCalledWith({ + body: { item: testPackagePolicy }, + }); + const validationResp = PackagePolicyResponseSchema.validate(testPackagePolicy); + expect(validationResp).toEqual(testPackagePolicy); + }); + + it('should return valid response simplified format', async () => { + packagePolicyServiceMock.get.mockResolvedValue(testPackagePolicy); + const request = httpServerMock.createKibanaRequest({ + params: { + packagePolicyId: '1', + }, + query: { + format: 'simplified', + }, + }); + await getOnePackagePolicyHandler(context, request, response); + const simplifiedPackagePolicy = { + ...testPackagePolicy, + inputs: { + logs: { + enabled: true, + streams: { + 'apache.access': { + enabled: true, + vars: { + var: 'value', + }, + }, + }, + vars: { + var: 'value', + }, + }, + }, + vars: { + var: 'value', + }, + }; + expect(response.ok).toHaveBeenCalledWith({ + body: { item: simplifiedPackagePolicy }, + }); + const validationResp = PackagePolicyResponseSchema.validate(simplifiedPackagePolicy); + expect(validationResp).toEqual(simplifiedPackagePolicy); + }); + }); + describe('create api handler', () => { + it('should return valid response', async () => { + packagePolicyServiceMock.get.mockResolvedValue(testPackagePolicy); + ( + (await context.fleet).packagePolicyService.asCurrentUser as jest.Mocked + ).create.mockResolvedValue(testPackagePolicy); + const request = httpServerMock.createKibanaRequest({ + body: testPackagePolicy, + }); + const expectedResponse = { item: testPackagePolicy }; + await createPackagePolicyHandler(context, request, response); expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = CreatePackagePolicyResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); + }); + + describe('bulk delete api handler', () => { + it('should return valid response', async () => { + const responseBody: PostDeletePackagePoliciesResponse = [ + { + id: '1', + name: 'policy', + success: true, + policy_ids: ['1'], + output_id: '1', + package: { + name: 'package', + version: '1.0.0', + title: 'Package', + }, + statusCode: 409, + body: { + message: 'conflict', + }, + }, + ]; + packagePolicyServiceMock.delete.mockResolvedValue(responseBody); + const request = httpServerMock.createKibanaRequest({ body: { - page: 1, - perPage: 10, - total: 1, - items: [ - { - agents: 100, - created_at: '2022-12-19T20:43:45.879Z', - created_by: 'elastic', - description: '', - enabled: true, - id: '123', - inputs: [], - name: 'Package Policy 123', - namespace: 'default', - package: { - name: 'a-package', - title: 'package A', - version: '1.0.0', + packagePolicyIds: ['1'], + }, + }); + await deletePackagePolicyHandler(context, request, response); + expect(response.ok).toHaveBeenCalledWith({ + body: responseBody, + }); + const validationResp = DeletePackagePoliciesResponseBodySchema.validate(responseBody); + expect(validationResp).toEqual(responseBody); + }); + }); + + describe('delete api handler', () => { + it('should return valid response', async () => { + const responseBody = { + id: '1', + }; + packagePolicyServiceMock.delete.mockResolvedValue([ + { + id: '1', + name: 'policy', + success: true, + policy_ids: ['1'], + output_id: '1', + package: { + name: 'package', + version: '1.0.0', + title: 'Package', + }, + statusCode: 409, + body: { + message: 'conflict', + }, + }, + ]); + const request = httpServerMock.createKibanaRequest({ + body: { + force: false, + }, + params: { + packagePolicyId: '1', + }, + }); + await deleteOnePackagePolicyHandler(context, request, response); + expect(response.ok).toHaveBeenCalledWith({ + body: responseBody, + }); + const validationResp = DeleteOnePackagePolicyResponseSchema.validate(responseBody); + expect(validationResp).toEqual(responseBody); + }); + }); + + describe('upgrade api handler', () => { + it('should return valid response', async () => { + const responseBody: UpgradePackagePolicyResponse = [ + { + id: '1', + name: 'policy', + success: true, + statusCode: 200, + body: { + message: 'success', + }, + }, + ]; + packagePolicyServiceMock.upgrade.mockResolvedValue(responseBody); + const request = httpServerMock.createKibanaRequest({ + body: { + packagePolicyIds: ['1'], + }, + }); + await upgradePackagePolicyHandler(context, request, response); + expect(response.ok).toHaveBeenCalledWith({ + body: responseBody, + }); + const validationResp = UpgradePackagePoliciesResponseBodySchema.validate(responseBody); + expect(validationResp).toEqual(responseBody); + }); + }); + + describe('dry run upgrade api handler', () => { + it('should return valid response', async () => { + const dryRunPackagePolicy: DryRunPackagePolicy = { + description: '', + enabled: true, + id: '123', + inputs: [ + { + streams: [ + { + id: '1', + enabled: true, + keep_enabled: false, + release: 'beta', + vars: { var: { type: 'text', value: 'value', frozen: false } }, + config: { config: { type: 'text', value: 'value', frozen: false } }, + data_stream: { dataset: 'apache.access', type: 'logs', elasticsearch: {} }, }, - policy_id: 'agent-policy-id-a', - policy_ids: ['agent-policy-id-a'], + ], + id: '1', + enabled: true, + type: 'logs', + policy_template: '', + keep_enabled: false, + vars: { var: { type: 'text', value: 'value', frozen: false } }, + config: { config: { type: 'text', value: 'value', frozen: false } }, + }, + ], + vars: { var: { type: 'text', value: 'value', frozen: false } }, + name: 'Package Policy 123', + namespace: 'default', + package: { + name: 'a-package', + title: 'package A', + version: '1.0.0', + experimental_data_stream_features: [{ data_stream: 'logs', features: { tsdb: true } }], + requires_root: false, + }, + policy_id: 'agent-policy-id-a', + policy_ids: ['agent-policy-id-a'], + errors: [{ key: 'error', message: 'error' }], + missingVars: ['var'], + }; + const responseItem: UpgradePackagePolicyDryRunResponseItem = { + hasErrors: false, + name: 'policy', + statusCode: 200, + body: { + message: 'success', + }, + diff: [testPackagePolicy, dryRunPackagePolicy], + agent_diff: [ + [ + { + id: '1', + name: 'input', revision: 1, - updated_at: '2022-12-19T20:43:45.879Z', - updated_by: 'elastic', + type: 'logs', + data_stream: { namespace: 'default' }, + use_output: 'default', + package_policy_id: '1', + streams: [ + { + id: 'logfile-log.logs-d46700b2-47f8-4b1a-9153-14a717dc5edf', + data_stream: { + dataset: 'generic', + }, + paths: ['/var/tmp'], + ignore_older: '72h', + }, + ], }, ], + ], + }; + const responseBody: UpgradePackagePolicyDryRunResponse = [responseItem, responseItem]; + packagePolicyServiceMock.getUpgradeDryRunDiff.mockResolvedValueOnce(responseBody[0]); + packagePolicyServiceMock.getUpgradeDryRunDiff.mockResolvedValueOnce(responseBody[1]); + const request = httpServerMock.createKibanaRequest({ + body: { + packagePolicyIds: ['1', '2'], }, }); + await dryRunUpgradePackagePolicyHandler(context, request, response); + expect(response.ok).toHaveBeenCalledWith({ + body: responseBody, + }); + const validationResp = DryRunPackagePoliciesResponseBodySchema.validate(responseBody); + expect(validationResp).toEqual(responseBody); }); }); }); diff --git a/x-pack/plugins/fleet/server/routes/package_policy/index.ts b/x-pack/plugins/fleet/server/routes/package_policy/index.ts index 893eb37a9b1bc..86ac38e658ee3 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/index.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/index.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { schema } from '@kbn/config-schema'; import { getRouteRequiredAuthz } from '../../services/security'; @@ -22,9 +23,21 @@ import { DryRunPackagePoliciesRequestSchema, DeleteOnePackagePolicyRequestSchema, BulkGetPackagePoliciesRequestSchema, + PackagePolicyResponseSchema, + BulkGetPackagePoliciesResponseBodySchema, + DeletePackagePoliciesResponseBodySchema, + DeleteOnePackagePolicyResponseSchema, + UpgradePackagePoliciesResponseBodySchema, + DryRunPackagePoliciesResponseBodySchema, + OrphanedPackagePoliciesResponseSchema, + CreatePackagePolicyResponseSchema, } from '../../types'; import { calculateRouteAuthz } from '../../services/security/security'; +import { genericErrorResponse, notFoundResponse } from '../schema/errors'; + +import { ListResponseSchema } from '../schema/utils'; + import { getPackagePoliciesHandler, getOnePackagePolicyHandler, @@ -48,11 +61,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz, getRouteRequiredAuthz('get', PACKAGE_POLICY_API_ROUTES.LIST_PATTERN) ).granted, + description: 'List package policies', + options: { + tags: ['oas-tag:Fleet package policies'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: GetPackagePoliciesRequestSchema }, + validate: { + request: GetPackagePoliciesRequestSchema, + response: { + 200: { + body: () => ListResponseSchema(PackagePolicyResponseSchema), + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, getPackagePoliciesHandler ); @@ -66,11 +93,28 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz, getRouteRequiredAuthz('post', PACKAGE_POLICY_API_ROUTES.BULK_GET_PATTERN) ).granted, + description: 'Bulk get package policies', + options: { + tags: ['oas-tag:Fleet package policies'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: BulkGetPackagePoliciesRequestSchema }, + validate: { + request: BulkGetPackagePoliciesRequestSchema, + response: { + 200: { + body: () => BulkGetPackagePoliciesResponseBodySchema, + }, + 400: { + body: genericErrorResponse, + }, + 404: { + body: notFoundResponse, + }, + }, + }, }, bulkGetPackagePoliciesHandler ); @@ -84,11 +128,31 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz, getRouteRequiredAuthz('get', PACKAGE_POLICY_API_ROUTES.INFO_PATTERN) ).granted, + description: 'Get package policy by ID', + options: { + tags: ['oas-tag:Fleet package policies'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: GetOnePackagePolicyRequestSchema }, + validate: { + request: GetOnePackagePolicyRequestSchema, + response: { + 200: { + body: () => + schema.object({ + item: PackagePolicyResponseSchema, + }), + }, + 400: { + body: genericErrorResponse, + }, + 404: { + body: notFoundResponse, + }, + }, + }, }, getOnePackagePolicyHandler ); @@ -103,20 +167,48 @@ export const registerRoutes = (router: FleetAuthzRouter) => { .addVersion( { version: API_VERSIONS.public.v1, - validate: {}, + validate: { + request: {}, + response: { + 200: { + body: () => OrphanedPackagePoliciesResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, getOrphanedPackagePolicies ); // Create + // Authz check moved to service here: https://github.com/elastic/kibana/pull/140458 router.versioned .post({ path: PACKAGE_POLICY_API_ROUTES.CREATE_PATTERN, + description: 'Create package policy', + options: { + tags: ['oas-tag:Fleet package policies'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: CreatePackagePolicyRequestSchema }, + validate: { + request: CreatePackagePolicyRequestSchema, + response: { + 200: { + body: () => CreatePackagePolicyResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + 409: { + body: genericErrorResponse, + }, + }, + }, }, createPackagePolicyHandler ); @@ -130,11 +222,31 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz, getRouteRequiredAuthz('put', PACKAGE_POLICY_API_ROUTES.UPDATE_PATTERN) ).granted, + description: 'Update package policy by ID', + options: { + tags: ['oas-tag:Fleet package policies'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: UpdatePackagePolicyRequestSchema }, + validate: { + request: UpdatePackagePolicyRequestSchema, + response: { + 200: { + body: () => + schema.object({ + item: PackagePolicyResponseSchema, + }), + }, + 400: { + body: genericErrorResponse, + }, + 403: { + body: genericErrorResponse, + }, + }, + }, }, updatePackagePolicyHandler @@ -147,11 +259,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { integrations: { writeIntegrationPolicies: true }, }, + description: 'Bulk delete package policies', + options: { + tags: ['oas-tag:Fleet package policies'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: DeletePackagePoliciesRequestSchema }, + validate: { + request: DeletePackagePoliciesRequestSchema, + response: { + 200: { + body: () => DeletePackagePoliciesResponseBodySchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, deletePackagePolicyHandler ); @@ -162,11 +288,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { integrations: { writeIntegrationPolicies: true }, }, + description: 'Delete package policy by ID', + options: { + tags: ['oas-tag:Fleet package policies'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: DeleteOnePackagePolicyRequestSchema }, + validate: { + request: DeleteOnePackagePolicyRequestSchema, + response: { + 200: { + body: () => DeleteOnePackagePolicyResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, deleteOnePackagePolicyHandler ); @@ -178,11 +318,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { integrations: { writeIntegrationPolicies: true }, }, + description: 'Upgrade package policy to a newer package version', + options: { + tags: ['oas-tag:Fleet package policies'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: UpgradePackagePoliciesRequestSchema }, + validate: { + request: UpgradePackagePoliciesRequestSchema, + response: { + 200: { + body: () => UpgradePackagePoliciesResponseBodySchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, upgradePackagePolicyHandler ); @@ -194,11 +348,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { integrations: { readIntegrationPolicies: true }, }, + description: 'Dry run package policy upgrade', + options: { + tags: ['oas-tag:Fleet package policies'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: DryRunPackagePoliciesRequestSchema }, + validate: { + request: DryRunPackagePoliciesRequestSchema, + response: { + 200: { + body: () => DryRunPackagePoliciesResponseBodySchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, dryRunUpgradePackagePolicyHandler ); diff --git a/x-pack/plugins/fleet/server/routes/schema/errors.ts b/x-pack/plugins/fleet/server/routes/schema/errors.ts new file mode 100644 index 0000000000000..1d8f0f5d5b92d --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/schema/errors.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +export const genericErrorResponse = () => + schema.object( + { + statusCode: schema.number(), + error: schema.string(), + message: schema.string(), + }, + { + meta: { description: 'Generic Error' }, + } + ); + +export const notFoundResponse = () => + schema.object({ + message: schema.string(), + }); + +export const internalErrorResponse = () => + schema.object( + { + message: schema.string(), + }, + { meta: { description: 'Internal Server Error' } } + ); diff --git a/x-pack/plugins/fleet/server/routes/schema/utils.ts b/x-pack/plugins/fleet/server/routes/schema/utils.ts new file mode 100644 index 0000000000000..2634b58cdcb34 --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/schema/utils.ts @@ -0,0 +1,16 @@ +/* + * 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 { Type } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; +export const ListResponseSchema = (itemSchema: Type) => + schema.object({ + items: schema.arrayOf(itemSchema), + total: schema.number(), + page: schema.number(), + perPage: schema.number(), + }); diff --git a/x-pack/plugins/fleet/server/routes/settings/enrollment_settings_handler.test.ts b/x-pack/plugins/fleet/server/routes/settings/enrollment_settings_handler.test.ts index 0a39101db8481..029efc146e09b 100644 --- a/x-pack/plugins/fleet/server/routes/settings/enrollment_settings_handler.test.ts +++ b/x-pack/plugins/fleet/server/routes/settings/enrollment_settings_handler.test.ts @@ -4,12 +4,20 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { savedObjectsClientMock } from '@kbn/core/server/mocks'; +import { httpServerMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; import { agentPolicyService } from '../../services'; import { getFleetServerPolicies } from '../../services/fleet_server'; -import { getFleetServerOrAgentPolicies, getDownloadSource } from './enrollment_settings_handler'; +import type { FleetRequestHandlerContext } from '../../types'; +import { GetEnrollmentSettingsResponseSchema } from '../../types'; +import { xpackMocks } from '../../mocks'; + +import { + getFleetServerOrAgentPolicies, + getDownloadSource, + getEnrollmentSettingsHandler, +} from './enrollment_settings_handler'; jest.mock('../../services', () => ({ agentPolicyService: { @@ -42,6 +50,27 @@ jest.mock('../../services', () => ({ jest.mock('../../services/fleet_server', () => ({ getFleetServerPolicies: jest.fn(), + hasFleetServersForPolicies: jest.fn().mockResolvedValue(true), +})); + +jest.mock('../../services/fleet_server_host', () => ({ + getFleetServerHostsForAgentPolicy: jest.fn().mockResolvedValue({ + id: 'host-1', + is_default: true, + is_preconfigured: true, + name: 'Host 1', + host_urls: ['http://localhost:8220'], + proxy_id: 'proxy-1', + }), +})); + +jest.mock('../../services/fleet_proxies', () => ({ + getFleetProxy: jest.fn().mockResolvedValue({ + id: 'proxy-1', + name: 'Proxy 1', + url: 'https://proxy-1/', + is_preconfigured: true, + }), })); describe('EnrollmentSettingsHandler utils', () => { @@ -206,5 +235,74 @@ describe('EnrollmentSettingsHandler utils', () => { proxy_id: 'proxy-1', }); }); + + describe('schema validation', () => { + let context: FleetRequestHandlerContext; + let response: ReturnType; + + beforeEach(() => { + context = xpackMocks.createRequestHandlerContext() as unknown as FleetRequestHandlerContext; + response = httpServerMock.createResponseFactory(); + }); + + it('should return valid enrollment settings', async () => { + const fleetServerPolicies = [ + { + id: 'fs-policy-1', + name: 'FS Policy 1', + is_managed: true, + is_default_fleet_server: true, + has_fleet_server: true, + download_source_id: 'source-2', + fleet_server_host_id: undefined, + }, + ]; + (getFleetServerPolicies as jest.Mock).mockResolvedValueOnce(fleetServerPolicies); + const expectedResponse = { + fleet_server: { + has_active: true, + host_proxy: { + id: 'proxy-1', + name: 'Proxy 1', + is_preconfigured: true, + url: 'https://proxy-1/', + }, + + host: { + host_urls: ['http://localhost:8220'], + id: 'host-1', + is_default: true, + is_preconfigured: true, + name: 'Host 1', + proxy_id: 'proxy-1', + }, + policies: [ + { + download_source_id: 'source-2', + fleet_server_host_id: undefined, + has_fleet_server: true, + id: 'fs-policy-1', + is_default_fleet_server: true, + is_managed: true, + name: 'FS Policy 1', + space_ids: undefined, + }, + ], + }, + download_source: { + host: 'https://source-1/', + id: 'source-1', + is_default: true, + name: 'Source 1', + }, + }; + await getEnrollmentSettingsHandler(context, {} as any, response); + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = GetEnrollmentSettingsResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); + }); }); }); diff --git a/x-pack/plugins/fleet/server/routes/settings/enrollment_settings_handler.ts b/x-pack/plugins/fleet/server/routes/settings/enrollment_settings_handler.ts index 17cb27296d9e4..73a7d03a14592 100644 --- a/x-pack/plugins/fleet/server/routes/settings/enrollment_settings_handler.ts +++ b/x-pack/plugins/fleet/server/routes/settings/enrollment_settings_handler.ts @@ -47,7 +47,6 @@ export const getEnrollmentSettingsHandler: FleetRequestHandler< fleet_server_host_id: undefined, download_source_id: undefined, }; - // Check if there is any active fleet server enrolled into the fleet server policies policies if (fleetServerPolicies) { settingsResponse.fleet_server.policies = fleetServerPolicies; diff --git a/x-pack/plugins/fleet/server/routes/settings/index.ts b/x-pack/plugins/fleet/server/routes/settings/index.ts index b9f672627daa7..b101937e45c27 100644 --- a/x-pack/plugins/fleet/server/routes/settings/index.ts +++ b/x-pack/plugins/fleet/server/routes/settings/index.ts @@ -15,9 +15,14 @@ import { GetEnrollmentSettingsRequestSchema, GetSpaceSettingsRequestSchema, PutSpaceSettingsRequestSchema, + SpaceSettingsResponseSchema, + SettingsResponseSchema, + GetEnrollmentSettingsResponseSchema, } from '../../types'; import type { FleetConfigType } from '../../config'; +import { genericErrorResponse, notFoundResponse } from '../schema/errors'; + import { getEnrollmentSettingsHandler } from './enrollment_settings_handler'; import { @@ -45,7 +50,14 @@ export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: GetSpaceSettingsRequestSchema }, + validate: { + request: GetSpaceSettingsRequestSchema, + response: { + 200: { + body: () => SpaceSettingsResponseSchema, + }, + }, + }, }, getSpaceSettingsHandler ); @@ -61,7 +73,14 @@ export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: PutSpaceSettingsRequestSchema }, + validate: { + request: PutSpaceSettingsRequestSchema, + response: { + 200: { + body: () => SpaceSettingsResponseSchema, + }, + }, + }, }, putSpaceSettingsHandler ); @@ -74,11 +93,27 @@ export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType fleet: { readSettings: true }, }, description: `Get settings`, + options: { + tags: ['oas-tag:Fleet internals'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: GetSettingsRequestSchema }, + validate: { + request: GetSettingsRequestSchema, + response: { + 200: { + body: () => SettingsResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + 404: { + body: notFoundResponse, + }, + }, + }, }, getSettingsHandler ); @@ -89,11 +124,27 @@ export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType fleet: { allSettings: true }, }, description: `Update settings`, + options: { + tags: ['oas-tag:Fleet internals'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: PutSettingsRequestSchema }, + validate: { + request: PutSettingsRequestSchema, + response: { + 200: { + body: () => SettingsResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + 404: { + body: notFoundResponse, + }, + }, + }, }, putSettingsHandler ); @@ -104,11 +155,24 @@ export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType return authz.fleet.addAgents || authz.fleet.addFleetServers; }, description: `Get enrollment settings`, + options: { + tags: ['oas-tag:Fleet internals'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: GetEnrollmentSettingsRequestSchema }, + validate: { + request: GetEnrollmentSettingsRequestSchema, + response: { + 200: { + body: () => GetEnrollmentSettingsResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, getEnrollmentSettingsHandler ); diff --git a/x-pack/plugins/fleet/server/routes/settings/settings_handler.test.ts b/x-pack/plugins/fleet/server/routes/settings/settings_handler.test.ts new file mode 100644 index 0000000000000..0b52c44cde269 --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/settings/settings_handler.test.ts @@ -0,0 +1,85 @@ +/* + * 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 { httpServerMock } from '@kbn/core-http-server-mocks'; + +import { xpackMocks } from '../../mocks'; +import type { FleetRequestHandlerContext } from '../..'; +import { SettingsResponseSchema, SpaceSettingsResponseSchema } from '../../types'; + +import { getSettingsHandler, getSpaceSettingsHandler } from './settings_handler'; + +jest.mock('../../services/spaces/space_settings', () => ({ + getSpaceSettings: jest + .fn() + .mockResolvedValue({ allowed_namespace_prefixes: [], managed_by: 'kibana' }), + saveSpaceSettings: jest.fn(), +})); + +jest.mock('../../services', () => ({ + settingsService: { + getSettings: jest.fn().mockResolvedValue({ + id: '1', + version: '1', + preconfigured_fields: ['fleet_server_hosts'], + secret_storage_requirements_met: true, + output_secret_storage_requirements_met: true, + has_seen_add_data_notice: true, + fleet_server_hosts: ['http://localhost:8220'], + prerelease_integrations_enabled: true, + }), + }, + appContextService: { + getLogger: jest.fn().mockReturnValue({ error: jest.fn() }), + getInternalUserSOClientWithoutSpaceExtension: jest.fn(), + }, + agentPolicyService: { + get: jest.fn(), + getByIDs: jest.fn(), + }, +})); + +describe('SettingsHandler', () => { + let context: FleetRequestHandlerContext; + let response: ReturnType; + + beforeEach(() => { + context = xpackMocks.createRequestHandlerContext() as unknown as FleetRequestHandlerContext; + response = httpServerMock.createResponseFactory(); + }); + + it('should return valid space settings', async () => { + await getSpaceSettingsHandler(context, {} as any, response); + const expectedResponse = { item: { allowed_namespace_prefixes: [], managed_by: 'kibana' } }; + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = SpaceSettingsResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); + + it('should return valid settings', async () => { + await getSettingsHandler(context, {} as any, response); + const expectedResponse = { + item: { + id: '1', + version: '1', + preconfigured_fields: ['fleet_server_hosts'], + secret_storage_requirements_met: true, + output_secret_storage_requirements_met: true, + has_seen_add_data_notice: true, + fleet_server_hosts: ['http://localhost:8220'], + prerelease_integrations_enabled: true, + }, + }; + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = SettingsResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); +}); diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts index 6923d00c18222..79544e7a4e932 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts @@ -25,6 +25,7 @@ import { hasFleetServers } from '../../services/fleet_server'; import { createFleetAuthzMock } from '../../../common/mocks'; import { fleetSetupHandler, getFleetStatusHandler } from './handlers'; +import { FleetSetupResponseSchema, GetAgentsSetupResponseSchema } from '.'; jest.mock('../../services/setup', () => { return { @@ -94,6 +95,8 @@ describe('FleetSetupHandler', () => { }; expect(response.customError).toHaveBeenCalledTimes(0); expect(response.ok).toHaveBeenCalledWith({ body: expectedBody }); + const validationResp = FleetSetupResponseSchema.validate(expectedBody); + expect(validationResp).toEqual(expectedBody); }); it('POST /setup fails w/500 on custom error', async () => { @@ -209,6 +212,8 @@ describe('FleetStatusHandler', () => { }; expect(response.customError).toHaveBeenCalledTimes(0); expect(response.ok).toHaveBeenCalledWith({ body: expectedBody }); + const validationResp = GetAgentsSetupResponseSchema.validate(expectedBody); + expect(validationResp).toEqual(expectedBody); }); it('POST /status w/200 with fleet server standalone', async () => { diff --git a/x-pack/plugins/fleet/server/routes/setup/index.ts b/x-pack/plugins/fleet/server/routes/setup/index.ts index 7052aacfc329d..4b6fd2316832d 100644 --- a/x-pack/plugins/fleet/server/routes/setup/index.ts +++ b/x-pack/plugins/fleet/server/routes/setup/index.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { schema } from '@kbn/config-schema'; import type { FleetAuthzRouter } from '../../services/security'; @@ -12,8 +13,28 @@ import { API_VERSIONS } from '../../../common/constants'; import type { FleetConfigType } from '../../../common/types'; +import { genericErrorResponse, internalErrorResponse } from '../schema/errors'; + import { getFleetStatusHandler, fleetSetupHandler } from './handlers'; +export const FleetSetupResponseSchema = schema.object( + { + isInitialized: schema.boolean(), + nonFatalErrors: schema.arrayOf( + schema.object({ + name: schema.string(), + message: schema.string(), + }) + ), + }, + { + meta: { + description: + "A summary of the result of Fleet's `setup` lifecycle. If `isInitialized` is true, Fleet is ready to accept agent enrollment. `nonFatalErrors` may include useful insight into non-blocking issues with Fleet setup.", + }, + } +); + export const registerFleetSetupRoute = (router: FleetAuthzRouter) => { router.versioned .post({ @@ -22,16 +43,59 @@ export const registerFleetSetupRoute = (router: FleetAuthzRouter) => { fleet: { setup: true }, }, description: `Initiate Fleet setup`, + options: { + tags: ['oas-tag:Fleet internals'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: false, + validate: { + request: {}, + response: { + 200: { + body: () => FleetSetupResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + 500: { + body: internalErrorResponse, + }, + }, + }, }, fleetSetupHandler ); }; +export const GetAgentsSetupResponseSchema = schema.object( + { + isReady: schema.boolean(), + missing_requirements: schema.arrayOf( + schema.oneOf([ + schema.literal('security_required'), + schema.literal('tls_required'), + schema.literal('api_keys'), + schema.literal('fleet_admin_user'), + schema.literal('fleet_server'), + ]) + ), + missing_optional_features: schema.arrayOf( + schema.oneOf([schema.literal('encrypted_saved_object_encryption_key_required')]) + ), + package_verification_key_id: schema.maybe(schema.string()), + is_space_awareness_enabled: schema.maybe(schema.boolean()), + is_secrets_storage_enabled: schema.maybe(schema.boolean()), + }, + { + meta: { + description: + 'A summary of the agent setup status. `isReady` indicates whether the setup is ready. If the setup is not ready, `missing_requirements` lists which requirements are missing.', + }, + } +); + // That route is used by agent to setup Fleet export const registerCreateFleetSetupRoute = (router: FleetAuthzRouter) => { router.versioned @@ -40,11 +104,25 @@ export const registerCreateFleetSetupRoute = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { setup: true }, }, + description: `Initiate agent setup`, + options: { + tags: ['oas-tag:Elastic Agents'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: false, + validate: { + request: {}, + response: { + 200: { + body: () => FleetSetupResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, fleetSetupHandler ); @@ -57,11 +135,25 @@ export const registerGetFleetStatusRoute = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { setup: true }, }, + description: `Get agent setup info`, + options: { + tags: ['oas-tag:Elastic Agents'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: false, + validate: { + request: {}, + response: { + 200: { + body: () => GetAgentsSetupResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, getFleetStatusHandler ); diff --git a/x-pack/plugins/fleet/server/routes/standalone_agent_api_key/index.ts b/x-pack/plugins/fleet/server/routes/standalone_agent_api_key/index.ts index 9255f058aee46..e178140beefa6 100644 --- a/x-pack/plugins/fleet/server/routes/standalone_agent_api_key/index.ts +++ b/x-pack/plugins/fleet/server/routes/standalone_agent_api_key/index.ts @@ -27,7 +27,9 @@ export const registerRoutes = (router: FleetAuthzRouter) => { .addVersion( { version: API_VERSIONS.internal.v1, - validate: { request: PostStandaloneAgentAPIKeyRequestSchema }, + validate: { + request: PostStandaloneAgentAPIKeyRequestSchema, + }, }, createStandaloneAgentApiKeyHandler ); diff --git a/x-pack/plugins/fleet/server/routes/uninstall_token/handlers.test.ts b/x-pack/plugins/fleet/server/routes/uninstall_token/handlers.test.ts index b51f19291c58a..d7432fa7f2f51 100644 --- a/x-pack/plugins/fleet/server/routes/uninstall_token/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/uninstall_token/handlers.test.ts @@ -28,9 +28,11 @@ import type { FleetRequestHandlerContext } from '../..'; import type { MockedFleetAppContext } from '../../mocks'; import { createAppContextStartContractMock, xpackMocks } from '../../mocks'; import { agentPolicyService, appContextService } from '../../services'; -import type { - GetUninstallTokenRequestSchema, - GetUninstallTokensMetadataRequestSchema, +import { + GetUninstallTokensMetadataResponseSchema, + type GetUninstallTokenRequestSchema, + type GetUninstallTokensMetadataRequestSchema, + GetUninstallTokenResponseSchema, } from '../../types/rest_spec/uninstall_token'; import { createAgentPolicyMock } from '../../../common/mocks'; @@ -116,6 +118,10 @@ describe('uninstall token handlers', () => { expect(response.ok).toHaveBeenCalledWith({ body: uninstallTokensResponseFixture, }); + const validateResp = GetUninstallTokensMetadataResponseSchema.validate( + uninstallTokensResponseFixture + ); + expect(validateResp).toEqual(uninstallTokensResponseFixture); }); it('should return internal error when uninstallTokenService throws error', async () => { @@ -131,18 +137,19 @@ describe('uninstall token handlers', () => { }); describe('getUninstallTokenHandler', () => { - const uninstallTokenFixture: UninstallToken = { - id: 'id-1', - policy_id: 'policy-id-1', - policy_name: null, - created_at: '2023-06-15T16:46:48.274Z', - token: '123456789', - }; + let uninstallTokenFixture: UninstallToken; let getTokenMock: jest.Mock; let request: KibanaRequest>; beforeEach(async () => { + uninstallTokenFixture = { + id: 'id-1', + policy_id: 'policy-id-1', + policy_name: null, + created_at: '2023-06-15T16:46:48.274Z', + token: '123456789', + }; const uninstallTokenService = (await context.fleet).uninstallTokenService.asCurrentUser; getTokenMock = uninstallTokenService.getToken as jest.Mock; @@ -165,6 +172,10 @@ describe('uninstall token handlers', () => { item: uninstallTokenFixture, }, }); + const validateResp = GetUninstallTokenResponseSchema.validate({ + item: uninstallTokenFixture, + }); + expect(validateResp).toEqual({ item: uninstallTokenFixture }); }); it('should return internal error when uninstallTokenService throws error', async () => { diff --git a/x-pack/plugins/fleet/server/routes/uninstall_token/handlers.ts b/x-pack/plugins/fleet/server/routes/uninstall_token/handlers.ts index 24d85b8d14250..2eb9a83456845 100644 --- a/x-pack/plugins/fleet/server/routes/uninstall_token/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/uninstall_token/handlers.ts @@ -84,7 +84,6 @@ export const getUninstallTokenHandler: FleetRequestHandler< body: { message: `Uninstall Token not found with id ${uninstallTokenId}` }, }); } - const body: GetUninstallTokenResponse = { item: token, }; diff --git a/x-pack/plugins/fleet/server/routes/uninstall_token/index.ts b/x-pack/plugins/fleet/server/routes/uninstall_token/index.ts index 9fb91b45fa373..a90dd678e99dd 100644 --- a/x-pack/plugins/fleet/server/routes/uninstall_token/index.ts +++ b/x-pack/plugins/fleet/server/routes/uninstall_token/index.ts @@ -4,16 +4,21 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { UNINSTALL_TOKEN_ROUTES, API_VERSIONS } from '../../../common/constants'; import type { FleetConfigType } from '../../config'; import type { FleetAuthzRouter } from '../../services/security'; import { GetUninstallTokenRequestSchema, + GetUninstallTokenResponseSchema, GetUninstallTokensMetadataRequestSchema, + GetUninstallTokensMetadataResponseSchema, } from '../../types/rest_spec/uninstall_token'; import { parseExperimentalConfigValue } from '../../../common/experimental_features'; +import { genericErrorResponse } from '../schema/errors'; + import { getUninstallTokenHandler, getUninstallTokensMetadataHandler } from './handlers'; export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType) => { @@ -26,11 +31,25 @@ export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType fleetAuthz: { fleet: { allAgents: true }, }, + description: 'List metadata for latest uninstall tokens per agent policy', + options: { + tags: ['oas-tag:Fleet uninstall tokens'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: GetUninstallTokensMetadataRequestSchema }, + validate: { + request: GetUninstallTokensMetadataRequestSchema, + response: { + 200: { + body: () => GetUninstallTokensMetadataResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, getUninstallTokensMetadataHandler ); @@ -41,11 +60,25 @@ export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType fleetAuthz: { fleet: { allAgents: true }, }, + description: 'Get one decrypted uninstall token by its ID', + options: { + tags: ['oas-tag:Fleet uninstall tokens'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: GetUninstallTokenRequestSchema }, + validate: { + request: GetUninstallTokenRequestSchema, + response: { + 200: { + body: () => GetUninstallTokenResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, getUninstallTokenHandler ); diff --git a/x-pack/plugins/fleet/server/services/settings.ts b/x-pack/plugins/fleet/server/services/settings.ts index 68829b734eeaf..394a2365f3c03 100644 --- a/x-pack/plugins/fleet/server/services/settings.ts +++ b/x-pack/plugins/fleet/server/services/settings.ts @@ -36,7 +36,15 @@ export async function getSettings(soClient: SavedObjectsClientContract): Promise return { id: settingsSo.id, version: settingsSo.version, - ...settingsSo.attributes, + secret_storage_requirements_met: settingsSo.attributes.secret_storage_requirements_met, + output_secret_storage_requirements_met: + settingsSo.attributes.output_secret_storage_requirements_met, + has_seen_add_data_notice: settingsSo.attributes.has_seen_add_data_notice, + prerelease_integrations_enabled: settingsSo.attributes.prerelease_integrations_enabled, + use_space_awareness_migration_status: + settingsSo.attributes.use_space_awareness_migration_status, + use_space_awareness_migration_started_at: + settingsSo.attributes.use_space_awareness_migration_started_at, fleet_server_hosts: fleetServerHosts.items.flatMap((item) => item.host_urls), preconfigured_fields: getConfigFleetServerHosts() ? ['fleet_server_hosts'] : [], }; diff --git a/x-pack/plugins/fleet/server/types/models/package_policy.ts b/x-pack/plugins/fleet/server/types/models/package_policy.ts index e105bc82b27db..68d3a089c2342 100644 --- a/x-pack/plugins/fleet/server/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/package_policy.ts @@ -16,15 +16,24 @@ export const PackagePolicyNamespaceSchema = schema.string({ return namespaceValidation.error; } }, + meta: { + description: + "The package policy namespace. Leave blank to inherit the agent policy's namespace.", + }, }); -const ConfigRecordSchema = schema.recordOf( +export const ConfigRecordSchema = schema.recordOf( schema.string(), schema.object({ type: schema.maybe(schema.string()), value: schema.maybe(schema.any()), frozen: schema.maybe(schema.boolean()), - }) + }), + { + meta: { + description: 'Package variable (see integration documentation for more information)', + }, + } ); const PackagePolicyStreamsSchema = { @@ -50,33 +59,18 @@ const PackagePolicyStreamsSchema = { ), }), vars: schema.maybe(ConfigRecordSchema), - config: schema.maybe( - schema.recordOf( - schema.string(), - schema.object({ - type: schema.maybe(schema.string()), - value: schema.maybe(schema.any()), - }) - ) - ), + config: schema.maybe(ConfigRecordSchema), compiled_stream: schema.maybe(schema.any()), }; -const PackagePolicyInputsSchema = { +export const PackagePolicyInputsSchema = { + id: schema.maybe(schema.string()), type: schema.string(), policy_template: schema.maybe(schema.string()), enabled: schema.boolean(), keep_enabled: schema.maybe(schema.boolean()), vars: schema.maybe(ConfigRecordSchema), - config: schema.maybe( - schema.recordOf( - schema.string(), - schema.object({ - type: schema.maybe(schema.string()), - value: schema.maybe(schema.any()), - }) - ) - ), + config: schema.maybe(ConfigRecordSchema), streams: schema.arrayOf(schema.object(PackagePolicyStreamsSchema)), }; @@ -92,44 +86,91 @@ const ExperimentalDataStreamFeatures = schema.arrayOf( }) ); -const PackagePolicyBaseSchema = { - name: schema.string(), - description: schema.maybe(schema.string()), +export const PackagePolicyPackageSchema = schema.object({ + name: schema.string({ + meta: { + description: 'Package name', + }, + }), + title: schema.maybe(schema.string()), + version: schema.string({ + meta: { + description: 'Package version', + }, + }), + experimental_data_stream_features: schema.maybe(ExperimentalDataStreamFeatures), + requires_root: schema.maybe(schema.boolean()), +}); + +export const PackagePolicyBaseSchema = { + name: schema.string({ + meta: { + description: 'Package policy name (should be unique)', + }, + }), + description: schema.maybe( + schema.string({ + meta: { + description: 'Package policy description', + }, + }) + ), namespace: schema.maybe(PackagePolicyNamespaceSchema), - policy_id: schema.nullable(schema.maybe(schema.string())), - policy_ids: schema.maybe(schema.arrayOf(schema.string())), - output_id: schema.nullable(schema.maybe(schema.string())), + policy_id: schema.maybe( + schema.oneOf([ + schema.literal(null), + schema.string({ + meta: { + description: 'Agent policy ID where that package policy will be added', + deprecated: true, + }, + }), + ]) + ), + policy_ids: schema.maybe( + schema.arrayOf( + schema.string({ + meta: { + description: 'Agent policy IDs where that package policy will be added', + }, + }) + ) + ), + output_id: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), enabled: schema.boolean(), is_managed: schema.maybe(schema.boolean()), - package: schema.maybe( - schema.object({ - name: schema.string(), - title: schema.string(), - version: schema.string(), - experimental_data_stream_features: schema.maybe(ExperimentalDataStreamFeatures), - requires_root: schema.maybe(schema.boolean()), - }) - ), + package: schema.maybe(PackagePolicyPackageSchema), + inputs: schema.arrayOf(schema.object(PackagePolicyInputsSchema)), vars: schema.maybe(ConfigRecordSchema), overrides: schema.maybe( - schema.nullable( - schema.object({ - inputs: schema.maybe( - schema.recordOf(schema.string(), schema.any(), { - validate: (val) => { - if ( - Object.keys(val).some( - (key) => key.match(/^compiled_inputs(\.)?/) || key.match(/^compiled_stream(\.)?/) - ) - ) { - return 'Overrides of compiled_inputs and compiled_stream are not allowed'; - } - }, - }) - ), - }) - ) + schema.oneOf([ + schema.literal(null), + schema.object( + { + inputs: schema.maybe( + schema.recordOf(schema.string(), schema.any(), { + validate: (val) => { + if ( + Object.keys(val).some( + (key) => + key.match(/^compiled_inputs(\.)?/) || key.match(/^compiled_stream(\.)?/) + ) + ) { + return 'Overrides of compiled_inputs and compiled_stream are not allowed'; + } + }, + }) + ), + }, + { + meta: { + description: + 'Override settings that are defined in the package policy. The override option should be used only in unusual circumstances and not as a routine procedure.', + }, + } + ), + ]) ), }; @@ -142,15 +183,7 @@ export const NewPackagePolicySchema = schema.object({ const CreatePackagePolicyProps = { ...PackagePolicyBaseSchema, enabled: schema.maybe(schema.boolean()), - package: schema.maybe( - schema.object({ - name: schema.string(), - title: schema.maybe(schema.string()), - version: schema.string(), - experimental_data_stream_features: schema.maybe(ExperimentalDataStreamFeatures), - requires_root: schema.maybe(schema.boolean()), - }) - ), + package: schema.maybe(PackagePolicyPackageSchema), inputs: schema.arrayOf( schema.object({ ...PackagePolicyInputsSchema, @@ -161,11 +194,24 @@ const CreatePackagePolicyProps = { export const CreatePackagePolicyRequestBodySchema = schema.object({ ...CreatePackagePolicyProps, - id: schema.maybe(schema.string()), - force: schema.maybe(schema.boolean()), + id: schema.maybe( + schema.string({ + meta: { + description: 'Package policy unique identifier', + }, + }) + ), + force: schema.maybe( + schema.boolean({ + meta: { + description: + 'Force package policy creation even if package is not verified, or if the agent policy is managed.', + }, + }) + ), }); -const SimplifiedVarsSchema = schema.recordOf( +export const SimplifiedVarsSchema = schema.recordOf( schema.string(), schema.nullable( schema.oneOf([ @@ -180,6 +226,55 @@ const SimplifiedVarsSchema = schema.recordOf( isSecretRef: schema.boolean(), }), ]) + ), + { + meta: { + description: + 'Input/stream level variable (see integration documentation for more information)', + }, + } +); + +export const SimplifiedPackagePolicyInputsSchema = schema.maybe( + schema.recordOf( + schema.string(), + schema.object({ + enabled: schema.maybe( + schema.boolean({ + meta: { + description: 'enable or disable that input, (default to true)', + }, + }) + ), + vars: schema.maybe(SimplifiedVarsSchema), + streams: schema.maybe( + schema.recordOf( + schema.string(), + schema.object({ + enabled: schema.maybe( + schema.boolean({ + meta: { + description: 'enable or disable that stream, (default to true)', + }, + }) + ), + vars: schema.maybe(SimplifiedVarsSchema), + }), + { + meta: { + description: + 'Input streams (see integration documentation to know what streams are available)', + }, + } + ) + ), + }), + { + meta: { + description: + 'Package policy inputs (see integration documentation to know what inputs are available)', + }, + } ) ); @@ -188,26 +283,9 @@ export const SimplifiedPackagePolicyBaseSchema = schema.object({ name: schema.string(), description: schema.maybe(schema.string()), namespace: schema.maybe(schema.string()), - output_id: schema.nullable(schema.maybe(schema.string())), + output_id: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), vars: schema.maybe(SimplifiedVarsSchema), - inputs: schema.maybe( - schema.recordOf( - schema.string(), - schema.object({ - enabled: schema.maybe(schema.boolean()), - vars: schema.maybe(SimplifiedVarsSchema), - streams: schema.maybe( - schema.recordOf( - schema.string(), - schema.object({ - enabled: schema.maybe(schema.boolean()), - vars: schema.maybe(SimplifiedVarsSchema), - }) - ) - ), - }) - ) - ), + inputs: SimplifiedPackagePolicyInputsSchema, }); export const SimplifiedPackagePolicyPreconfiguredSchema = SimplifiedPackagePolicyBaseSchema.extends( @@ -221,15 +299,10 @@ export const SimplifiedPackagePolicyPreconfiguredSchema = SimplifiedPackagePolic export const SimplifiedCreatePackagePolicyRequestBodySchema = SimplifiedPackagePolicyBaseSchema.extends({ - policy_id: schema.nullable(schema.maybe(schema.string())), + policy_id: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), policy_ids: schema.maybe(schema.arrayOf(schema.string())), force: schema.maybe(schema.boolean()), - package: schema.object({ - name: schema.string(), - version: schema.string(), - experimental_data_stream_features: schema.maybe(ExperimentalDataStreamFeatures), - requires_root: schema.maybe(schema.boolean()), - }), + package: PackagePolicyPackageSchema, }); export const UpdatePackagePolicyRequestBodySchema = schema.object({ @@ -261,15 +334,19 @@ export const PackagePolicySchema = schema.object({ updated_by: schema.string(), created_at: schema.string(), created_by: schema.string(), - elasticsearch: schema.maybe( - schema.object({ - privileges: schema.maybe( - schema.object({ - cluster: schema.maybe(schema.arrayOf(schema.string())), - }) - ), - }) - ), + elasticsearch: schema + .maybe( + schema.object({ + privileges: schema.maybe( + schema.object({ + cluster: schema.maybe(schema.arrayOf(schema.string())), + }) + ), + }) + ) + .extendsDeep({ + unknowns: 'allow', + }), inputs: schema.arrayOf( schema.object({ ...PackagePolicyInputsSchema, @@ -284,3 +361,46 @@ export const PackagePolicySchema = schema.object({ ) ), }); + +export const PackagePolicyResponseSchema = PackagePolicySchema.extends({ + vars: schema.maybe(schema.oneOf([ConfigRecordSchema, schema.maybe(SimplifiedVarsSchema)])), + inputs: schema.oneOf([ + schema.arrayOf( + schema.object({ + ...PackagePolicyInputsSchema, + compiled_input: schema.maybe(schema.any()), + }) + ), + SimplifiedPackagePolicyInputsSchema, + ]), + spaceIds: schema.maybe(schema.arrayOf(schema.string())), + agents: schema.maybe(schema.number()), +}); + +export const OrphanedPackagePoliciesResponseSchema = schema.object({ + items: schema.arrayOf(PackagePolicyResponseSchema), + total: schema.number(), +}); + +export const DryRunPackagePolicySchema = schema.object({ + ...PackagePolicyBaseSchema, + id: schema.maybe(schema.string()), + force: schema.maybe(schema.boolean()), + errors: schema.maybe( + schema.arrayOf( + schema.object({ + message: schema.string(), + key: schema.maybe(schema.string()), + }) + ) + ), + missingVars: schema.maybe(schema.arrayOf(schema.string())), +}); + +export const PackagePolicyStatusResponseSchema = schema.object({ + id: schema.string(), + success: schema.boolean(), + name: schema.maybe(schema.string()), + statusCode: schema.maybe(schema.number()), + body: schema.maybe(schema.object({ message: schema.string() })), +}); diff --git a/x-pack/plugins/fleet/server/types/rest_spec/common.ts b/x-pack/plugins/fleet/server/types/rest_spec/common.ts index 0c5f16ff87f90..2be083d677dd3 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/common.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/common.ts @@ -23,7 +23,10 @@ export const ListWithKuerySchema = schema.object({ }); export const BulkRequestBodySchema = schema.object({ - ids: schema.arrayOf(schema.string(), { minSize: 1 }), + ids: schema.arrayOf(schema.string(), { + minSize: 1, + meta: { description: 'list of package policy ids' }, + }), ignoreMissing: schema.maybe(schema.boolean()), }); diff --git a/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts b/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts index 88b4452a5fe7a..1cf332efba843 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts @@ -9,6 +9,10 @@ import { schema } from '@kbn/config-schema'; import { CreatePackagePolicyRequestBodySchema, + DryRunPackagePolicySchema, + PackagePolicyPackageSchema, + PackagePolicyResponseSchema, + PackagePolicyStatusResponseSchema, SimplifiedCreatePackagePolicyRequestBodySchema, UpdatePackagePolicyRequestBodySchema, } from '../models'; @@ -59,6 +63,10 @@ export const BulkGetPackagePoliciesRequestSchema = { }), }; +export const BulkGetPackagePoliciesResponseBodySchema = schema.object({ + items: schema.arrayOf(PackagePolicyResponseSchema), +}); + export const GetOnePackagePolicyRequestSchema = { params: schema.object({ packagePolicyId: schema.string(), @@ -71,10 +79,14 @@ export const GetOnePackagePolicyRequestSchema = { }; export const CreatePackagePolicyRequestSchema = { - body: schema.oneOf([ - CreatePackagePolicyRequestBodySchema, - SimplifiedCreatePackagePolicyRequestBodySchema, - ]), + body: schema.oneOf( + [CreatePackagePolicyRequestBodySchema, SimplifiedCreatePackagePolicyRequestBodySchema], + { + meta: { + description: 'You should use inputs as an object and not use the deprecated inputs array.', + }, + } + ), query: schema.object({ format: schema.maybe( schema.oneOf([schema.literal(inputsFormat.Simplified), schema.literal(inputsFormat.Legacy)]) @@ -82,6 +94,10 @@ export const CreatePackagePolicyRequestSchema = { }), }; +export const CreatePackagePolicyResponseSchema = schema.object({ + item: PackagePolicyResponseSchema, +}); + export const UpdatePackagePolicyRequestSchema = { ...GetOnePackagePolicyRequestSchema, body: schema.oneOf([ @@ -102,6 +118,25 @@ export const DeletePackagePoliciesRequestSchema = { }), }; +export const DeletePackagePoliciesResponseBodySchema = schema.arrayOf( + PackagePolicyStatusResponseSchema.extends({ + policy_id: schema.maybe( + schema.oneOf([ + schema.literal(null), + schema.string({ + meta: { + description: 'Use `policy_ids` instead', + deprecated: true, + }, + }), + ]) + ), + policy_ids: schema.arrayOf(schema.string()), + output_id: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + package: PackagePolicyPackageSchema, + }) +); + export const DeleteOnePackagePolicyRequestSchema = { params: schema.object({ packagePolicyId: schema.string(), @@ -111,15 +146,104 @@ export const DeleteOnePackagePolicyRequestSchema = { }), }; +export const DeleteOnePackagePolicyResponseSchema = schema.object({ + id: schema.string(), +}); + export const UpgradePackagePoliciesRequestSchema = { body: schema.object({ packagePolicyIds: schema.arrayOf(schema.string()), }), }; +export const UpgradePackagePoliciesResponseBodySchema = schema.arrayOf( + PackagePolicyStatusResponseSchema +); + export const DryRunPackagePoliciesRequestSchema = { body: schema.object({ packagePolicyIds: schema.arrayOf(schema.string()), packageVersion: schema.maybe(schema.string()), }), }; + +export const DryRunPackagePoliciesResponseBodySchema = schema.arrayOf( + schema.object({ + name: schema.maybe(schema.string()), + statusCode: schema.maybe(schema.number()), + body: schema.maybe(schema.object({ message: schema.string() })), + hasErrors: schema.boolean(), + diff: schema.maybe( + schema.arrayOf( + schema.oneOf([ + PackagePolicyResponseSchema.extends({ + id: schema.maybe(schema.string()), + }), + DryRunPackagePolicySchema, + ]) + ) + ), + agent_diff: schema.maybe( + schema.arrayOf( + schema.arrayOf( + schema + .object({ + id: schema.string(), + name: schema.string(), + revision: schema.number(), + type: schema.string(), + data_stream: schema.object({ + namespace: schema.string(), + }), + use_output: schema.string(), + package_policy_id: schema.string(), + meta: schema.maybe( + schema.object({ + package: schema + .object({ + name: schema.string(), + version: schema.string(), + }) + .extendsDeep({ + // equivalent of allowing extra keys like `[key: string]: any;` + unknowns: 'allow', + }), + }) + ), + streams: schema.maybe( + schema.arrayOf( + schema + .object({ + id: schema.string(), + data_stream: schema.object({ + dataset: schema.string(), + type: schema.maybe(schema.string()), + }), + }) + .extendsDeep({ + unknowns: 'allow', + }) + ) + ), + processors: schema.maybe( + schema.arrayOf( + schema.object({ + add_fields: schema.object({ + target: schema.string(), + fields: schema.recordOf( + schema.string(), + schema.oneOf([schema.string(), schema.number()]) + ), + }), + }) + ) + ), + }) + .extendsDeep({ + unknowns: 'allow', + }) + ) + ) + ), + }) +); 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 10db7b0f4def7..6553d2e976bed 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/settings.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/settings.ts @@ -41,6 +41,30 @@ export const PutSettingsRequestSchema = { export const GetSpaceSettingsRequestSchema = {}; +export const SpaceSettingsResponseSchema = schema.object({ + item: schema.object({ + managed_by: schema.maybe(schema.string()), + allowed_namespace_prefixes: schema.arrayOf(schema.string()), + }), +}); + +export const SettingsResponseSchema = schema.object({ + item: schema.object({ + has_seen_add_data_notice: schema.maybe(schema.boolean()), + fleet_server_hosts: schema.maybe(schema.arrayOf(schema.string())), + prerelease_integrations_enabled: schema.boolean(), + id: schema.string(), + version: schema.maybe(schema.string()), + preconfigured_fields: schema.maybe(schema.arrayOf(schema.literal('fleet_server_hosts'))), + secret_storage_requirements_met: schema.maybe(schema.boolean()), + output_secret_storage_requirements_met: schema.maybe(schema.boolean()), + use_space_awareness_migration_status: schema.maybe( + schema.oneOf([schema.literal('pending'), schema.literal('success'), schema.literal('error')]) + ), + use_space_awareness_migration_started_at: schema.maybe(schema.string()), + }), +}); + export const PutSpaceSettingsRequestSchema = { body: schema.object({ allowed_namespace_prefixes: schema.maybe( @@ -64,3 +88,70 @@ export const GetEnrollmentSettingsRequestSchema = { }) ), }; + +export const GetEnrollmentSettingsResponseSchema = schema.object({ + fleet_server: schema.object({ + policies: schema.arrayOf( + schema.object({ + id: schema.string(), + name: schema.string(), + is_managed: schema.boolean(), + is_default_fleet_server: schema.maybe(schema.boolean()), + has_fleet_server: schema.maybe(schema.boolean()), + fleet_server_host_id: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + download_source_id: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + space_ids: schema.maybe(schema.arrayOf(schema.string())), + }) + ), + has_active: schema.boolean(), + host: schema.maybe( + schema.object({ + id: schema.string(), + name: schema.string(), + host_urls: schema.arrayOf(schema.string()), + is_default: schema.boolean(), + is_preconfigured: schema.boolean(), + is_internal: schema.maybe(schema.boolean()), + proxy_id: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + }) + ), + host_proxy: schema.maybe( + schema.object({ + id: schema.string(), + proxy_headers: schema.maybe( + schema.recordOf( + schema.string(), + schema.oneOf([schema.string(), schema.number(), schema.boolean()]) + ) + ), + name: schema.string(), + url: schema.string(), + certificate_authorities: schema.maybe( + schema.oneOf([schema.literal(null), schema.string()]) + ), + certificate: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + certificate_key: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + is_preconfigured: schema.boolean(), + }) + ), + }), + download_source: schema.maybe( + schema.object({ + id: schema.string(), + name: schema.string(), + host: schema.string(), + is_default: schema.boolean(), + proxy_id: schema.maybe( + schema.oneOf([ + schema.literal(null), + schema.string({ + meta: { + description: + 'The ID of the proxy to use for this download source. See the proxies API for more information.', + }, + }), + ]) + ), + }) + ), +}); diff --git a/x-pack/plugins/fleet/server/types/rest_spec/uninstall_token.ts b/x-pack/plugins/fleet/server/types/rest_spec/uninstall_token.ts index 924e1da2cb9e8..7915e5bbc8b07 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/uninstall_token.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/uninstall_token.ts @@ -6,17 +6,48 @@ */ import { schema } from '@kbn/config-schema'; +import { ListResponseSchema } from '../../routes/schema/utils'; + export const GetUninstallTokensMetadataRequestSchema = { query: schema.object({ - policyId: schema.maybe(schema.string({ maxLength: 50 })), + policyId: schema.maybe( + schema.string({ + maxLength: 50, + meta: { description: 'Partial match filtering for policy IDs' }, + }) + ), search: schema.maybe(schema.string({ maxLength: 50 })), - perPage: schema.maybe(schema.number({ defaultValue: 20, min: 5 })), + perPage: schema.maybe( + schema.number({ + defaultValue: 20, + min: 5, + meta: { description: 'The number of items to return' }, + }) + ), page: schema.maybe(schema.number({ defaultValue: 1, min: 1 })), }), }; +const UninstallTokenMetadataSchema = schema.object({ + id: schema.string(), + policy_id: schema.string(), + policy_name: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + created_at: schema.string(), + namespaces: schema.maybe(schema.arrayOf(schema.string())), +}); + +export const GetUninstallTokensMetadataResponseSchema = ListResponseSchema( + UninstallTokenMetadataSchema +); + export const GetUninstallTokenRequestSchema = { params: schema.object({ uninstallTokenId: schema.string(), }), }; + +export const GetUninstallTokenResponseSchema = schema.object({ + item: UninstallTokenMetadataSchema.extends({ + token: schema.string(), + }), +}); diff --git a/x-pack/plugins/fleet/server/types/so_attributes.ts b/x-pack/plugins/fleet/server/types/so_attributes.ts index 1bb954cf990d2..9be09fe4ee554 100644 --- a/x-pack/plugins/fleet/server/types/so_attributes.ts +++ b/x-pack/plugins/fleet/server/types/so_attributes.ts @@ -239,6 +239,8 @@ export interface SettingsSOAttributes { fleet_server_hosts?: string[]; secret_storage_requirements_met?: boolean; output_secret_storage_requirements_met?: boolean; + use_space_awareness_migration_status?: 'pending' | 'success' | 'error'; + use_space_awareness_migration_started_at?: string | null; } export interface SpaceSettingsSOAttributes { diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/__snapshots__/agent_policy.snap b/x-pack/test/fleet_api_integration/apis/agent_policy/__snapshots__/agent_policy.snap index a4d255613133e..9112ad20ad860 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/__snapshots__/agent_policy.snap +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/__snapshots__/agent_policy.snap @@ -30,7 +30,6 @@ Object { "enabled": true, "name": "system-1", "namespace": "default", - "output_id": null, "package": Object { "name": "system", "requires_root": true, diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts index 159c40926f4e7..ea50aaaf53eb8 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts @@ -198,7 +198,7 @@ export default function (providerContext: FtrProviderContext) { }, }) .expect(200); - expect(response.body.item.policy_id).to.eql(null); + expect(response.body.item.policy_id).to.eql(undefined); expect(response.body.item.policy_ids).to.eql([]); });