mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
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:
parent
95661be228
commit
ae31d2a07b
9 changed files with 79 additions and 16 deletions
|
@ -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>
|
||||
|
||||
|
|
|
@ -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<HistoryLocationState> | 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) |
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
},
|
||||
})
|
||||
),
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
|
30
test/api_integration/apis/core/capabilities.ts
Normal file
30
test/api_integration/apis/core/capabilities.ts
Normal 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -12,5 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
describe('core', () => {
|
||||
loadTestFile(require.resolve('./compression'));
|
||||
loadTestFile(require.resolve('./translations'));
|
||||
loadTestFile(require.resolve('./capabilities'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue