Add validation for the /api/core/capabilities endpoint (#129564)

* Add validation for the /api/core/capabilities endpoint

* update doc for app.id

* also allow `:`

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Pierre Gayvallet 2022-04-07 16:02:04 +02:00 committed by GitHub
parent 95661be228
commit ae31d2a07b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 79 additions and 16 deletions

View file

@ -4,7 +4,9 @@
## App.id property
The unique identifier of the application
The unique identifier of the application.
Can only be composed of alphanumeric characters, `-`<!-- -->, `:` and `_`
<b>Signature:</b>

View file

@ -23,7 +23,7 @@ export interface App<HistoryLocationState = unknown> extends AppNavOptions
| [deepLinks?](./kibana-plugin-core-public.app.deeplinks.md) | AppDeepLink\[\] | <i>(Optional)</i> Input type for registering secondary in-app locations for an application.<!-- -->Deep links must include at least one of <code>path</code> or <code>deepLinks</code>. A deep link that does not have a <code>path</code> represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. |
| [defaultPath?](./kibana-plugin-core-public.app.defaultpath.md) | string | <i>(Optional)</i> Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the <code>path</code> option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)<!-- -->\`<!-- -->, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar. |
| [exactRoute?](./kibana-plugin-core-public.app.exactroute.md) | boolean | <i>(Optional)</i> If set to true, the application's route will only be checked against an exact match. Defaults to <code>false</code>. |
| [id](./kibana-plugin-core-public.app.id.md) | string | The unique identifier of the application |
| [id](./kibana-plugin-core-public.app.id.md) | string | The unique identifier of the application.<!-- -->Can only be composed of alphanumeric characters, <code>-</code>, <code>:</code> and <code>_</code> |
| [keywords?](./kibana-plugin-core-public.app.keywords.md) | string\[\] | <i>(Optional)</i> Optional keywords to match with in deep links search. Omit if this part of the hierarchy does not have a page URL. |
| [mount](./kibana-plugin-core-public.app.mount.md) | AppMount&lt;HistoryLocationState&gt; | A mount function called when the user navigates to this app's route. |
| [navLinkStatus?](./kibana-plugin-core-public.app.navlinkstatus.md) | AppNavLinkStatus | <i>(Optional)</i> The initial status of the application's navLink. Defaulting to <code>visible</code> if <code>status</code> is <code>accessible</code> and <code>hidden</code> if status is <code>inaccessible</code> See [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) |

View file

@ -23,7 +23,7 @@ navigateToUrl(url: string, options?: NavigateToUrlOptions): Promise<void>;
| Parameter | Type | Description |
| --- | --- | --- |
| url | string | an absolute URL, an absolute path or a relative path, to navigate to. |
| options | NavigateToUrlOptions | |
| options | NavigateToUrlOptions | navigation options |
<b>Returns:</b>

View file

@ -65,6 +65,16 @@ describe('#setup()', () => {
);
});
it('throws an error if app is registered with an invalid id', () => {
const { register } = service.setup(setupDeps);
expect(() =>
register(Symbol(), createApp({ id: 'invalid&app' }))
).toThrowErrorMatchingInlineSnapshot(
`"Invalid application id: it can only be composed of alphanum chars, '-' and '_'"`
);
});
it('throws error if additional apps are registered after setup', async () => {
const { register } = service.setup(setupDeps);

View file

@ -73,6 +73,7 @@ const getAppDeepLinkPath = (app: App<any>, appId: string, deepLinkId: string) =>
return flattenedLinks[deepLinkId];
};
const applicationIdRegexp = /^[a-zA-Z0-9_:-]+$/;
const allApplicationsFilter = '__ALL__';
interface AppUpdaterWrapper {
@ -155,21 +156,27 @@ export class ApplicationService {
};
};
const validateApp = (app: App<unknown>) => {
if (this.registrationClosed) {
throw new Error(`Applications cannot be registered after "setup"`);
} else if (!applicationIdRegexp.test(app.id)) {
throw new Error(
`Invalid application id: it can only be composed of alphanum chars, '-' and '_'`
);
} else if (this.apps.has(app.id)) {
throw new Error(`An application is already registered with the id "${app.id}"`);
} else if (findMounter(this.mounters, app.appRoute)) {
throw new Error(`An application is already registered with the appRoute "${app.appRoute}"`);
} else if (basename && app.appRoute!.startsWith(`${basename}/`)) {
throw new Error('Cannot register an application route that includes HTTP base path');
}
};
return {
register: (plugin, app: App<any>) => {
app = { appRoute: `/app/${app.id}`, ...app };
if (this.registrationClosed) {
throw new Error(`Applications cannot be registered after "setup"`);
} else if (this.apps.has(app.id)) {
throw new Error(`An application is already registered with the id "${app.id}"`);
} else if (findMounter(this.mounters, app.appRoute)) {
throw new Error(
`An application is already registered with the appRoute "${app.appRoute}"`
);
} else if (basename && app.appRoute!.startsWith(`${basename}/`)) {
throw new Error('Cannot register an application route that includes HTTP base path');
}
validateApp(app);
const { updater$, ...appProps } = app;
this.apps.set(app.id, {

View file

@ -107,7 +107,9 @@ export type AppUpdater = (app: App) => Partial<AppUpdatableFields> | undefined;
*/
export interface App<HistoryLocationState = unknown> extends AppNavOptions {
/**
* The unique identifier of the application
* The unique identifier of the application.
*
* Can only be composed of alphanumeric characters, `-`, `:` and `_`
*/
id: string;
@ -824,6 +826,7 @@ export interface ApplicationStart {
* @param options - navigation options
*/
navigateToUrl(url: string, options?: NavigateToUrlOptions): Promise<void>;
/**
* Returns the absolute path (or URL) to a given app, including the global base path.
*

View file

@ -10,6 +10,8 @@ import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CapabilitiesResolver } from '../resolve_capabilities';
const applicationIdRegexp = /^[a-zA-Z0-9_:-]+$/;
export function registerCapabilitiesRoutes(router: IRouter, resolver: CapabilitiesResolver) {
router.post(
{
@ -22,7 +24,15 @@ export function registerCapabilitiesRoutes(router: IRouter, resolver: Capabiliti
useDefaultCapabilities: schema.boolean({ defaultValue: false }),
}),
body: schema.object({
applications: schema.arrayOf(schema.string()),
applications: schema.arrayOf(
schema.string({
validate: (appName) => {
if (!applicationIdRegexp.test(appName)) {
return 'Invalid application id';
}
},
})
),
}),
},
},

View file

@ -0,0 +1,30 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('/api/core/capabilities', () => {
it(`returns a 400 when an invalid app id is provided`, async () => {
const { body } = await supertest
.post('/api/core/capabilities')
.send({
applications: ['dashboard', 'discover', 'bad%app'],
})
.expect(400);
expect(body).to.eql({
statusCode: 400,
error: 'Bad Request',
message: '[request body.applications.2]: Invalid application id',
});
});
});
}

View file

@ -12,5 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
describe('core', () => {
loadTestFile(require.resolve('./compression'));
loadTestFile(require.resolve('./translations'));
loadTestFile(require.resolve('./capabilities'));
});
}