Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UiSettings validation & Kibana default route redirection #59694

Merged
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b20ecc7
add schema to ui settings params
mshustov Mar 9, 2020
7e46aef
add validation for defaults and overrides
mshustov Mar 9, 2020
5b55b83
validate in ui settings client
mshustov Mar 9, 2020
19dae10
ui settings routes validation
mshustov Mar 9, 2020
012a715
clean up tests
mshustov Mar 9, 2020
cc96364
use schema for defaultRoutes
mshustov Mar 9, 2020
269e34e
move URL redirection to NP
mshustov Mar 9, 2020
f2b2dea
fix spaces test
mshustov Mar 10, 2020
39e7405
Merge branch 'master' into issue-50658-default-route-redirection
mshustov Mar 10, 2020
b736ada
update docs
mshustov Mar 10, 2020
b01a668
update kbn pm
mshustov Mar 10, 2020
5518bd0
fix karma test
mshustov Mar 10, 2020
7a5a354
fix tests
mshustov Mar 10, 2020
850d78c
address comments
mshustov Mar 10, 2020
a7d0100
get rid of getDEfaultRoute
mshustov Mar 10, 2020
8ec9103
Merge branch 'master' into issue-50658-default-route-redirection
mshustov Mar 10, 2020
b21efe2
Merge branch 'master' into issue-50658-default-route-redirection
mshustov Mar 11, 2020
cb40a2b
regen docs
mshustov Mar 11, 2020
2f8f956
fix tests
mshustov Mar 11, 2020
f17a82f
fix enter-spaces test
mshustov Mar 11, 2020
6b5f699
validate on relative url format
mshustov Mar 11, 2020
9856d0b
update i18n
mshustov Mar 11, 2020
6e389a6
Merge branch 'master' into issue-50658-default-route-redirection
mshustov Mar 11, 2020
84e3e5a
fix enter-spoace test
mshustov Mar 11, 2020
c72b5cc
Merge branch 'master' into issue-50658-default-route-redirection
mshustov Mar 12, 2020
ed8a9a9
move relative url validation to utils
mshustov Mar 12, 2020
1036d77
add CoreApp containing application logic
mshustov Mar 12, 2020
075fe07
extract public uiSettings params in a separate type
mshustov Mar 12, 2020
3b8ad0c
make schema required
mshustov Mar 12, 2020
57c9aef
Merge branch 'master' into issue-50658-default-route-redirection
mshustov Mar 12, 2020
2ad3292
Merge branch 'master' into issue-50658-default-route-redirection
mshustov Mar 16, 2020
400a167
update docs
mshustov Mar 16, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ UiSettings parameters defined by the plugins.
<b>Signature:</b>

```typescript
export interface UiSettingsParams
export interface UiSettingsParams<T = unknown>
```

## Properties
Expand All @@ -24,7 +24,8 @@ export interface UiSettingsParams
| [options](./kibana-plugin-public.uisettingsparams.options.md) | <code>string[]</code> | array of permitted values for this setting |
| [readonly](./kibana-plugin-public.uisettingsparams.readonly.md) | <code>boolean</code> | a flag indicating that value cannot be changed |
| [requiresPageReload](./kibana-plugin-public.uisettingsparams.requirespagereload.md) | <code>boolean</code> | a flag indicating whether new value applying requires page reloading |
| [schema](./kibana-plugin-public.uisettingsparams.schema.md) | <code>Type&lt;T&gt;</code> | |
| [type](./kibana-plugin-public.uisettingsparams.type.md) | <code>UiSettingsType</code> | defines a type of UI element [UiSettingsType](./kibana-plugin-public.uisettingstype.md) |
| [validation](./kibana-plugin-public.uisettingsparams.validation.md) | <code>ImageValidation &#124; StringValidation</code> | |
| [value](./kibana-plugin-public.uisettingsparams.value.md) | <code>SavedObjectAttribute</code> | default value to fall back to if a user doesn't provide any |
| [value](./kibana-plugin-public.uisettingsparams.value.md) | <code>T</code> | default value to fall back to if a user doesn't provide any |

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [UiSettingsParams](./kibana-plugin-public.uisettingsparams.md) &gt; [schema](./kibana-plugin-public.uisettingsparams.schema.md)

## UiSettingsParams.schema property

<b>Signature:</b>

```typescript
schema?: Type<T>;
```
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ default value to fall back to if a user doesn't provide any
<b>Signature:</b>

