Skip to content

Commit

Permalink
Merge branch 'main' into max-con-reporting
Browse files Browse the repository at this point in the history
  • Loading branch information
rshen91 committed Nov 16, 2023
2 parents 9950af0 + 2263213 commit f4c5668
Show file tree
Hide file tree
Showing 238 changed files with 4,890 additions and 2,813 deletions.
3 changes: 2 additions & 1 deletion .buildkite/ftr_configs.yml
Expand Up @@ -225,7 +225,6 @@ enabled:
- x-pack/test/dataset_quality_api_integration/basic/config.ts
- x-pack/test/detection_engine_api_integration/basic/config.ts
- x-pack/test/detection_engine_api_integration/security_and_spaces/group1/config.ts
- x-pack/test/detection_engine_api_integration/security_and_spaces/group4/config.ts
- x-pack/test/detection_engine_api_integration/security_and_spaces/group10/config.ts
- x-pack/test/disable_ems/config.ts
- x-pack/test/encrypted_saved_objects_api_integration/config.ts
Expand Down Expand Up @@ -477,3 +476,5 @@ enabled:
- x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_execution_logic/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/user_roles/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/user_roles/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/telemetry/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/telemetry/configs/ess.config.ts
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -1410,7 +1410,7 @@ x-pack/test/security_solution_api_integration/test_suites/detections_response/de
/x-pack/plugins/security_solution/server/routes @elastic/security-detections-response @elastic/security-threat-hunting
/x-pack/plugins/security_solution/server/utils @elastic/security-detections-response @elastic/security-threat-hunting
x-pack/test/security_solution_api_integration/test_suites/detections_response/utils @elastic/security-detections-response

x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/telemetry @elastic/security-detections-response

## Security Solution sub teams - security-defend-workflows
/x-pack/plugins/security_solution/public/management/ @elastic/security-defend-workflows
Expand Down
6 changes: 5 additions & 1 deletion config/serverless.yml
Expand Up @@ -150,10 +150,14 @@ xpack.reporting.queue.pollInterval: 3m
xpack.reporting.roles.enabled: false
xpack.reporting.statefulSettings.enabled: false


# Disabled Observability plugins
xpack.ux.enabled: false
xpack.monitoring.enabled: false
xpack.uptime.enabled: false
xpack.legacy_uptime.enabled: false
monitoring.ui.enabled: false

## Enable uiSettings validations
xpack.securitySolution.enableUiSettingsValidations: true
data.enableUiSettingsValidations: true
discover.enableUiSettingsValidations: true

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Expand Up @@ -345,3 +345,45 @@ describe('#stop', () => {
await batchSetPromise;
});
});

describe('#validate', () => {
it('sends a validation request', async () => {
fetchMock.mock('*', {
body: { errorMessage: 'Test validation error message.' },
});

const { uiSettingsApi } = setup();
await uiSettingsApi.validate('foo', 'bar');
expect(fetchMock.calls()).toMatchSnapshot('validation request');
});

it('rejects on 404 response', async () => {
fetchMock.mock('*', {
status: 404,
body: 'not found',
});

const { uiSettingsApi } = setup();
await expect(uiSettingsApi.validate('foo', 'bar')).rejects.toThrowErrorMatchingSnapshot();
});

it('rejects on 301', async () => {
fetchMock.mock('*', {
status: 301,
body: 'redirect',
});

const { uiSettingsApi } = setup();
await expect(uiSettingsApi.validate('foo', 'bar')).rejects.toThrowErrorMatchingSnapshot();
});

it('rejects on 500', async () => {
fetchMock.mock('*', {
status: 500,
body: 'redirect',
});

const { uiSettingsApi } = setup();
await expect(uiSettingsApi.validate('foo', 'bar')).rejects.toThrowErrorMatchingSnapshot();
});
});
Expand Up @@ -16,6 +16,11 @@ export interface UiSettingsApiResponse {
settings: UiSettingsState;
}

export interface ValidationApiResponse {
valid: boolean;
errorMessage?: string;
}

