Skip to content

Commit

Permalink
Add validation for the /api/core/capabilities endpoint (#129564) (#12…
Browse files Browse the repository at this point in the history
…9747)

* 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>
(cherry picked from commit ae31d2a)
  • Loading branch information
pgayvallet authored Apr 7, 2022
1 parent e135271 commit 2796612
Show file tree
Hide file tree
Showing 9 changed files with 79 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down
10 changes: 10 additions & 0 deletions src/core/public/application/application_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
29 changes: 18 additions & 11 deletions src/core/public/application/application_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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, {
Expand Down
5 changes: 4 additions & 1 deletion src/core/public/application/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -823,6 +825,7 @@ export interface ApplicationStart {
* @param url - an absolute URL, an absolute path or a relative path, to navigate to.
*/
navigateToUrl(url: string, options?: NavigateToUrlOptions): Promise<void>;

/**
* Returns the absolute path (or URL) to a given app, including the global base path.
*
Expand Down
12 changes: 11 additions & 1 deletion src/core/server/capabilities/routes/resolve_capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand All @@ -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';
}
},
})
),
}),
},
},
Expand Down
30 changes: 30 additions & 0 deletions test/api_integration/apis/core/capabilities.ts
Original file line number Diff line number Diff line change
@@ -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',
});
});
});
}
1 change: 1 addition & 0 deletions test/api_integration/apis/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
describe('core', () => {
loadTestFile(require.resolve('./compression'));
loadTestFile(require.resolve('./translations'));
loadTestFile(require.resolve('./capabilities'));
});
}

0 comments on commit 2796612

Please sign in to comment.