[Fleet] use @kbn/config-schema in Fleet API (Part 1) (#192883)

## Summary

Relates https://github.com/elastic/kibana/issues/184685

Readd https://github.com/elastic/kibana/pull/192447 with fixes and tests
to validate that the response schemas are correct.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Julia Bardi 2024-09-18 15:13:21 +02:00 committed by GitHub
parent 6a79e2d0be
commit 406f07386c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1603 additions and 241 deletions

View file

@ -71,7 +71,7 @@ export interface FullAgentPolicyInputStream {
id: string;
data_stream: {
dataset: string;
type: string;
type?: string;
};
[key: string]: any;
}

View file

@ -90,6 +90,7 @@ export interface NewPackagePolicy {
privileges?: {
cluster?: string[];
};
[key: string]: any;
};
overrides?: { inputs?: { [key: string]: any } } | null;
}

View file

@ -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<PackagePolicyClient>;
@ -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<FleetAuthzRouter>;
let routeHandler: FleetRequestHandler<any, any, any>;
@ -160,6 +176,65 @@ describe('When calling package policy', () => {
context = xpackMocks.createRequestHandlerContext() as unknown as FleetRequestHandlerContext;
(await context.fleet).packagePolicyService.asCurrentUser as jest.Mocked<PackagePolicyClient>;
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<PackagePolicy> = {
page: 1,
perPage: 10,
total: 1,
items: [testPackagePolicy],
};
expect(response.ok).toHaveBeenCalledWith({
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',
},
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',
},
],
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<PackagePolicyClient>
).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: {
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: {} },
},
],
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,
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);
});
});
});

View file

@ -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
);

View file

@ -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' } }
);

View file

@ -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<any>) =>
schema.object({
items: schema.arrayOf(itemSchema),
total: schema.number(),
page: schema.number(),
perPage: schema.number(),
});

View file

@ -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<typeof httpServerMock.createResponseFactory>;
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);
});
});
});
});

View file

@ -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;

View file

@ -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
);

View file

@ -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<typeof httpServerMock.createResponseFactory>;
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);
});
});

View file

@ -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 () => {

View file

@ -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
);

View file

@ -27,7 +27,9 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
.addVersion(
{
version: API_VERSIONS.internal.v1,
validate: { request: PostStandaloneAgentAPIKeyRequestSchema },
validate: {
request: PostStandaloneAgentAPIKeyRequestSchema,
},
},
createStandaloneAgentApiKeyHandler
);

View file

@ -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<TypeOf<typeof GetUninstallTokenRequestSchema.params>>;
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 () => {

View file

@ -84,7 +84,6 @@ export const getUninstallTokenHandler: FleetRequestHandler<
body: { message: `Uninstall Token not found with id ${uninstallTokenId}` },
});
}
const body: GetUninstallTokenResponse = {
item: token,
};

View file

@ -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
);

View file

@ -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'] : [],
};

View file

@ -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()),
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())),
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()),
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.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(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() })),
});

View file

@ -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()),
});

View file

@ -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',
})
)
)
),
})
);

View file

@ -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.',
},
}),
])
),
})
),
});

View file

@ -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(),
}),
});

View file

@ -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 {

View file

@ -30,7 +30,6 @@ Object {
"enabled": true,
"name": "system-1",
"namespace": "default",
"output_id": null,
"package": Object {
"name": "system",
"requires_root": true,

View file

@ -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([]);
});