interface Changes {
values: {
[key: string]: any;
Expand Down Expand Up @@ -94,6 +99,15 @@ export class UiSettingsApi {
});
}

/**
* Sends a validation request to the server for the provided key+value pair.
*/
public async validate(key: string, value: any): Promise<ValidationApiResponse> {
return await this.sendRequest('POST', `/internal/kibana/settings/${key}/validate`, {
value,
});
}

/**
* Gets an observable that notifies subscribers of the current number of active requests
*/
Expand Down
Expand Up @@ -10,6 +10,9 @@ import { Subject } from 'rxjs';
import { materialize, take, toArray } from 'rxjs/operators';

import { UiSettingsClient } from './ui_settings_client';
import { ValidationApiResponse } from './ui_settings_api';

const TEST_VALIDATION_ERROR_MESSAGE = 'Test validation message.';

let done$: Subject<unknown>;

Expand All @@ -22,18 +25,25 @@ function setup(options: { defaults?: any; initialSettings?: any } = {}) {
const batchSetGlobal = jest.fn(() => ({
settings: {},
}));
const validate = jest.fn(
(): ValidationApiResponse => ({
valid: false,
errorMessage: TEST_VALIDATION_ERROR_MESSAGE,
})
);
done$ = new Subject();
const client = new UiSettingsClient({
defaults,
initialSettings,
api: {
batchSet,
batchSetGlobal,
validate,
} as any,
done$,
});

return { client, batchSet, batchSetGlobal };
return { client, batchSet, batchSetGlobal, validate };
}

afterEach(() => {
Expand Down Expand Up @@ -283,3 +293,27 @@ describe('#getUpdate$', () => {
expect(onComplete).toHaveBeenCalled();
});
});