```typescript
value?: SavedObjectAttribute;
value?: T;
```
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ export interface AuthToolkit
| --- | --- | --- |
| [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | <code>(data?: AuthResultParams) =&gt; AuthResult</code> | Authentication is successful with given credentials, allow request to pass through |
| [notHandled](./kibana-plugin-server.authtoolkit.nothandled.md) | <code>() =&gt; AuthResult</code> | User has no credentials. Allows user to access a resource when authRequired: 'optional' Rejects a request when authRequired: true |
| [redirected](./kibana-plugin-server.authtoolkit.redirected.md) | <code>(headers: {</code><br/><code> location: string;</code><br/><code> } &amp; ResponseHeaders) =&gt; AuthResult</code> | Redirect user to IdP when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional' |
| [redirected](./kibana-plugin-server.authtoolkit.redirected.md) | <code>(headers: {</code><br/><code> location: string;</code><br/><code> } &amp; ResponseHeaders) =&gt; AuthResult</code> | Redirects user to another location to complete authentication when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional' |

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## AuthToolkit.redirected property

Redirect user to IdP when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional'
Redirects user to another location to complete authentication when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional'

<b>Signature:</b>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ Returns registered uiSettings values [UiSettingsParams](./kibana-plugin-server.u
<b>Signature:</b>

```typescript
getRegistered: () => Readonly<Record<string, UiSettingsParams>>;
getRegistered: () => Readonly<Record<string, Omit<UiSettingsParams, 'schema'>>>;
```
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface IUiSettingsClient
| --- | --- | --- |
| [get](./kibana-plugin-server.iuisettingsclient.get.md) | <code>&lt;T = any&gt;(key: string) =&gt; Promise&lt;T&gt;</code> | Retrieves uiSettings values set by the user with fallbacks to default values if not specified. |
| [getAll](./kibana-plugin-server.iuisettingsclient.getall.md) | <code>&lt;T = any&gt;() =&gt; Promise&lt;Record&lt;string, T&gt;&gt;</code> | Retrieves a set of all uiSettings values set by the user with fallbacks to default values if not specified. |
| [getRegistered](./kibana-plugin-server.iuisettingsclient.getregistered.md) | <code>() =&gt; Readonly&lt;Record&lt;string, UiSettingsParams&gt;&gt;</code> | Returns registered uiSettings values [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) |
| [getRegistered](./kibana-plugin-server.iuisettingsclient.getregistered.md) | <code>() =&gt; Readonly&lt;Record&lt;string, Omit&lt;UiSettingsParams, 'schema'&gt;&gt;&gt;</code> | Returns registered uiSettings values [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) |
| [getUserProvided](./kibana-plugin-server.iuisettingsclient.getuserprovided.md) | <code>&lt;T = any&gt;() =&gt; Promise&lt;Record&lt;string, UserProvidedValues&lt;T&gt;&gt;&gt;</code> | Retrieves a set of all uiSettings values set by the user. |
| [isOverridden](./kibana-plugin-server.iuisettingsclient.isoverridden.md) | <code>(key: string) =&gt; boolean</code> | Shows whether the uiSettings value set by the user. |
| [remove](./kibana-plugin-server.iuisettingsclient.remove.md) | <code>(key: string) =&gt; Promise&lt;void&gt;</code> | Removes uiSettings value by key. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ UiSettings parameters defined by the plugins.
<b>Signature:</b>

```typescript
export interface UiSettingsParams
export interface UiSettingsParams<T = unknown>
```

## Properties
Expand All @@ -24,7 +24,8 @@ export interface UiSettingsParams
| [options](./kibana-plugin-server.uisettingsparams.options.md) | <code>string[]</code> | array of permitted values for this setting |
| [readonly](./kibana-plugin-server.uisettingsparams.readonly.md) | <code>boolean</code> | a flag indicating that value cannot be changed |
| [requiresPageReload](./kibana-plugin-server.uisettingsparams.requirespagereload.md) | <code>boolean</code> | a flag indicating whether new value applying requires page reloading |
| [schema](./kibana-plugin-server.uisettingsparams.schema.md) | <code>Type&lt;T&gt;</code> | |
| [type](./kibana-plugin-server.uisettingsparams.type.md) | <code>UiSettingsType</code> | defines a type of UI element [UiSettingsType](./kibana-plugin-server.uisettingstype.md) |
| [validation](./kibana-plugin-server.uisettingsparams.validation.md) | <code>ImageValidation &#124; StringValidation</code> | |
| [value](./kibana-plugin-server.uisettingsparams.value.md) | <code>SavedObjectAttribute</code> | default value to fall back to if a user doesn't provide any |
| [value](./kibana-plugin-server.uisettingsparams.value.md) | <code>T</code> | default value to fall back to if a user doesn't provide any |

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) &gt; [schema](./kibana-plugin-server.uisettingsparams.schema.md)

## UiSettingsParams.schema property

<b>Signature:</b>

```typescript
schema?: Type<T>;
```
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ default value to fall back to if a user doesn't provide any
<b>Signature:</b>

```typescript
value?: SavedObjectAttribute;
value?: T;
```
3 changes: 3 additions & 0 deletions packages/kbn-config-schema/src/errors/validation_error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,8 @@ export class ValidationError extends SchemaError {

constructor(error: SchemaTypeError, namespace?: string) {
super(ValidationError.extractMessage(error, namespace), error);

// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class KbnClientUiSettings {
* Replace all uiSettings with the `doc` values, `doc` is merged
* with some defaults
*/
async replace(doc: UiSettingValues) {
async replace(doc: UiSettingValues, { retries = 5 }: { retries?: number } = {}) {
this.log.debug('replacing kibana config doc: %j', doc);

const changes: Record<string, any> = {
Expand All @@ -85,7 +85,7 @@ export class KbnClientUiSettings {
method: 'POST',
path: '/api/kibana/settings',
body: { changes },
retries: 5,
retries,
});
}

Expand Down
4 changes: 2 additions & 2 deletions packages/kbn-pm/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43920,7 +43920,7 @@ class KbnClientUiSettings {
* Replace all uiSettings with the `doc` values, `doc` is merged
* with some defaults
*/
async replace(doc) {
async replace(doc, { retries = 5 } = {}) {
this.log.debug('replacing kibana config doc: %j', doc);
const changes = {
...this.defaults,
Expand All @@ -43935,7 +43935,7 @@ class KbnClientUiSettings {
method: 'POST',
path: '/api/kibana/settings',
body: { changes },
retries: 5,
retries,
});
}
/**
Expand Down
7 changes: 5 additions & 2 deletions src/core/public/public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Observable } from 'rxjs';
import React from 'react';
import * as Rx from 'rxjs';
import { ShallowPromise } from '@kbn/utility-types';
import { Type } from '@kbn/config-schema';
import { UiSettingsParams as UiSettingsParams_2 } from 'src/core/server/types';
import { UnregisterCallback } from 'history';
import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/types';
Expand Down Expand Up @@ -1289,7 +1290,7 @@ export type ToastsSetup = IToasts;
export type ToastsStart = IToasts;

// @public
export interface UiSettingsParams {
export interface UiSettingsParams<T = unknown> {
category?: string[];
// Warning: (ae-forgotten-export) The symbol "DeprecationSettings" needs to be exported by the entry point index.d.ts
deprecation?: DeprecationSettings;
Expand All @@ -1299,10 +1300,12 @@ export interface UiSettingsParams {
options?: string[];
readonly?: boolean;
requiresPageReload?: boolean;
// (undocumented)
schema?: Type<T>;
type?: UiSettingsType;
// (undocumented)
validation?: ImageValidation | StringValidation;
value?: SavedObjectAttribute;
value?: T;
}

// @public (undocumented)
Expand Down

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

2 changes: 1 addition & 1 deletion src/core/public/ui_settings/ui_settings_api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ describe('#batchSet', () => {
'*',
{
status: 400,
body: 'invalid',
body: { message: 'invalid' },
},
{
overwriteRoutes: false,
Expand Down
10 changes: 7 additions & 3 deletions src/core/public/ui_settings/ui_settings_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,14 @@ export class UiSettingsApi {
},
});
} catch (err) {
if (err.response && err.response.status >= 300) {
throw new Error(`Request failed with status code: ${err.response.status}`);
if (err.response) {
if (err.response.status === 400) {
throw new Error(err.body.message);
}
if (err.response.status > 400) {
throw new Error(`Request failed with status code: ${err.response.status}`);
Comment on lines +156 to +160
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably related to the changes in src/core/server/ui_settings/routes/set.ts , but why the difference between 400 and 4xx?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request failed with status code: ${err.response.status} is not really useful. I'd rather remove them completely and provide a more descriptive message, but BWC...So I added this for BadRequest only to provide an actionable message.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We really need typing for our errors so that we can evolve these 😢

}
}

throw err;
} finally {
this.loadingCount$.next(this.loadingCount$.getValue() - 1);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import * as kbnTestServer from '../../../../test_utils/kbn_server';
import { Root } from '../../root';

const { startES } = kbnTestServer.createTestServers({
mshustov marked this conversation as resolved.
Show resolved Hide resolved
adjustTimeout: (t: number) => jest.setTimeout(t),
});
let esServer: kbnTestServer.TestElasticsearchUtils;

describe('default route provider', () => {
let root: Root;

beforeAll(async () => {
esServer = await startES();
root = kbnTestServer.createRootWithCorePlugins({
server: {
basePath: '/hello',
},
});

await root.setup();
await root.start();
});

afterAll(async () => {
await esServer.stop();
await root.shutdown();
});

it('redirects to the configured default route respecting basePath', async function() {
const { status, header } = await kbnTestServer.request.get(root, '/');

expect(status).toEqual(302);
expect(header).toMatchObject({
location: '/hello/app/kibana',
});
});

it('ignores invalid values', async function() {
const invalidRoutes = ['http://not-your-kibana.com', 'example.com', ' //example.com'];

for (const url of invalidRoutes) {
await kbnTestServer.request
.post(root, '/api/kibana/settings/defaultRoute')
.send({ value: url })
.expect(400);
}

const { status, header } = await kbnTestServer.request.get(root, '/');
expect(status).toEqual(302);
expect(header).toMatchObject({
location: '/hello/app/kibana',
});
});

it('consumes valid values', async function() {
await kbnTestServer.request
.post(root, '/api/kibana/settings/defaultRoute')
.send({ value: '/valid' })
.expect(200);

const { status, header } = await kbnTestServer.request.get(root, '/');
expect(status).toEqual(302);
expect(header).toMatchObject({
location: '/hello/valid',
});
});
});
8 changes: 5 additions & 3 deletions src/core/server/server.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -982,7 +982,7 @@ export interface IScopedRenderingClient {
export interface IUiSettingsClient {
get: <T = any>(key: string) => Promise<T>;
getAll: <T = any>() => Promise<Record<string, T>>;
getRegistered: () => Readonly<Record<string, UiSettingsParams>>;
getRegistered: () => Readonly<Record<string, Omit<UiSettingsParams, 'schema'>>>;
getUserProvided: <T = any>() => Promise<Record<string, UserProvidedValues<T>>>;
isOverridden: (key: string) => boolean;
remove: (key: string) => Promise<void>;
Expand Down Expand Up @@ -2251,7 +2251,7 @@ export interface StringValidationRegexString {
}

// @public
export interface UiSettingsParams {
export interface UiSettingsParams<T = unknown> {
category?: string[];
deprecation?: DeprecationSettings;
description?: string;
Expand All @@ -2260,10 +2260,12 @@ export interface UiSettingsParams {
options?: string[];
readonly?: boolean;
requiresPageReload?: boolean;
// (undocumented)
schema?: Type<T>;
type?: UiSettingsType;
// (undocumented)
validation?: ImageValidation | StringValidation;
value?: SavedObjectAttribute;
value?: T;
}

// @public (undocumented)
Expand Down
20 changes: 15 additions & 5 deletions src/core/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,6 @@ export class Server {
context: contextServiceSetup,
});

this.registerDefaultRoute(httpSetup);

const capabilitiesSetup = this.capabilities.setup({ http: httpSetup });

const elasticsearchServiceSetup = await this.elasticsearch.setup({
Expand Down Expand Up @@ -167,6 +165,7 @@ export class Server {
});

this.registerCoreContext(coreSetup, renderingSetup);
this.registerDefaultRoutes(httpSetup);

return coreSetup;
}
Expand Down Expand Up @@ -217,9 +216,20 @@ export class Server {
await this.metrics.stop();
}

private registerDefaultRoute(httpSetup: InternalHttpServiceSetup) {
const router = httpSetup.createRouter('/core');
router.get({ path: '/', validate: false }, async (context, req, res) =>
private registerDefaultRoutes(httpSetup: InternalHttpServiceSetup) {
const router = httpSetup.createRouter('/');
router.get({ path: '/', validate: false }, async (context, req, res) => {
const defaultRoute = await context.core.uiSettings.client.get<string>('defaultRoute');
const basePath = httpSetup.basePath.get(req);
const url = `${basePath}${defaultRoute}`;

return res.redirected({
headers: {
location: url,
},
});
});
router.get({ path: '/core', validate: false }, async (context, req, res) =>
res.ok({ body: { version: '0.0.1' } })
);
}
Expand Down
Loading