describe('#validateValue', () => {
it('resolves to a ValueValidation', async () => {
const { client } = setup();

await expect(client.validateValue('foo', 'bar')).resolves.toMatchObject({
successfulValidation: true,
valid: false,
errorMessage: TEST_VALIDATION_ERROR_MESSAGE,
});
});

it('resolves to a ValueValidation on failure', async () => {
const { client, validate } = setup();

validate.mockImplementation(() => {
throw new Error('Error in request');
});

await expect(client.validateValue('foo', 'bar')).resolves.toMatchObject({
successfulValidation: false,
});
});
});
Expand Up @@ -128,6 +128,19 @@ You can use \`IUiSettingsClient.get("${key}", defaultValue)\`, which will just r
return this.updateErrors$.asObservable();
}

async validateValue(key: string, value: unknown) {
try {
const resp = await this.api.validate(key, value);
const isValid = resp.valid;
return isValid
? { successfulValidation: true, valid: true }
: { successfulValidation: true, valid: false, errorMessage: resp.errorMessage };
} catch (error) {
this.updateErrors$.next(error);
return { successfulValidation: false };
}
}

protected assertUpdateAllowed(key: string) {
if (this.isOverridden(key)) {
throw new Error(
Expand Down
Expand Up @@ -22,6 +22,7 @@ export const clientMock = () => {
isOverridden: jest.fn(),
getUpdate$: jest.fn(),
getUpdateErrors$: jest.fn(),
validateValue: jest.fn(),
};
mock.get$.mockReturnValue(new Subject<any>());
mock.getUpdate$.mockReturnValue(new Subject<any>());
Expand Down
11 changes: 11 additions & 0 deletions packages/core/ui-settings/core-ui-settings-browser/src/types.ts
Expand Up @@ -16,6 +16,12 @@ export interface UiSettingsState {
[key: string]: PublicUiSettingsParams & UserProvidedValues;
}

export interface ValueValidation {
successfulValidation: boolean;
valid?: boolean;
errorMessage?: string;
}

/**
* Client-side client that provides access to the advanced settings stored in elasticsearch.
* The settings provide control over the behavior of the Kibana application.
Expand Down Expand Up @@ -100,6 +106,11 @@ export interface IUiSettingsClient {
* the settings, containing the actual Error class.
*/
getUpdateErrors$: () => Observable<Error>;

/**
* Validates a uiSettings value and returns a ValueValidation object.
*/
validateValue: (key: string, value: any) => Promise<ValueValidation>;
}

/** @public */
Expand Down
Expand Up @@ -10,6 +10,7 @@ import { omit } from 'lodash';
import type { Logger } from '@kbn/logging';
import type { UiSettingsParams, UserProvidedValues } from '@kbn/core-ui-settings-common';
import type { IUiSettingsClient } from '@kbn/core-ui-settings-server';
import { ValidationBadValueError, ValidationSettingNotFoundError } from '../ui_settings_errors';

export interface BaseUiSettingsDefaultsClientOptions {
overrides?: Record<string, any>;
Expand Down Expand Up @@ -72,6 +73,24 @@ export abstract class BaseUiSettingsClient implements IUiSettingsClient {
return !!definition?.sensitive;
}

async validate(key: string, value: unknown) {
if (!value) {
throw new ValidationBadValueError();
}
const definition = this.defaults[key];
if (!definition) {
throw new ValidationSettingNotFoundError(key);
}
if (definition.schema) {
try {
definition.schema.validate(value);
} catch (error) {
return { valid: false, errorMessage: error.message };
}
}
return { valid: true };
}

protected validateKey(key: string, value: unknown) {
const definition = this.defaults[key];
if (value === null || definition === undefined) return;
Expand Down
Expand Up @@ -13,7 +13,11 @@ import { mockCreateOrUpgradeSavedConfig } from './ui_settings_client.test.mock';
import { SavedObjectsClient } from '@kbn/core-saved-objects-api-server-internal';
import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
import { UiSettingsClient } from './ui_settings_client';
import { CannotOverrideError } from '../ui_settings_errors';
import {
CannotOverrideError,
ValidationBadValueError,
ValidationSettingNotFoundError,
} from '../ui_settings_errors';

const logger = loggingSystemMock.create().get();

Expand Down Expand Up @@ -732,6 +736,48 @@ describe('ui settings', () => {
});
});

describe('#validate()', () => {
it('returns a correct validation response for an existing setting key and an invalid value', async () => {
const defaults = { foo: { schema: schema.number() } };
const { uiSettings } = setup({ defaults });

expect(await uiSettings.validate('foo', 'testValue')).toMatchObject({
valid: false,
errorMessage: 'expected value of type [number] but got [string]',
});
});

it('returns a correct validation response for an existing setting key and a valid value', async () => {
const defaults = { foo: { schema: schema.number() } };
const { uiSettings } = setup({ defaults });

expect(await uiSettings.validate('foo', 5)).toMatchObject({ valid: true });
});

it('throws for a non-existing setting key', async () => {
const { uiSettings } = setup();

try {
await uiSettings.validate('bar', 5);
} catch (error) {
expect(error).toBeInstanceOf(ValidationSettingNotFoundError);
expect(error.message).toBe('Setting with a key [bar] does not exist.');
}
});

it('throws for a null value', async () => {
const defaults = { foo: { schema: schema.number() } };
const { uiSettings } = setup({ defaults });

try {
await uiSettings.validate('foo', null);
} catch (error) {
expect(error).toBeInstanceOf(ValidationBadValueError);
expect(error.message).toBe('No value was specified.');
}
});
});

describe('caching', () => {
describe('read operations cache user config', () => {
beforeEach(() => {
Expand Down
Expand Up @@ -11,10 +11,12 @@ import { registerInternalDeleteRoute } from './delete';
import { registerInternalGetRoute } from './get';
import { registerInternalSetManyRoute } from './set_many';
import { registerInternalSetRoute } from './set';
import { registerInternalValidateRoute } from './validate';

export function registerInternalRoutes(router: InternalUiSettingsRouter) {
registerInternalGetRoute(router);
registerInternalDeleteRoute(router);
registerInternalSetRoute(router);
registerInternalSetManyRoute(router);
registerInternalValidateRoute(router);
}

0 comments on commit f4c5668

Please sign in to comment.