[7.x] [new-platform] Introduce ApplicationService scaffolding and capabilities loading (#35545) (#36154)

This commit is contained in:
Josh Dover 2019-05-07 09:43:52 -05:00 committed by GitHub
parent 9e0687a4e0
commit d6f15af6e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
98 changed files with 1783 additions and 742 deletions

View file

@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ApplicationSetup](./kibana-plugin-public.applicationsetup.md)
## ApplicationSetup interface
<b>Signature:</b>
```typescript
export interface ApplicationSetup
```
## Methods
| Method | Description |
| --- | --- |
| [registerApp(app)](./kibana-plugin-public.applicationsetup.registerapp.md) | Register an mountable application to the system. Apps will be mounted based on their <code>rootRoute</code>. |

View file

@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) &gt; [registerApp](./kibana-plugin-public.applicationsetup.registerapp.md)
## ApplicationSetup.registerApp() method
Register an mountable application to the system. Apps will be mounted based on their `rootRoute`<!-- -->.
<b>Signature:</b>
```typescript
registerApp(app: App): void;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| app | <code>App</code> | |
<b>Returns:</b>
`void`

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ApplicationStart](./kibana-plugin-public.applicationstart.md) &gt; [availableApps](./kibana-plugin-public.applicationstart.availableapps.md)
## ApplicationStart.availableApps property
<b>Signature:</b>
```typescript
availableApps: CapabilitiesStart['availableApps'];
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ApplicationStart](./kibana-plugin-public.applicationstart.md) &gt; [capabilities](./kibana-plugin-public.applicationstart.capabilities.md)
## ApplicationStart.capabilities property
<b>Signature:</b>
```typescript
capabilities: CapabilitiesStart['capabilities'];
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ApplicationStart](./kibana-plugin-public.applicationstart.md)
## ApplicationStart interface
<b>Signature:</b>
```typescript
export interface ApplicationStart
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [availableApps](./kibana-plugin-public.applicationstart.availableapps.md) | <code>CapabilitiesStart['availableApps']</code> | |
| [capabilities](./kibana-plugin-public.applicationstart.capabilities.md) | <code>CapabilitiesStart['capabilities']</code> | |
| [mount](./kibana-plugin-public.applicationstart.mount.md) | <code>(mountHandler: Function) =&gt; void</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ApplicationStart](./kibana-plugin-public.applicationstart.md) &gt; [mount](./kibana-plugin-public.applicationstart.mount.md)
## ApplicationStart.mount property
<b>Signature:</b>
```typescript
mount: (mountHandler: Function) => void;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [BasePathStart](./kibana-plugin-public.basepathstart.md)
## BasePathStart type
Provides access to the 'server.basePath' configuration option in kibana.yml
<b>Signature:</b>
```typescript
export declare type BasePathStart = BasePathSetup;
```

View file

@ -1,13 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [CapabilitiesStart](./kibana-plugin-public.capabilitiesstart.md) &gt; [getCapabilities](./kibana-plugin-public.capabilitiesstart.getcapabilities.md)
## CapabilitiesStart.getCapabilities property
Gets the read-only capabilities.
<b>Signature:</b>
```typescript
getCapabilities: () => Capabilities;
```

View file

@ -1,20 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [CapabilitiesStart](./kibana-plugin-public.capabilitiesstart.md)
## CapabilitiesStart interface
Capabilities Setup.
<b>Signature:</b>
```typescript
export interface CapabilitiesStart
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [getCapabilities](./kibana-plugin-public.capabilitiesstart.getcapabilities.md) | <code>() =&gt; Capabilities</code> | Gets the read-only capabilities. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [CoreSetup](./kibana-plugin-public.coresetup.md) &gt; [application](./kibana-plugin-public.coresetup.application.md)
## CoreSetup.application property
[ApplicationSetup](./kibana-plugin-public.applicationsetup.md)
<b>Signature:</b>
```typescript
application: ApplicationSetup;
```

View file

@ -16,6 +16,7 @@ export interface CoreSetup
| Property | Type | Description |
| --- | --- | --- |
| [application](./kibana-plugin-public.coresetup.application.md) | <code>ApplicationSetup</code> | [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) |
| [basePath](./kibana-plugin-public.coresetup.basepath.md) | <code>BasePathSetup</code> | [BasePathSetup](./kibana-plugin-public.basepathsetup.md) |
| [chrome](./kibana-plugin-public.coresetup.chrome.md) | <code>ChromeSetup</code> | [ChromeSetup](./kibana-plugin-public.chromesetup.md) |
| [fatalErrors](./kibana-plugin-public.coresetup.fatalerrors.md) | <code>FatalErrorsSetup</code> | [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) |

View file

@ -1,13 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [CoreStart](./kibana-plugin-public.corestart.md) &gt; [capabilities](./kibana-plugin-public.corestart.capabilities.md)
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [CoreStart](./kibana-plugin-public.corestart.md) &gt; [application](./kibana-plugin-public.corestart.application.md)
## CoreStart.capabilities property
## CoreStart.application property
[CapabilitiesStart](./kibana-plugin-public.capabilitiesstart.md)
[ApplicationStart](./kibana-plugin-public.applicationstart.md)
<b>Signature:</b>
```typescript
capabilities: CapabilitiesStart;
application: ApplicationStart;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [CoreStart](./kibana-plugin-public.corestart.md) &gt; [basePath](./kibana-plugin-public.corestart.basepath.md)
## CoreStart.basePath property
[BasePathStart](./kibana-plugin-public.basepathstart.md)
<b>Signature:</b>
```typescript
basePath: BasePathStart;
```

View file

@ -14,7 +14,8 @@ export interface CoreStart
| Property | Type | Description |
| --- | --- | --- |
| [capabilities](./kibana-plugin-public.corestart.capabilities.md) | <code>CapabilitiesStart</code> | [CapabilitiesStart](./kibana-plugin-public.capabilitiesstart.md) |
| [application](./kibana-plugin-public.corestart.application.md) | <code>ApplicationStart</code> | [ApplicationStart](./kibana-plugin-public.applicationstart.md) |
| [basePath](./kibana-plugin-public.corestart.basepath.md) | <code>BasePathStart</code> | [BasePathStart](./kibana-plugin-public.basepathstart.md) |
| [i18n](./kibana-plugin-public.corestart.i18n.md) | <code>I18nStart</code> | [I18nStart](./kibana-plugin-public.i18nstart.md) |
| [injectedMetadata](./kibana-plugin-public.corestart.injectedmetadata.md) | <code>InjectedMetadataStart</code> | [InjectedMetadataStart](./kibana-plugin-public.injectedmetadatastart.md) |
| [notifications](./kibana-plugin-public.corestart.notifications.md) | <code>NotificationsStart</code> | [NotificationsStart](./kibana-plugin-public.notificationsstart.md) |

View file

@ -11,7 +11,7 @@ getLegacyMetadata: () => {
app: unknown;
translations: unknown;
bundleId: string;
nav: unknown;
nav: LegacyNavLink[];
version: string;
branch: string;
buildNum: number;

View file

@ -21,6 +21,6 @@ export interface InjectedMetadataSetup
| [getInjectedVar](./kibana-plugin-public.injectedmetadatasetup.getinjectedvar.md) | <code>(name: string, defaultValue?: any) =&gt; unknown</code> | |
| [getInjectedVars](./kibana-plugin-public.injectedmetadatasetup.getinjectedvars.md) | <code>() =&gt; {`<p/>` [key: string]: unknown;`<p/>` }</code> | |
| [getKibanaVersion](./kibana-plugin-public.injectedmetadatasetup.getkibanaversion.md) | <code>() =&gt; string</code> | |
| [getLegacyMetadata](./kibana-plugin-public.injectedmetadatasetup.getlegacymetadata.md) | <code>() =&gt; {`<p/>` app: unknown;`<p/>` translations: unknown;`<p/>` bundleId: string;`<p/>` nav: unknown;`<p/>` version: string;`<p/>` branch: string;`<p/>` buildNum: number;`<p/>` buildSha: string;`<p/>` basePath: string;`<p/>` serverName: string;`<p/>` devMode: boolean;`<p/>` uiSettings: {`<p/>` defaults: UiSettingsState;`<p/>` user?: UiSettingsState &#124; undefined;`<p/>` };`<p/>` }</code> | |
| [getLegacyMetadata](./kibana-plugin-public.injectedmetadatasetup.getlegacymetadata.md) | <code>() =&gt; {`<p/>` app: unknown;`<p/>` translations: unknown;`<p/>` bundleId: string;`<p/>` nav: LegacyNavLink[];`<p/>` version: string;`<p/>` branch: string;`<p/>` buildNum: number;`<p/>` buildSha: string;`<p/>` basePath: string;`<p/>` serverName: string;`<p/>` devMode: boolean;`<p/>` uiSettings: {`<p/>` defaults: UiSettingsState;`<p/>` user?: UiSettingsState &#124; undefined;`<p/>` };`<p/>` }</code> | |
| [getPlugins](./kibana-plugin-public.injectedmetadatasetup.getplugins.md) | <code>() =&gt; Array&lt;{`<p/>` id: string;`<p/>` plugin: DiscoveredPlugin;`<p/>` }&gt;</code> | An array of frontend plugins in topological order. |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) &gt; [euiIconType](./kibana-plugin-public.legacynavlink.euiicontype.md)
## LegacyNavLink.euiIconType property
<b>Signature:</b>
```typescript
euiIconType?: string;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) &gt; [icon](./kibana-plugin-public.legacynavlink.icon.md)
## LegacyNavLink.icon property
<b>Signature:</b>
```typescript
icon?: string;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) &gt; [id](./kibana-plugin-public.legacynavlink.id.md)
## LegacyNavLink.id property
<b>Signature:</b>
```typescript
id: string;
```

View file

@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [LegacyNavLink](./kibana-plugin-public.legacynavlink.md)
## LegacyNavLink interface
<b>Signature:</b>
```typescript
export interface LegacyNavLink
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [euiIconType](./kibana-plugin-public.legacynavlink.euiicontype.md) | <code>string</code> | |
| [icon](./kibana-plugin-public.legacynavlink.icon.md) | <code>string</code> | |
| [id](./kibana-plugin-public.legacynavlink.id.md) | <code>string</code> | |
| [order](./kibana-plugin-public.legacynavlink.order.md) | <code>number</code> | |
| [title](./kibana-plugin-public.legacynavlink.title.md) | <code>string</code> | |
| [url](./kibana-plugin-public.legacynavlink.url.md) | <code>string</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) &gt; [order](./kibana-plugin-public.legacynavlink.order.md)
## LegacyNavLink.order property
<b>Signature:</b>
```typescript
order: number;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) &gt; [title](./kibana-plugin-public.legacynavlink.title.md)
## LegacyNavLink.title property
<b>Signature:</b>
```typescript
title: string;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) &gt; [url](./kibana-plugin-public.legacynavlink.url.md)
## LegacyNavLink.url property
<b>Signature:</b>
```typescript
url: string;
```

View file

@ -16,9 +16,10 @@
| Interface | Description |
| --- | --- |
| [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | |
| [ApplicationStart](./kibana-plugin-public.applicationstart.md) | |
| [BasePathSetup](./kibana-plugin-public.basepathsetup.md) | Provides access to the 'server.basePath' configuration option in kibana.yml |
| [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. |
| [CapabilitiesStart](./kibana-plugin-public.capabilitiesstart.md) | Capabilities Setup. |
| [ChromeBadge](./kibana-plugin-public.chromebadge.md) | |
| [ChromeBrand](./kibana-plugin-public.chromebrand.md) | |
| [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | |
@ -27,6 +28,7 @@
| [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. |
| [I18nSetup](./kibana-plugin-public.i18nsetup.md) | I18nSetup.Context is required by any localizable React component from @<!-- -->kbn/i18n and @<!-- -->elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. |
| [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) | Provides access to the metadata injected by the server into the page |
| [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) | |
| [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | |
| [OverlayStart](./kibana-plugin-public.overlaystart.md) | |
| [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a <code>PluginInitializer</code>. |
@ -38,6 +40,7 @@
| Type Alias | Description |
| --- | --- |
| [BasePathStart](./kibana-plugin-public.basepathstart.md) | Provides access to the 'server.basePath' configuration option in kibana.yml |
| [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | |
| [ChromeSetup](./kibana-plugin-public.chromesetup.md) | |
| [HttpSetup](./kibana-plugin-public.httpsetup.md) | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [PluginSetupContext](./kibana-plugin-public.pluginsetupcontext.md) &gt; [http](./kibana-plugin-public.pluginsetupcontext.http.md)
## PluginSetupContext.http property
<b>Signature:</b>
```typescript
http: HttpSetup;
```

View file

@ -19,6 +19,7 @@ export interface PluginSetupContext
| [basePath](./kibana-plugin-public.pluginsetupcontext.basepath.md) | <code>BasePathSetup</code> | |
| [chrome](./kibana-plugin-public.pluginsetupcontext.chrome.md) | <code>ChromeSetup</code> | |
| [fatalErrors](./kibana-plugin-public.pluginsetupcontext.fatalerrors.md) | <code>FatalErrorsSetup</code> | |
| [http](./kibana-plugin-public.pluginsetupcontext.http.md) | <code>HttpSetup</code> | |
| [i18n](./kibana-plugin-public.pluginsetupcontext.i18n.md) | <code>I18nSetup</code> | |
| [notifications](./kibana-plugin-public.pluginsetupcontext.notifications.md) | <code>NotificationsSetup</code> | |
| [uiSettings](./kibana-plugin-public.pluginsetupcontext.uisettings.md) | <code>UiSettingsSetup</code> | |

View file

@ -0,0 +1,45 @@
/*
* 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 { capabilitiesServiceMock } from './capabilities/capabilities_service.mock';
import { ApplicationService, ApplicationSetup, ApplicationStart } from './application_service';
type ApplicationServiceContract = PublicMethodsOf<ApplicationService>;
const createSetupContractMock = (): jest.Mocked<ApplicationSetup> => ({
registerApp: jest.fn(),
registerLegacyApp: jest.fn(),
});
const createStartContractMock = (): jest.Mocked<ApplicationStart> => ({
mount: jest.fn(),
...capabilitiesServiceMock.createStartContract(),
});
const createMock = (): jest.Mocked<ApplicationServiceContract> => ({
setup: jest.fn().mockReturnValue(createSetupContractMock()),
start: jest.fn().mockReturnValue(createStartContractMock()),
stop: jest.fn(),
});
export const applicationServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -0,0 +1,28 @@
/*
* 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 { capabilitiesServiceMock } from './capabilities/capabilities_service.mock';
export const MockCapabilitiesService = capabilitiesServiceMock.create();
export const CapabilitiesServiceConstructor = jest
.fn()
.mockImplementation(() => MockCapabilitiesService);
jest.doMock('./capabilities', () => ({
CapabilitiesService: CapabilitiesServiceConstructor,
}));

View file

@ -0,0 +1,73 @@
/*
* 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 { basePathServiceMock } from '../base_path/base_path_service.mock';
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
import { MockCapabilitiesService } from './application_service.test.mocks';
import { ApplicationService } from './application_service';
describe('#start()', () => {
it('exposes available apps from capabilities', async () => {
const service = new ApplicationService();
const setup = service.setup();
setup.registerApp({ id: 'app1' } as any);
setup.registerLegacyApp({ id: 'app2' } as any);
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
const basePath = basePathServiceMock.createStartContract();
expect((await service.start({ basePath, injectedMetadata })).availableApps)
.toMatchInlineSnapshot(`
Array [
Object {
"id": "app1",
},
Object {
"id": "app2",
},
]
`);
});
it('passes registered applications to capabilities', async () => {
const service = new ApplicationService();
const setup = service.setup();
setup.registerApp({ id: 'app1' } as any);
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
const basePath = basePathServiceMock.createStartContract();
await service.start({ basePath, injectedMetadata });
expect(MockCapabilitiesService.start).toHaveBeenCalledWith({
apps: [{ id: 'app1' }],
basePath,
injectedMetadata,
});
});
it('passes registered legacy applications to capabilities', async () => {
const service = new ApplicationService();
const setup = service.setup();
setup.registerLegacyApp({ id: 'legacyApp1' } as any);
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
const basePath = basePathServiceMock.createStartContract();
await service.start({ basePath, injectedMetadata });
expect(MockCapabilitiesService.start).toHaveBeenCalledWith({
apps: [{ id: 'legacyApp1' }],
basePath,
injectedMetadata,
});
});
});

View file

@ -0,0 +1,152 @@
/*
* 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 { Observable, BehaviorSubject } from 'rxjs';
import { CapabilitiesStart, CapabilitiesService, Capabilities } from './capabilities';
import { InjectedMetadataStart } from '../injected_metadata';
import { BasePathStart } from '../base_path';
interface BaseApp {
id: string;
/**
* An ordinal used to sort nav links relative to one another for display.
*/
order: number;
/**
* The title of the application.
*/
title: string;
/**
* An observable for a tooltip shown when hovering over app link.
*/
tooltip$?: Observable<string>;
/**
* A EUI iconType that will be used for the app's icon. This icon
* takes precendence over the `icon` property.
*/
euiIconType?: string;
/**
* A URL to an image file used as an icon. Used as a fallback
* if `euiIconType` is not provided.
*/
icon?: string;
/**
* Custom capabilities defined by the app.
*/
capabilities?: Partial<Capabilities>;
}
/** @public */
export interface App extends BaseApp {
/**
* The root route to mount this application at.
*/
rootRoute: string;
/**
* A mount function called when the user navigates to this app's `rootRoute`.
* @param targetDomElement An HTMLElement to mount the application onto.
* @returns An unmounting function that will be called to unmount the application.
*/
mount(targetDomElement: HTMLElement): () => void;
}
/** @internal */
export interface LegacyApp extends BaseApp {
appUrl: string;
url?: string;
}
/** @internal */
export type MixedApp = Partial<App> & Partial<LegacyApp> & BaseApp;
/** @public */
export interface ApplicationSetup {
/**
* Register an mountable application to the system. Apps will be mounted based on their `rootRoute`.
* @param app
*/
registerApp(app: App): void;
/**
* Register metadata about legacy applications. Legacy apps will not be mounted when navigated to.
* @param app
* @internal
*/
registerLegacyApp(app: LegacyApp): void;
}
export interface ApplicationStart {
mount: (mountHandler: Function) => void;
availableApps: CapabilitiesStart['availableApps'];
capabilities: CapabilitiesStart['capabilities'];
}
interface StartDeps {
basePath: BasePathStart;
injectedMetadata: InjectedMetadataStart;
}
/**
* Service that is responsible for registering new applications.
* @internal
*/
export class ApplicationService {
private readonly apps$ = new BehaviorSubject<App[]>([]);
private readonly legacyApps$ = new BehaviorSubject<LegacyApp[]>([]);
private readonly capabilities = new CapabilitiesService();
public setup(): ApplicationSetup {
return {
registerApp: (app: App) => {
this.apps$.next([...this.apps$.value, app]);
},
registerLegacyApp: (app: LegacyApp) => {
this.legacyApps$.next([...this.legacyApps$.value, app]);
},
};
}
public async start({ basePath, injectedMetadata }: StartDeps): Promise<ApplicationStart> {
this.apps$.complete();
this.legacyApps$.complete();
const apps = [...this.apps$.value, ...this.legacyApps$.value];
const { capabilities, availableApps } = await this.capabilities.start({
apps,
basePath,
injectedMetadata,
});
return {
mount() {},
capabilities,
availableApps,
};
}
public stop() {}
}

View file

@ -16,28 +16,25 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Capabilities, CapabilitiesService, CapabilitiesStart } from './capabilities_service';
import { CapabilitiesService, CapabilitiesStart } from './capabilities_service';
import { deepFreeze } from '../../utils/deep_freeze';
import { MixedApp } from '../application_service';
const createStartContractMock = () => {
const startContract: jest.Mocked<CapabilitiesStart> = {
getCapabilities: jest.fn(),
};
startContract.getCapabilities.mockReturnValue({
const createStartContractMock = (
apps: ReadonlyArray<MixedApp> = []
): jest.Mocked<CapabilitiesStart> => ({
availableApps: apps,
capabilities: deepFreeze({
catalogue: {},
management: {},
navLinks: {},
} as Capabilities);
return startContract;
};
}),
});
type CapabilitiesServiceContract = PublicMethodsOf<CapabilitiesService>;
const createMock = () => {
const mocked: jest.Mocked<CapabilitiesServiceContract> = {
start: jest.fn(),
};
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
const createMock = (): jest.Mocked<CapabilitiesServiceContract> => ({
start: jest.fn().mockImplementation(({ apps }) => createStartContractMock(apps)),
});
export const capabilitiesServiceMock = {
create: createMock,

View file

@ -0,0 +1,117 @@
/*
* 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.
*/
// @ts-ignore
import fetchMock from 'fetch-mock/es5/client';
import { InjectedMetadataService } from '../../injected_metadata';
import { CapabilitiesService } from './capabilities_service';
import { basePathServiceMock } from '../../base_path/base_path_service.mock';
describe('#start', () => {
const basePath = basePathServiceMock.createStartContract();
basePath.addToPath.mockImplementation(str => str);
const injectedMetadata = new InjectedMetadataService({
injectedMetadata: {
vars: {
uiCapabilities: {
foo: { feature: true },
bar: { feature: true },
},
},
} as any,
}).start();
const apps = [{ id: 'app1' }, { id: 'app2', capabilities: { app2: { feature: true } } }] as any;
beforeEach(() => {
fetchMock.post('/api/capabilities', (url: string, options: any) => ({
body: options.body,
status: 200,
}));
});
afterEach(() => {
fetchMock.restore();
});
it('calls backend API with merged capabilities', async () => {
const service = new CapabilitiesService();
await service.start({ apps, basePath, injectedMetadata });
expect(fetchMock.calls()).toMatchInlineSnapshot(`
Array [
Array [
"/api/capabilities",
Object {
"body": "{\\"capabilities\\":{\\"navLinks\\":{\\"app2\\":true,\\"app1\\":true},\\"management\\":{},\\"catalogue\\":{},\\"app2\\":{\\"feature\\":true}}}",
"credentials": "same-origin",
"headers": Object {
"kbn-xsrf": "xxx",
},
"method": "POST",
},
],
]
`);
});
it('returns capabilities from backend', async () => {
const service = new CapabilitiesService();
expect((await service.start({ apps, basePath, injectedMetadata })).capabilities)
.toMatchInlineSnapshot(`
Object {
"app2": Object {
"feature": true,
},
"catalogue": Object {},
"management": Object {},
"navLinks": Object {
"app1": true,
"app2": true,
},
}
`);
});
it('filters available apps based on returned navLinks', async () => {
fetchMock.post(
'/api/capabilities',
(url: string, options: any) => ({
body: JSON.stringify({ capabilities: { navLinks: { app1: true, app2: false } } }),
status: 200,
}),
{ overwriteRoutes: true }
);
const service = new CapabilitiesService();
expect((await service.start({ apps, basePath, injectedMetadata })).availableApps).toEqual([
{ id: 'app1' },
]);
});
it('does not allow Capabilities to be modified', async () => {
const service = new CapabilitiesService();
const { capabilities } = await service.start({
apps,
basePath,
injectedMetadata,
});
// @ts-ignore TypeScript knows this shouldn't be possible
expect(() => (capabilities.foo = 'foo')).toThrowError();
});
});

View file

@ -16,10 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
import { InjectedMetadataStart } from '../injected_metadata';
import { deepFreeze } from '../utils/deep_freeze';
import { deepFreeze, RecursiveReadonly } from '../../utils/deep_freeze';
import { MixedApp } from '../application_service';
import { mergeCapabilities } from './merge_capabilities';
import { InjectedMetadataStart } from '../../injected_metadata';
import { BasePathStart } from '../../base_path';
interface StartDeps {
apps: ReadonlyArray<MixedApp>;
basePath: BasePathStart;
injectedMetadata: InjectedMetadataStart;
}
@ -54,7 +60,13 @@ export interface CapabilitiesStart {
/**
* Gets the read-only capabilities.
*/
getCapabilities: () => Capabilities;
capabilities: RecursiveReadonly<Capabilities>;
/**
* Apps available based on the current capabilities. Should be used
* to show navigation links and make routing decisions.
*/
availableApps: ReadonlyArray<MixedApp>;
}
/** @internal */
@ -63,10 +75,44 @@ export interface CapabilitiesStart {
* Service that is responsible for UI Capabilities.
*/
export class CapabilitiesService {
public start({ injectedMetadata }: StartDeps): CapabilitiesStart {
public async start({ apps, basePath, injectedMetadata }: StartDeps): Promise<CapabilitiesStart> {
const mergedCapabilities = mergeCapabilities(
// Custom capabilites for new platform apps
...apps.filter(app => app.capabilities).map(app => app.capabilities!),
// Generate navLink capabilities for all apps
...apps.map(app => ({ navLinks: { [app.id]: true } }))
);
// NOTE: should replace `fetch` with browser HTTP service once it exists
const res = await fetch(basePath.addToPath('/api/capabilities'), {
method: 'POST',
body: JSON.stringify({ capabilities: mergedCapabilities }),
headers: {
'kbn-xsrf': 'xxx',
},
credentials: 'same-origin',
});
if (res.status === 401) {
return {
availableApps: [],
capabilities: deepFreeze({
navLinks: {},
management: {},
catalogue: {},
}),
};
} else if (res.status !== 200) {
throw new Error(`Capabilities check failed.`);
}
const body = await res.json();
const capabilities = deepFreeze(body.capabilities as Capabilities);
const availableApps = apps.filter(app => capabilities.navLinks[app.id]);
return {
getCapabilities: () =>
deepFreeze(injectedMetadata.getInjectedVar('uiCapabilities') as Capabilities),
availableApps,
capabilities,
};
}
}

View file

@ -0,0 +1,39 @@
/*
* 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 { Capabilities } from './capabilities_service';
export const mergeCapabilities = (...sources: Array<Partial<Capabilities>>) =>
sources.reduce(
(capabilities, source) => {
Object.entries(source).forEach(([key, value]) => {
capabilities[key] = {
...value,
...capabilities[key],
};
});
return capabilities;
},
{
navLinks: {},
management: {},
catalogue: {},
}
);

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
export { ApplicationService, ApplicationSetup, ApplicationStart } from './application_service';
export { Capabilities } from './capabilities';

View file

@ -30,16 +30,21 @@ const createSetupContractMock = () => {
return setupContract;
};
const createStartContractMock = createSetupContractMock;
type BasePathServiceContract = PublicMethodsOf<BasePathService>;
const createMock = () => {
const mocked: jest.Mocked<BasePathServiceContract> = {
setup: jest.fn(),
start: jest.fn(),
};
mocked.setup.mockReturnValue(createSetupContractMock());
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const basePathServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -18,7 +18,7 @@
*/
/* eslint-disable max-classes-per-file */
import { InjectedMetadataSetup } from '../injected_metadata';
import { InjectedMetadataSetup, InjectedMetadataStart } from '../injected_metadata';
import { modifyUrl } from '../utils';
/**
@ -49,13 +49,24 @@ export interface BasePathSetup {
removeFromPath(path: string): string;
}
interface BasePathDeps {
/**
* Provides access to the 'server.basePath' configuration option in kibana.yml
*
* @public
*/
export type BasePathStart = BasePathSetup;
interface SetupDeps {
injectedMetadata: InjectedMetadataSetup;
}
interface StartDeps {
injectedMetadata: InjectedMetadataStart;
}
/** @internal */
export class BasePathService {
public setup({ injectedMetadata }: BasePathDeps) {
public setup({ injectedMetadata }: SetupDeps) {
const basePath = injectedMetadata.getBasePath() || '';
const basePathSetup: BasePathSetup = {
@ -86,4 +97,8 @@ export class BasePathService {
return basePathSetup;
}
public start({ injectedMetadata }: StartDeps) {
return this.setup({ injectedMetadata });
}
}

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { BasePathService, BasePathSetup } from './base_path_service';
export { BasePathService, BasePathSetup, BasePathStart } from './base_path_service';

View file

@ -1,60 +0,0 @@
/*
* 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 { InjectedMetadataService } from '../injected_metadata';
import { CapabilitiesService } from './capabilities_service';
describe('#start', () => {
it('returns a service with getCapabilities', () => {
const injectedMetadata = new InjectedMetadataService({
injectedMetadata: {
vars: {
uiCapabilities: {
foo: 'bar',
bar: 'baz',
},
},
} as any,
});
const service = new CapabilitiesService();
const startContract = service.start({ injectedMetadata: injectedMetadata.start() });
expect(startContract.getCapabilities()).toEqual({
foo: 'bar',
bar: 'baz',
});
});
it(`does not allow Capabilities to be modified`, () => {
const injectedMetadata = new InjectedMetadataService({
injectedMetadata: {
vars: {
uiCapabilities: {
foo: 'bar',
bar: 'baz',
},
},
} as any,
});
const service = new CapabilitiesService();
const startContract = service.start({ injectedMetadata: injectedMetadata.start() });
const capabilities = startContract.getCapabilities();
// @ts-ignore TypeScript knows this shouldn't be possible
expect(() => (capabilities.foo = 'foo')).toThrowError();
});
});

View file

@ -18,7 +18,7 @@
*/
import { basePathServiceMock } from './base_path/base_path_service.mock';
import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock';
import { applicationServiceMock } from './application/application_service.mock';
import { chromeServiceMock } from './chrome/chrome_service.mock';
import { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock';
import { httpServiceMock } from './http/http_service.mock';
@ -106,10 +106,10 @@ jest.doMock('./plugins', () => ({
PluginsService: PluginsServiceConstructor,
}));
export const MockCapabilitiesService = capabilitiesServiceMock.create();
export const CapabilitiesServiceConstructor = jest
export const MockApplicationService = applicationServiceMock.create();
export const ApplicationServiceConstructor = jest
.fn()
.mockImplementation(() => MockCapabilitiesService);
jest.doMock('./capabilities', () => ({
CapabilitiesService: CapabilitiesServiceConstructor,
.mockImplementation(() => MockApplicationService);
jest.doMock('./application', () => ({
ApplicationService: ApplicationServiceConstructor,
}));

View file

@ -39,7 +39,7 @@ import {
NotificationServiceConstructor,
OverlayServiceConstructor,
UiSettingsServiceConstructor,
MockCapabilitiesService,
MockApplicationService,
} from './core_system.test.mocks';
import { CoreSystem } from './core_system';
@ -155,6 +155,11 @@ describe('#setup()', () => {
return core.setup();
}
it('calls application#setup()', async () => {
await setupCore();
expect(MockApplicationService.setup).toHaveBeenCalledTimes(1);
});
it('calls injectedMetadata#setup()', async () => {
await setupCore();
expect(MockInjectedMetadataService.setup).toHaveBeenCalledTimes(1);
@ -219,9 +224,9 @@ describe('#start()', () => {
expect(root.innerHTML).toBe('<div></div><div></div><div></div>');
});
it('calls capabilities#start()', async () => {
it('calls application#start()', async () => {
await startCore();
expect(MockCapabilitiesService.start).toHaveBeenCalledTimes(1);
expect(MockApplicationService.start).toHaveBeenCalledTimes(1);
});
it('calls i18n#start()', async () => {

View file

@ -21,7 +21,6 @@ import './core.css';
import { CoreSetup, CoreStart } from '.';
import { BasePathService } from './base_path';
import { CapabilitiesService } from './capabilities';
import { ChromeService } from './chrome';
import { FatalErrorsService } from './fatal_errors';
import { HttpService } from './http';
@ -32,6 +31,7 @@ import { NotificationsService } from './notifications';
import { OverlayService } from './overlays';
import { PluginsService } from './plugins';
import { UiSettingsService } from './ui_settings';
import { ApplicationService } from './application';
interface Params {
rootDomElement: HTMLElement;
@ -63,9 +63,9 @@ export class CoreSystem {
private readonly basePath: BasePathService;
private readonly chrome: ChromeService;
private readonly i18n: I18nService;
private readonly capabilities: CapabilitiesService;
private readonly overlay: OverlayService;
private readonly plugins: PluginsService;
private readonly application: ApplicationService;
private readonly rootDomElement: HTMLElement;
private readonly overlayTargetDomElement: HTMLDivElement;
@ -83,8 +83,6 @@ export class CoreSystem {
this.i18n = new I18nService();
this.capabilities = new CapabilitiesService();
this.injectedMetadata = new InjectedMetadataService({
injectedMetadata,
});
@ -103,6 +101,7 @@ export class CoreSystem {
this.uiSettings = new UiSettingsService();
this.overlayTargetDomElement = document.createElement('div');
this.overlay = new OverlayService(this.overlayTargetDomElement);
this.application = new ApplicationService();
this.chrome = new ChromeService({ browserSupportsCsp });
const core: CoreContext = {};
@ -127,12 +126,14 @@ export class CoreSystem {
basePath,
});
const notifications = this.notifications.setup({ uiSettings });
const application = this.application.setup();
const chrome = this.chrome.setup({
injectedMetadata,
notifications,
});
const core: CoreSetup = {
application,
basePath,
chrome,
fatalErrors,
@ -155,27 +156,30 @@ export class CoreSystem {
public async start() {
try {
// ensure the rootDomElement is empty
this.rootDomElement.textContent = '';
this.rootDomElement.classList.add('coreSystemRootDomElement');
const injectedMetadata = await this.injectedMetadata.start();
const basePath = await this.basePath.start({ injectedMetadata });
const i18n = await this.i18n.start();
const application = await this.application.start({ basePath, injectedMetadata });
const notificationsTargetDomElement = document.createElement('div');
const legacyPlatformTargetDomElement = document.createElement('div');
// ensure the rootDomElement is empty
this.rootDomElement.textContent = '';
this.rootDomElement.classList.add('coreSystemRootDomElement');
this.rootDomElement.appendChild(notificationsTargetDomElement);
this.rootDomElement.appendChild(legacyPlatformTargetDomElement);
this.rootDomElement.appendChild(this.overlayTargetDomElement);
const injectedMetadata = this.injectedMetadata.start();
const i18n = this.i18n.start();
const capabilities = this.capabilities.start({ injectedMetadata });
const notifications = this.notifications.start({
const notifications = await this.notifications.start({
i18n,
targetDomElement: notificationsTargetDomElement,
});
const overlays = this.overlay.start({ i18n });
const overlays = await this.overlay.start({ i18n });
const core: CoreStart = {
capabilities,
application,
basePath,
i18n,
injectedMetadata,
notifications,

View file

@ -17,8 +17,7 @@
* under the License.
*/
import { BasePathSetup } from './base_path';
import { Capabilities, CapabilitiesStart } from './capabilities';
import { BasePathSetup, BasePathStart } from './base_path';
import {
ChromeBadge,
ChromeBrand,
@ -33,6 +32,7 @@ import {
InjectedMetadataParams,
InjectedMetadataSetup,
InjectedMetadataStart,
LegacyNavLink,
} from './injected_metadata';
import {
NotificationsSetup,
@ -44,6 +44,7 @@ import {
import { FlyoutRef, OverlayStart } from './overlays';
import { Plugin, PluginInitializer, PluginInitializerContext, PluginSetupContext } from './plugins';
import { UiSettingsClient, UiSettingsSetup, UiSettingsState } from './ui_settings';
import { ApplicationSetup, Capabilities, ApplicationStart } from './application';
/** @interal */
export { CoreContext, CoreSystem } from './core_system';
@ -58,6 +59,8 @@ export { CoreContext, CoreSystem } from './core_system';
* https://github.com/Microsoft/web-build-tools/issues/1237
*/
export interface CoreSetup {
/** {@link ApplicationSetup} */
application: ApplicationSetup;
/** {@link I18nSetup} */
i18n: I18nSetup;
/** {@link InjectedMetadataSetup} */
@ -77,8 +80,10 @@ export interface CoreSetup {
}
export interface CoreStart {
/** {@link CapabilitiesStart} */
capabilities: CapabilitiesStart;
/** {@link ApplicationStart} */
application: ApplicationStart;
/** {@link BasePathStart} */
basePath: BasePathStart;
/** {@link I18nStart} */
i18n: I18nStart;
/** {@link InjectedMetadataStart} */
@ -90,11 +95,13 @@ export interface CoreStart {
}
export {
ApplicationSetup,
ApplicationStart,
BasePathSetup,
BasePathStart,
HttpSetup,
FatalErrorsSetup,
Capabilities,
CapabilitiesStart,
ChromeSetup,
ChromeBadge,
ChromeBreadcrumb,
@ -105,6 +112,7 @@ export {
InjectedMetadataSetup,
InjectedMetadataStart,
InjectedMetadataParams,
LegacyNavLink,
Plugin,
PluginInitializer,
PluginInitializerContext,

View file

@ -22,4 +22,5 @@ export {
InjectedMetadataParams,
InjectedMetadataSetup,
InjectedMetadataStart,
LegacyNavLink,
} from './injected_metadata_service';

View file

@ -31,6 +31,7 @@ const createSetupContractMock = () => {
setupContract.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true });
setupContract.getKibanaVersion.mockReturnValue('kibanaVersion');
setupContract.getLegacyMetadata.mockReturnValue({
nav: [],
uiSettings: {
defaults: { legacyInjectedUiSettingDefaults: true },
user: { legacyInjectedUiSettingUserValues: true },

View file

@ -22,6 +22,16 @@ import { DiscoveredPlugin, PluginName } from '../../server';
import { UiSettingsState } from '../ui_settings';
import { deepFreeze } from '../utils/deep_freeze';
/** @public */
export interface LegacyNavLink {
id: string;
title: string;
order: number;
url: string;
icon?: string;
euiIconType?: string;
}
/** @internal */
export interface InjectedMetadataParams {
injectedMetadata: {
@ -42,7 +52,7 @@ export interface InjectedMetadataParams {
app: unknown;
translations: unknown;
bundleId: string;
nav: unknown;
nav: LegacyNavLink[];
version: string;
branch: string;
buildNum: number;
@ -140,7 +150,7 @@ export interface InjectedMetadataSetup {
app: unknown;
translations: unknown;
bundleId: string;
nav: unknown;
nav: LegacyNavLink[];
version: string;
branch: string;
buildNum: number;

View file

@ -150,7 +150,6 @@ jest.mock('ui/chrome/services/global_nav_state', () => {
});
import { basePathServiceMock } from '../base_path/base_path_service.mock';
import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock';
import { chromeServiceMock } from '../chrome/chrome_service.mock';
import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock';
import { httpServiceMock } from '../http/http_service.mock';
@ -160,7 +159,9 @@ import { notificationServiceMock } from '../notifications/notifications_service.
import { overlayServiceMock } from '../overlays/overlay_service.mock';
import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock';
import { LegacyPlatformService } from './legacy_service';
import { applicationServiceMock } from '../application/application_service.mock';
const applicationSetup = applicationServiceMock.createSetupContract();
const basePathSetup = basePathServiceMock.createSetupContract();
const chromeSetup = chromeServiceMock.createSetupContract();
const fatalErrorsSetup = fatalErrorsServiceMock.createSetupContract();
@ -178,6 +179,7 @@ const defaultParams = {
const defaultSetupDeps = {
core: {
application: applicationSetup,
i18n: i18nSetup,
fatalErrors: fatalErrorsSetup,
injectedMetadata: injectedMetadataSetup,
@ -189,7 +191,8 @@ const defaultSetupDeps = {
},
};
const capabilitiesStart = capabilitiesServiceMock.createStartContract();
const applicationStart = applicationServiceMock.createStartContract();
const basePathStart = basePathServiceMock.createStartContract();
const i18nStart = i18nServiceMock.createStartContract();
const injectedMetadataStart = injectedMetadataServiceMock.createStartContract();
const notificationsStart = notificationServiceMock.createStartContract();
@ -197,7 +200,8 @@ const overlayStart = overlayServiceMock.createStartContract();
const defaultStartDeps = {
core: {
capabilities: capabilitiesStart,
application: applicationStart,
basePath: basePathStart,
i18n: i18nStart,
injectedMetadata: injectedMetadataStart,
notifications: notificationsStart,
@ -208,7 +212,6 @@ const defaultStartDeps = {
afterEach(() => {
jest.clearAllMocks();
injectedMetadataSetup.getLegacyMetadata.mockReset();
jest.resetModules();
mockLoadOrder.length = 0;
});
@ -216,8 +219,8 @@ afterEach(() => {
describe('#setup()', () => {
describe('default', () => {
it('passes legacy metadata from injectedVars to ui/metadata', () => {
const legacyMetadata = { isLegacyMetadata: true };
injectedMetadataSetup.getLegacyMetadata.mockReturnValue(legacyMetadata as any);
const legacyMetadata = { nav: [], isLegacyMetadata: true };
injectedMetadataSetup.getLegacyMetadata.mockReturnValueOnce(legacyMetadata as any);
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
@ -404,7 +407,7 @@ describe('#start()', () => {
legacyPlatform.start(defaultStartDeps);
expect(mockUICapabilitiesInit).toHaveBeenCalledTimes(1);
expect(mockUICapabilitiesInit).toHaveBeenCalledWith(capabilitiesStart);
expect(mockUICapabilitiesInit).toHaveBeenCalledWith(applicationStart.capabilities);
});
describe('useLegacyTestHarness = false', () => {

View file

@ -52,8 +52,9 @@ export class LegacyPlatformService {
constructor(private readonly params: LegacyPlatformParams) {}
public async setup({ core }: SetupDeps) {
public setup({ core }: SetupDeps) {
const {
application,
i18n,
injectedMetadata,
fatalErrors,
@ -81,6 +82,17 @@ export class LegacyPlatformService {
require('ui/chrome/api/breadcrumbs').__newPlatformSetup__(chrome);
require('ui/chrome/services/global_nav_state').__newPlatformSetup__(chrome);
injectedMetadata.getLegacyMetadata().nav.forEach((navLink: any) =>
application.registerLegacyApp({
id: navLink.id,
order: navLink.order,
title: navLink.title,
euiIconType: navLink.euiIconType,
icon: navLink.icon,
appUrl: navLink.url,
})
);
// Load the bootstrap module before loading the legacy platform files so that
// the bootstrap module can modify the environment a bit first
this.bootstrapModule = this.loadBootstrapModule();
@ -97,7 +109,7 @@ export class LegacyPlatformService {
this.targetDomElement = targetDomElement;
require('ui/new_platform').__newPlatformStart__(core);
require('ui/capabilities').__newPlatformStart__(core.capabilities);
require('ui/capabilities').__newPlatformStart__(core.application.capabilities);
this.bootstrapModule.bootstrap(this.targetDomElement);
}

View file

@ -18,7 +18,7 @@
*/
import { DiscoveredPlugin } from '../../server';
import { BasePathSetup } from '../base_path';
import { BasePathSetup, BasePathStart } from '../base_path';
import { ChromeSetup } from '../chrome';
import { CoreContext } from '../core_system';
import { FatalErrorsSetup } from '../fatal_errors';
@ -27,8 +27,9 @@ import { NotificationsSetup, NotificationsStart } from '../notifications';
import { UiSettingsSetup } from '../ui_settings';
import { PluginWrapper } from './plugin';
import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service';
import { CapabilitiesStart } from '../capabilities';
import { OverlayStart } from '../overlays';
import { ApplicationStart } from '../application';
import { HttpSetup } from '../http';
/**
* The available core services passed to a `PluginInitializer`
@ -47,6 +48,7 @@ export interface PluginSetupContext {
basePath: BasePathSetup;
chrome: ChromeSetup;
fatalErrors: FatalErrorsSetup;
http: HttpSetup;
i18n: I18nSetup;
notifications: NotificationsSetup;
uiSettings: UiSettingsSetup;
@ -58,7 +60,8 @@ export interface PluginSetupContext {
* @public
*/
export interface PluginStartContext {
capabilities: CapabilitiesStart;
application: Pick<ApplicationStart, 'capabilities'>;
basePath: BasePathStart;
i18n: I18nStart;
notifications: NotificationsStart;
overlays: OverlayStart;
@ -95,6 +98,7 @@ export function createPluginSetupContext<TSetup, TStart, TPluginsSetup, TPlugins
plugin: PluginWrapper<TSetup, TStart, TPluginsSetup, TPluginsStart>
): PluginSetupContext {
return {
http: deps.http,
basePath: deps.basePath,
chrome: deps.chrome,
fatalErrors: deps.fatalErrors,
@ -120,7 +124,10 @@ export function createPluginStartContext<TSetup, TStart, TPluginsSetup, TPlugins
plugin: PluginWrapper<TSetup, TStart, TPluginsSetup, TPluginsStart>
): PluginStartContext {
return {
capabilities: deps.capabilities,
application: {
capabilities: deps.application.capabilities,
},
basePath: deps.basePath,
i18n: deps.i18n,
notifications: deps.notifications,
overlays: deps.overlays,

View file

@ -33,7 +33,7 @@ import {
PluginsServiceSetupDeps,
} from './plugins_service';
import { notificationServiceMock } from '../notifications/notifications_service.mock';
import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock';
import { applicationServiceMock } from '../application/application_service.mock';
import { i18nServiceMock } from '../i18n/i18n_service.mock';
import { overlayServiceMock } from '../overlays/overlay_service.mock';
import { PluginStartContext, PluginSetupContext } from './plugin_context';
@ -42,6 +42,8 @@ import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.moc
import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock';
import { basePathServiceMock } from '../base_path/base_path_service.mock';
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
import { UiSettingsClient } from '../ui_settings';
import { httpServiceMock } from '../http/http_service.mock';
export let mockPluginInitializers: Map<PluginName, MockedPluginInitializer>;
@ -59,6 +61,7 @@ let mockStartContext: DeeplyMocked<PluginStartContext>;
beforeEach(() => {
mockSetupDeps = {
application: applicationServiceMock.createSetupContract(),
injectedMetadata: (function() {
const metadata = injectedMetadataServiceMock.createSetupContract();
metadata.getPlugins.mockReturnValue([
@ -78,19 +81,26 @@ beforeEach(() => {
})(),
chrome: chromeServiceMock.createSetupContract(),
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
http: httpServiceMock.createSetupContract(),
i18n: i18nServiceMock.createSetupContract(),
notifications: notificationServiceMock.createSetupContract(),
uiSettings: uiSettingsServiceMock.createSetupContract(),
} as any;
mockSetupContext = omit(mockSetupDeps, 'injectedMetadata');
uiSettings: uiSettingsServiceMock.createSetupContract() as jest.Mocked<UiSettingsClient>,
};
mockSetupContext = omit(mockSetupDeps, 'application', 'injectedMetadata');
mockStartDeps = {
capabilities: capabilitiesServiceMock.createStartContract(),
application: applicationServiceMock.createStartContract(),
basePath: basePathServiceMock.createStartContract(),
i18n: i18nServiceMock.createStartContract(),
injectedMetadata: injectedMetadataServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
overlays: overlayServiceMock.createStartContract(),
};
mockStartContext = omit(mockStartDeps, 'injectedMetadata');
mockStartContext = {
...omit(mockStartDeps, 'injectedMetadata'),
application: {
capabilities: mockStartDeps.application.capabilities,
},
};
// Reset these for each test.
mockPluginInitializers = new Map<PluginName, MockedPluginInitializer>(([

View file

@ -7,10 +7,35 @@
import * as CSS from 'csstype';
import { default } from 'react';
import { IconType } from '@elastic/eui';
import { Observable } from 'rxjs';
import * as PropTypes from 'prop-types';
import * as Rx from 'rxjs';
import { Toast } from '@elastic/eui';
// @public (undocumented)
export interface ApplicationSetup {
// Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts
registerApp(app: App): void;
// Warning: (ae-forgotten-export) The symbol "LegacyApp" needs to be exported by the entry point index.d.ts
//
// @internal
registerLegacyApp(app: LegacyApp): void;
}
// Warning: (ae-missing-release-tag) "ApplicationStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface ApplicationStart {
// Warning: (ae-forgotten-export) The symbol "CapabilitiesStart" needs to be exported by the entry point index.d.ts
//
// (undocumented)
availableApps: CapabilitiesStart['availableApps'];
// (undocumented)
capabilities: CapabilitiesStart['capabilities'];
// (undocumented)
mount: (mountHandler: Function) => void;
}
// @public
export interface BasePathSetup {
addToPath(path: string): string;
@ -18,6 +43,9 @@ export interface BasePathSetup {
removeFromPath(path: string): string;
}
// @public
export type BasePathStart = BasePathSetup;
// @public
export interface Capabilities {
[key: string]: Record<string, boolean | Record<string, boolean>>;
@ -28,11 +56,6 @@ export interface Capabilities {
navLinks: Record<string, boolean>;
}
// @public
export interface CapabilitiesStart {
getCapabilities: () => Capabilities;
}
// @public (undocumented)
export interface ChromeBadge {
// (undocumented)
@ -75,6 +98,8 @@ export interface CoreContext {
// @public
export interface CoreSetup {
// (undocumented)
application: ApplicationSetup;
// (undocumented)
basePath: BasePathSetup;
// (undocumented)
@ -98,7 +123,9 @@ export interface CoreSetup {
// @public (undocumented)
export interface CoreStart {
// (undocumented)
capabilities: CapabilitiesStart;
application: ApplicationStart;
// (undocumented)
basePath: BasePathStart;
// (undocumented)
i18n: I18nStart;
// (undocumented)
@ -175,7 +202,7 @@ export interface InjectedMetadataParams {
app: unknown;
translations: unknown;
bundleId: string;
nav: unknown;
nav: LegacyNavLink[];
version: string;
branch: string;
buildNum: number;
@ -212,7 +239,7 @@ export interface InjectedMetadataSetup {
app: unknown;
translations: unknown;
bundleId: string;
nav: unknown;
nav: LegacyNavLink[];
version: string;
branch: string;
buildNum: number;
@ -234,6 +261,22 @@ export interface InjectedMetadataSetup {
// @public (undocumented)
export type InjectedMetadataStart = InjectedMetadataSetup;
// @public (undocumented)
export interface LegacyNavLink {
// (undocumented)
euiIconType?: string;
// (undocumented)
icon?: string;
// (undocumented)
id: string;
// (undocumented)
order: number;
// (undocumented)
title: string;
// (undocumented)
url: string;
}
// @public (undocumented)
export interface NotificationsSetup {
// (undocumented)
@ -282,6 +325,8 @@ export interface PluginSetupContext {
// (undocumented)
fatalErrors: FatalErrorsSetup;
// (undocumented)
http: HttpSetup;
// (undocumented)
i18n: I18nSetup;
// (undocumented)
notifications: NotificationsSetup;
@ -356,8 +401,8 @@ export interface UiSettingsState {
// Warnings were encountered during analysis:
//
// src/core/public/injected_metadata/injected_metadata_service.ts:38:7 - (ae-forgotten-export) The symbol "PluginName" needs to be exported by the entry point index.d.ts
// src/core/public/injected_metadata/injected_metadata_service.ts:39:7 - (ae-forgotten-export) The symbol "DiscoveredPlugin" needs to be exported by the entry point index.d.ts
// src/core/public/injected_metadata/injected_metadata_service.ts:48:7 - (ae-forgotten-export) The symbol "PluginName" needs to be exported by the entry point index.d.ts
// src/core/public/injected_metadata/injected_metadata_service.ts:49:7 - (ae-forgotten-export) The symbol "DiscoveredPlugin" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)

View file

@ -23,7 +23,7 @@ type Freezable = { [k: string]: any } | any[];
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface RecursiveReadonlyArray<T> extends Array<RecursiveReadonly<T>> {}
type RecursiveReadonly<T> = T extends any[]
export type RecursiveReadonly<T> = T extends any[]
? RecursiveReadonlyArray<T[number]>
: T extends object
? Readonly<{ [K in keyof T]: RecursiveReadonly<T[K]> }>

View file

@ -96,6 +96,15 @@ export default function (kibana) {
];
},
uiCapabilities() {
return {
dev_tools: {
show: true,
save: true,
},
};
},
async init(server, options) {
server.expose('addExtensionSpecFilePath', addExtensionSpecFilePath);
if (options.ssl && options.ssl.verify) {
@ -111,12 +120,6 @@ export default function (kibana) {
elasticsearchUrl: url.format(
Object.assign(url.parse(head(legacyEsConfig.hosts)), { auth: false })
),
uiCapabilities: {
dev_tools: {
show: true,
save: true,
},
}
};
server.route(createProxyRoute({

View file

@ -228,62 +228,9 @@ export default function (kibana) {
},
injectDefaultVars(server, options) {
const { savedObjects } = server;
return {
kbnIndex: options.index,
kbnBaseUrl,
uiCapabilities: {
discover: {
show: true,
createShortUrl: true,
save: true,
},
visualize: {
show: true,
createShortUrl: true,
delete: true,
save: true,
},
dashboard: {
createNew: true,
show: true,
showWriteControls: true,
},
catalogue: {
discover: true,
dashboard: true,
visualize: true,
console: true,
advanced_settings: true,
index_patterns: true,
},
advancedSettings: {
show: true,
save: true
},
indexPatterns: {
save: true,
},
savedObjectsManagement: savedObjects.types.reduce((acc, type) => ({
...acc,
[type]: {
delete: true,
edit: true,
read: true,
}
}), {}),
management: {
/*
* Management settings correspond to management section/link ids, and should not be changed
* without also updating those definitions.
*/
kibana: {
settings: true,
index_patterns: true,
},
}
}
};
},
@ -293,6 +240,62 @@ export default function (kibana) {
migrations,
},
uiCapabilities: async function (server) {
const { savedObjects } = server;
return {
discover: {
show: true,
createShortUrl: true,
save: true,
},
visualize: {
show: true,
createShortUrl: true,
delete: true,
save: true,
},
dashboard: {
createNew: true,
show: true,
showWriteControls: true,
},
catalogue: {
discover: true,
dashboard: true,
visualize: true,
console: true,
advanced_settings: true,
index_patterns: true,
},
advancedSettings: {
show: true,
save: true
},
indexPatterns: {
save: true,
},
savedObjectsManagement: savedObjects.types.reduce((acc, type) => ({
...acc,
[type]: {
delete: true,
edit: true,
read: true,
}
}), {}),
management: {
/*
* Management settings correspond to management section/link ids, and should not be changed
* without also updating those definitions.
*/
kibana: {
settings: true,
index_patterns: true,
},
}
};
},
preInit: async function (server) {
try {
// Create the data directory (recursively, if the a parent dir doesn't exist).

View file

@ -28,6 +28,7 @@ import { render, unmountComponentAtNode } from 'react-dom';
import { ObjectsTable } from './components/objects_table';
import { I18nContext } from 'ui/i18n';
import { get } from 'lodash';
import { getNewPlatform } from 'ui/new_platform';
import { getIndexBreadcrumbs } from './breadcrumbs';
@ -39,10 +40,10 @@ function updateObjectsTable($scope, $injector) {
const $http = $injector.get('$http');
const kbnUrl = $injector.get('kbnUrl');
const config = $injector.get('config');
const uiCapabilites = chrome.getInjected('uiCapabilities');
const savedObjectsClient = Private(SavedObjectsClientProvider);
const services = savedObjectManagementRegistry.all().map(obj => $injector.get(obj.service));
const uiCapabilites = getNewPlatform().start.core.application.capabilities;
$scope.$$postDigest(() => {
const node = document.getElementById(REACT_OBJECTS_TABLE_DOM_ELEMENT_ID);

View file

@ -35,8 +35,47 @@ import 'custom-event-polyfill';
import 'whatwg-fetch';
import 'abortcontroller-polyfill';
import 'childnode-remove-polyfill';
import sinon from 'sinon';
import { CoreSystem } from '__kibanaCore__'
import { CoreSystem } from '__kibanaCore__';
// Fake uiCapabilities returned to Core in browser tests
const uiCapabilities = {
navLinks: {
myLink: true,
notMyLink: true,
},
discover: {
showWriteControls: true
},
visualize: {
save: true
},
dashboard: {
showWriteControls: true
},
timelion: {
save: true
},
};
// Stub fetch for CoreSystem calls.
const fetchStub = sinon.stub(window, 'fetch');
fetchStub.callsFake((url, options) => {
if (url !== '/api/capabilities') {
console.warn('Stubbed window.fetch does not support this request.');
return Promise.resolve(new window.Response('Resource not found', { status: 404 }));
}
return Promise.resolve(
new window.Response(
JSON.stringify({ capabilities: uiCapabilities })),
{
status: 200,
headers: { 'Content-Type': 'application/json' }
}
);
});
// render the core system in a child of the body as the default children of the body
// in the browser tests are needed for mocha and other test components to work
@ -48,6 +87,7 @@ const coreSystem = new CoreSystem({
version: '1.2.3',
buildNumber: 1234,
legacyMetadata: {
nav: [],
version: '1.2.3',
buildNum: 1234,
devMode: true,
@ -85,24 +125,6 @@ const coreSystem = new CoreSystem({
enabled: true,
enableExternalUrls: true
},
uiCapabilities: {
navLinks: {
myLink: true,
notMyLink: true,
},
discover: {
showWriteControls: true
},
visualize: {
save: true
},
dashboard: {
showWriteControls: true
},
timelion: {
save: true
},
},
interpreterConfig: {
enableInVisualize: true
}

View file

@ -37,6 +37,14 @@ export default function (kibana) {
}).default();
},
uiCapabilities() {
return {
timelion: {
save: true,
}
};
},
uiExports: {
app: {
title: 'Timelion',
@ -54,11 +62,6 @@ export default function (kibana) {
injectDefaultVars(server) {
return {
timelionUiEnabled: server.config().get('timelion.ui.enabled'),
uiCapabilities: {
timelion: {
save: true,
}
}
};
},
visTypes: [

View file

@ -56,6 +56,7 @@ export class PluginSpec {
version,
kibanaVersion,
uiExports,
uiCapabilities,
publicDir,
configPrefix,
config,
@ -74,6 +75,7 @@ export class PluginSpec {
this._publicDir = publicDir;
this._uiExports = uiExports;
this._uiCapabilities = uiCapabilities;
this._configPrefix = configPrefix;
this._configSchemaProvider = config;
@ -170,6 +172,10 @@ export class PluginSpec {
return this._uiExports;
}
getUiCapabilitiesProvider() {
return this._uiCapabilities;
}
getPreInitHandler() {
return this._preInit;
}

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { Server } from '../../server/kbn_server';
import { Capabilities } from '../../../core/public';
export type InitPluginFunction = (server: Server) => void;
export interface UiExports {
@ -29,6 +30,7 @@ export interface PluginSpecOptions {
require: string[];
publicDir: string;
uiExports?: UiExports;
uiCapabilities?: Capabilities;
init: InitPluginFunction;
config: any;
}

View file

@ -0,0 +1,23 @@
/*
* 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.
*/
export const mockRegisterCapabilitiesRoute = jest.fn();
jest.mock('./capabilities_route', () => ({
registerCapabilitiesRoute: mockRegisterCapabilitiesRoute,
}));

View file

@ -0,0 +1,88 @@
/*
* 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 { Server } from 'hapi';
import KbnServer from '../kbn_server';
import { mockRegisterCapabilitiesRoute } from './capabilities_mixin.test.mocks';
import { capabilitiesMixin } from './capabilities_mixin';
describe('capabilitiesMixin', () => {
const getKbnServer = (pluginSpecs: any[] = []) => {
return {
afterPluginsInit: (callback: () => void) => callback(),
pluginSpecs,
} as KbnServer;
};
let server: Server;
beforeEach(() => {
server = new Server();
});
afterEach(() => {
mockRegisterCapabilitiesRoute.mockClear();
});
it('calls registerCapabilitiesRoute with merged uiCapabilitiesProviers', async () => {
const kbnServer = getKbnServer([
{
getUiCapabilitiesProvider: () => () => ({
app1: { read: true },
management: { section1: { feature1: true } },
}),
},
{
getUiCapabilitiesProvider: () => () => ({
app2: { write: true },
catalogue: { feature3: true },
management: { section2: { feature2: true } },
}),
},
]);
await capabilitiesMixin(kbnServer, server);
expect(mockRegisterCapabilitiesRoute).toHaveBeenCalledWith(
server,
{
app1: { read: true },
app2: { write: true },
catalogue: { feature3: true },
management: {
section1: { feature1: true },
section2: { feature2: true },
},
navLinks: {},
},
[]
);
});
it('exposes server#registerCapabilitiesModifier for providing modifiers to the route', async () => {
const kbnServer = getKbnServer();
await capabilitiesMixin(kbnServer, server);
const mockModifier1 = jest.fn();
const mockModifier2 = jest.fn();
server.registerCapabilitiesModifier(mockModifier1);
server.registerCapabilitiesModifier(mockModifier2);
expect(mockRegisterCapabilitiesRoute.mock.calls[0][2]).toEqual([mockModifier1, mockModifier2]);
});
});

View file

@ -0,0 +1,53 @@
/*
* 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 { Server, Request } from 'hapi';
import { Capabilities } from '../../../core/public';
import KbnServer from '../kbn_server';
import { registerCapabilitiesRoute } from './capabilities_route';
import { mergeCapabilities } from './merge_capabilities';
export type CapabilitiesModifier = (
request: Request,
uiCapabilities: Capabilities
) => Capabilities | Promise<Capabilities>;
export async function capabilitiesMixin(kbnServer: KbnServer, server: Server) {
const modifiers: CapabilitiesModifier[] = [];
server.decorate('server', 'registerCapabilitiesModifier', (provider: CapabilitiesModifier) => {
modifiers.push(provider);
});
// Some plugin capabilities are derived from data provided by other plugins,
// so we need to wait until after all plugins have been init'd to fetch uiCapabilities.
kbnServer.afterPluginsInit(async () => {
const defaultCapabilities = mergeCapabilities(
...(await Promise.all(
kbnServer.pluginSpecs
.map(spec => spec.getUiCapabilitiesProvider())
.filter(provider => !!provider)
.map(provider => provider(server))
))
);
registerCapabilitiesRoute(server, defaultCapabilities, modifiers);
});
}

View file

@ -0,0 +1,137 @@
/*
* 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 { Server } from 'hapi';
import { registerCapabilitiesRoute } from './capabilities_route';
import { Capabilities } from '../../../core/public';
describe('capabilities api', () => {
const defaultCapabilities = {
catalogue: {
feature1: true,
feature2: true,
},
management: {
section1: {
read: true,
},
section2: {
write: true,
},
},
navLinks: {
app1: true,
app2: true,
},
myApp: {
read: true,
write: true,
kioskMode: true,
},
} as Capabilities;
let server: Server;
beforeEach(() => {
server = new Server();
});
it('returns unmodified uiCapabilities if no modifiers are available', async () => {
registerCapabilitiesRoute(server, defaultCapabilities, []);
const resp = await server.inject({
method: 'POST',
url: '/api/capabilities',
payload: { capabilities: {} },
});
expect(JSON.parse(resp.payload)).toEqual({
capabilities: defaultCapabilities,
});
});
it('merges payload capabilities with defaultCapabilities', async () => {
registerCapabilitiesRoute(server, defaultCapabilities, []);
const resp = await server.inject({
method: 'POST',
url: '/api/capabilities',
payload: { capabilities: { navLinks: { app3: true } } },
});
expect(JSON.parse(resp.payload)).toEqual({
capabilities: {
...defaultCapabilities,
navLinks: {
...defaultCapabilities.navLinks,
app3: true,
},
},
});
});
it('allows a single provider to modify uiCapabilities', async () => {
registerCapabilitiesRoute(server, defaultCapabilities, [
(req, caps) => {
caps.management.section2.write = false;
caps.myApp.write = false;
return caps;
},
]);
const resp = await server.inject({
method: 'POST',
url: '/api/capabilities',
payload: { capabilities: {} },
});
const results = JSON.parse(resp.payload);
expect(results.capabilities.management.section2.write).toBe(false);
expect(results.capabilities.myApp.write).toBe(false);
});
it('allows multiple providers to modify uiCapabilities', async () => {
registerCapabilitiesRoute(server, defaultCapabilities, [
(req, caps) => {
caps.management.section2.write = false;
return caps;
},
(req, caps) => {
caps.myApp.write = false;
return caps;
},
]);
const resp = await server.inject({
method: 'POST',
url: '/api/capabilities',
payload: { capabilities: {} },
});
const results = JSON.parse(resp.payload);
expect(results.capabilities.management.section2.write).toBe(false);
expect(results.capabilities.myApp.write).toBe(false);
});
it('returns an error if any providers fail', async () => {
registerCapabilitiesRoute(server, defaultCapabilities, [
(req, caps) => {
throw new Error(`Couldn't fetch license`);
},
]);
const resp = await server.inject({
method: 'POST',
url: '/api/capabilities',
payload: { capabilities: {} },
});
expect(resp.statusCode).toBe(500);
});
});

View file

@ -0,0 +1,55 @@
/*
* 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 Joi from 'joi';
import { Server } from 'hapi';
import { CapabilitiesModifier } from '.';
import { Capabilities } from '../../../core/public';
import { mergeCapabilities } from './merge_capabilities';
export const registerCapabilitiesRoute = (
server: Server,
defaultCapabilities: Capabilities,
modifiers: CapabilitiesModifier[]
) => {
server.route({
path: '/api/capabilities',
method: 'POST',
options: {
validate: {
payload: Joi.object({
capabilities: Joi.object().required(),
}).required(),
},
},
async handler(request) {
let { capabilities } = request.payload as { capabilities: Capabilities };
capabilities = mergeCapabilities({ ...defaultCapabilities }, capabilities);
for (const provider of modifiers) {
capabilities = await provider(request, capabilities);
}
return {
capabilities,
};
},
});
};

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
export { CapabilitiesModifier, capabilitiesMixin } from './capabilities_mixin';

View file

@ -0,0 +1,39 @@
/*
* 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 { Capabilities } from '../../../core/public';
export const mergeCapabilities = (...sources: Capabilities[]): Capabilities =>
sources.reduce(
(capabilities, source) => {
Object.entries(source).forEach(([key, value]) => {
capabilities[key] = {
...value,
...capabilities[key],
};
});
return capabilities;
},
{
navLinks: {},
management: {},
catalogue: {},
} as Capabilities
);

View file

@ -30,6 +30,7 @@ import {
import { ApmOssPlugin } from '../core_plugins/apm_oss';
import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch';
import { CapabilitiesModifier } from './capabilities';
import { IndexPatternsServiceFactory } from './index_patterns';
import {
SavedObjectsClient,
@ -61,8 +62,13 @@ declare module 'hapi' {
config: () => KibanaConfig;
indexPatternsServiceFactory: IndexPatternsServiceFactory;
savedObjects: SavedObjectsService;
usage: { collectorSet: any };
injectUiAppVars: (pluginName: string, getAppVars: () => { [key: string]: any }) => void;
getHiddenUiAppById(appId: string): UiApp;
registerCapabilitiesModifier: (provider: CapabilitiesModifier) => void;
addScopedTutorialContextFactory: (
scopedTutorialContextFactory: (...args: any[]) => any
) => void;
savedObjectsManagement(): SavedObjectsManagement;
}
@ -103,6 +109,7 @@ export default class KbnServer {
};
public server: Server;
public inject: Server['inject'];
public pluginSpecs: any[];
constructor(settings: any, core: any);
@ -110,6 +117,7 @@ export default class KbnServer {
public mixin(...fns: KbnMixinFunc[]): Promise<void>;
public listen(): Promise<Server>;
public close(): Promise<void>;
public afterPluginsInit(callback: () => void): void;
public applyLoggingConfiguration(settings: any): void;
}

View file

@ -38,6 +38,7 @@ import * as Plugins from './plugins';
import { indexPatternsMixin } from './index_patterns';
import { savedObjectsMixin } from './saved_objects';
import { sampleDataMixin } from './sample_data';
import { capabilitiesMixin } from './capabilities';
import { urlShorteningMixin } from './url_shortening';
import { serverExtensionsMixin } from './server_extensions';
import { uiMixin } from '../ui';
@ -114,6 +115,9 @@ export default class KbnServer {
// setup saved object routes
savedObjectsMixin,
// setup capabilities routes
capabilitiesMixin,
// setup routes for installing/uninstalling sample data sets
sampleDataMixin,

View file

@ -17,18 +17,17 @@
* under the License.
*/
import { Capabilities as UICapabilities, CapabilitiesStart } from '../../../../core/public';
import { Capabilities as UICapabilities } from '../../../../core/public';
export { UICapabilities };
let uiCapabilities: UICapabilities;
let uiCapabilities: UICapabilities = null!;
export function __newPlatformStart__(capabililitiesService: CapabilitiesStart) {
export function __newPlatformStart__(capabilities: UICapabilities) {
if (uiCapabilities) {
throw new Error('ui/capabilities already initialized with new platform apis');
}
uiCapabilities = capabililitiesService.getCapabilities();
uiCapabilities = capabilities;
}
export const capabilities = {

View file

@ -36,6 +36,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { CoreSetup } from 'kibana/public';
import { fatalError } from 'ui/notify';
import { capabilities } from 'ui/capabilities';
// @ts-ignore
import { modifyUrl } from 'ui/url';
// @ts-ignore
@ -65,6 +66,7 @@ export const configureAppAngularModule = (angularModule: IModule) => {
.value('serverName', legacyMetadata.serverName)
.value('sessionId', Date.now())
.value('esUrl', getEsUrl(newPlatform))
.value('uiCapabilities', capabilities.get())
.config(setupCompileProvider(newPlatform))
.config(setupLocationProvider(newPlatform))
.config($setupXsrfRequestInterceptor(newPlatform))

View file

@ -39,19 +39,6 @@ export function uiRenderMixin(kbnServer, server, config) {
);
}
function getInitialDefaultInjectedVars() {
const navLinkSpecs = server.getUiNavLinks();
return {
uiCapabilities: {
navLinks: navLinkSpecs.reduce((acc, navLinkSpec) => ({
...acc,
[navLinkSpec._id]: true
}), {})
}
};
}
let defaultInjectedVars = {};
kbnServer.afterPluginsInit(() => {
const { defaultInjectedVarProviders = [] } = kbnServer.uiExports;
@ -61,7 +48,7 @@ export function uiRenderMixin(kbnServer, server, config) {
allDefaults,
fn(kbnServer.server, pluginSpec.readConfigValue(kbnServer.config, []))
)
), getInitialDefaultInjectedVars());
), {});
});
// render all views from ./views

View file

@ -109,28 +109,6 @@ export const security = (kibana) => new kibana.Plugin({
sessionTimeout: config.get('xpack.security.sessionTimeout'),
enableSpaceAwarePrivileges: config.get('xpack.spaces.enabled'),
};
},
replaceInjectedVars: async function (originalInjectedVars, request, server) {
// if we have a license which doesn't enable security, or we're a legacy user
// we shouldn't disable any ui capabilities
const { authorization } = server.plugins.security;
if (!authorization.mode.useRbacForRequest(request)) {
return originalInjectedVars;
}
const disableUICapabilites = disableUICapabilitesFactory(server, request);
// if we're an anonymous route, we disable all ui capabilities
if (request.route.settings.auth === false) {
return {
...originalInjectedVars,
uiCapabilities: disableUICapabilites.all(originalInjectedVars.uiCapabilities)
};
}
return {
...originalInjectedVars,
uiCapabilities: await disableUICapabilites.usingPrivileges(originalInjectedVars.uiCapabilities)
};
}
},
@ -242,5 +220,22 @@ export const security = (kibana) => new kibana.Plugin({
}
};
});
server.registerCapabilitiesModifier((request, uiCapabilities) => {
// if we have a license which doesn't enable security, or we're a legacy user
// we shouldn't disable any ui capabilities
const { authorization } = server.plugins.security;
if (!authorization.mode.useRbacForRequest(request)) {
return uiCapabilities;
}
const disableUICapabilites = disableUICapabilitesFactory(server, request);
// if we're an anonymous route, we disable all ui capabilities
if (request.route.settings.auth === false) {
return disableUICapabilites.all(uiCapabilities);
}
return disableUICapabilites.usingPrivileges(uiCapabilities);
});
}
});

View file

@ -51,7 +51,7 @@ export function disableUICapabilitesFactory(
throw new Error(`Expected value type of boolean or object, but found ${value}`);
})
);
) as UICapabilities;
};
const usingPrivileges = async (uiCapabilities: UICapabilities) => {
@ -143,7 +143,7 @@ export function disableUICapabilitesFactory(
);
}
);
});
}) as UICapabilities;
};
return {

View file

@ -6,8 +6,9 @@
import JoiNamespace from 'joi';
import { resolve } from 'path';
import { Server } from 'hapi';
import { getConfigSchema, initServerWithKibana, KbnServer } from './server/kibana.index';
import { getConfigSchema, initServerWithKibana } from './server/kibana.index';
const APP_ID = 'siem';
export const APP_NAME = 'SIEM';
@ -43,7 +44,7 @@ export function siem(kibana: any) {
config(Joi: typeof JoiNamespace) {
return getConfigSchema(Joi);
},
init(server: KbnServer) {
init(server: Server) {
initServerWithKibana(server);
},
});

View file

@ -14,13 +14,9 @@ import { createLogger } from './utils/logger';
const APP_ID = 'siem';
export interface KbnServer extends Server {
usage: unknown;
}
export const amMocking = (): boolean => process.env.INGEST_MOCKS === 'true';
export const initServerWithKibana = (kbnServer: KbnServer) => {
export const initServerWithKibana = (kbnServer: Server) => {
// bind is so "this" binds correctly to the logger since hapi server does not auto-bind its methods
const logger = createLogger(kbnServer.log.bind(kbnServer));
logger.info('Plugin initializing');

View file

@ -7,6 +7,7 @@
import { resolve } from 'path';
import { SavedObjectsService } from 'src/legacy/server/saved_objects';
import { Request, Server } from 'hapi';
// @ts-ignore
import { AuditLogger } from '../../server/lib/audit_logger';
// @ts-ignore
@ -43,6 +44,14 @@ export const spaces = (kibana: Record<string, any>) =>
}).default();
},
uiCapabilities() {
return {
spaces: {
manage: true,
},
};
},
uiExports: {
chromeNavControls: ['plugins/spaces/views/nav_control'],
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
@ -74,11 +83,6 @@ export const spaces = (kibana: Record<string, any>) =>
spaces: [],
activeSpace: null,
spaceSelectorURL: getSpaceSelectorUrl(server.config()),
uiCapabilities: {
spaces: {
manage: true,
},
},
};
},
async replaceInjectedVars(
@ -103,20 +107,11 @@ export const spaces = (kibana: Record<string, any>) =>
};
}
if (vars.activeSpace.space) {
const features = server.plugins.xpack_main.getFeatures();
vars.uiCapabilities = toggleUICapabilities(
features,
vars.uiCapabilities,
vars.activeSpace.space
);
}
return vars;
},
},
async init(server: any) {
async init(server: Server) {
const thisPlugin = this;
const xpackMainPlugin = server.plugins.xpack_main;
@ -140,10 +135,10 @@ export const spaces = (kibana: Record<string, any>) =>
);
server.expose('spacesClient', {
getScopedClient: (request: Record<string, any>) => {
getScopedClient: (request: Request) => {
const adminCluster = server.plugins.elasticsearch.getCluster('admin');
const { callWithRequest, callWithInternalUser } = adminCluster;
const callCluster = (...args: any[]) => callWithRequest(request, ...args);
const callCluster = callWithRequest.bind(adminCluster, request);
const { savedObjects } = server;
const internalRepository = savedObjects.getSavedObjectsRepository(callWithInternalUser);
const callWithRequestRepository = savedObjects.getSavedObjectsRepository(callCluster);
@ -182,5 +177,21 @@ export const spaces = (kibana: Record<string, any>) =>
// Register a function with server to manage the collection of usage stats
server.usage.collectorSet.register(getSpacesUsageCollector(server));
server.registerCapabilitiesModifier(async (request, uiCapabilities) => {
const spacesClient = server.plugins.spaces.spacesClient.getScopedClient(request);
try {
const activeSpace = await getActiveSpace(
spacesClient,
request.getBasePath(),
server.config().get('server.basePath')
);
const features = server.plugins.xpack_main.getFeatures();
return toggleUICapabilities(features, uiCapabilities, activeSpace);
} catch (e) {
return uiCapabilities;
}
});
},
});

View file

@ -14,6 +14,7 @@ import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status';
import { replaceInjectedVars } from './server/lib/replace_injected_vars';
import { setupXPackMain } from './server/lib/setup_xpack_main';
import { getLocalizationUsageCollector } from './server/lib/get_localization_usage_collector';
import { uiCapabilitiesForFeatures } from './server/lib/ui_capabilities_for_features';
import {
xpackInfoRoute,
telemetryRoute,
@ -61,6 +62,10 @@ export const xpackMain = (kibana) => {
}).default();
},
uiCapabilities(server) {
return uiCapabilitiesForFeatures(server.plugins.xpack_main);
},
uiExports: {
managementSections: ['plugins/xpack_main/views/management'],
uiSettingDefaults: {
@ -92,6 +97,7 @@ export const xpackMain = (kibana) => {
},
injectDefaultVars(server) {
const config = server.config();
return {
telemetryUrl: config.get('xpack.xpack_main.telemetry.url'),
telemetryEnabled: isTelemetryEnabled(config),

View file

@ -47,12 +47,6 @@ describe('replaceInjectedVars uiExport', () => {
xpackInitialInfo: {
b: 1
},
uiCapabilities: {
mockFeature: {
mockFeatureCapability: true,
},
catalogue: {}
},
});
sinon.assert.calledOnce(server.plugins.security.isAuthenticated);
@ -72,12 +66,6 @@ describe('replaceInjectedVars uiExport', () => {
xpackInitialInfo: {
b: 1
},
uiCapabilities: {
mockFeature: {
mockFeatureCapability: true,
},
catalogue: {}
},
});
});
@ -94,12 +82,6 @@ describe('replaceInjectedVars uiExport', () => {
xpackInitialInfo: {
b: 1
},
uiCapabilities: {
mockFeature: {
mockFeatureCapability: true,
},
catalogue: {}
},
});
});
@ -116,12 +98,6 @@ describe('replaceInjectedVars uiExport', () => {
xpackInitialInfo: {
b: 1
},
uiCapabilities: {
mockFeature: {
mockFeatureCapability: true,
},
catalogue: {}
},
});
});
@ -138,12 +114,6 @@ describe('replaceInjectedVars uiExport', () => {
xpackInitialInfo: {
b: 1
},
uiCapabilities: {
mockFeature: {
mockFeatureCapability: true,
},
catalogue: {}
},
});
});
@ -160,49 +130,27 @@ describe('replaceInjectedVars uiExport', () => {
xpackInitialInfo: {
b: 1
},
uiCapabilities: {
mockFeature: {
mockFeatureCapability: true,
},
catalogue: {}
},
});
});
it('sends the originalInjectedVars augmented with UI Capabilities if not authenticated', async () => {
it('sends the originalInjectedVars if not authenticated', async () => {
const originalInjectedVars = { a: 1 };
const request = buildRequest();
const server = mockServer();
server.plugins.security.isAuthenticated.returns(false);
const newVars = await replaceInjectedVars(originalInjectedVars, request, server);
expect(newVars).to.eql({
...originalInjectedVars,
uiCapabilities: {
mockFeature: {
mockFeatureCapability: true,
},
catalogue: {}
},
});
expect(newVars).to.eql(originalInjectedVars);
});
it('sends the originalInjectedVars augmented with UI Capabilities if xpack info is unavailable', async () => {
it('sends the originalInjectedVars if xpack info is unavailable', async () => {
const originalInjectedVars = { a: 1 };
const request = buildRequest();
const server = mockServer();
server.plugins.xpack_main.info.isAvailable.returns(false);
const newVars = await replaceInjectedVars(originalInjectedVars, request, server);
expect(newVars).to.eql({
...originalInjectedVars,
uiCapabilities: {
mockFeature: {
mockFeatureCapability: true,
},
catalogue: {}
},
});
expect(newVars).to.eql(originalInjectedVars);
});
it('sends the originalInjectedVars (with xpackInitialInfo = undefined) if security is disabled, xpack info is unavailable', async () => {
@ -220,9 +168,6 @@ describe('replaceInjectedVars uiExport', () => {
uiCapabilities: {
navLinks: { foo: true },
bar: { baz: true },
mockFeature: {
mockFeatureCapability: true,
},
catalogue: {
cfoo: true,
}
@ -230,22 +175,14 @@ describe('replaceInjectedVars uiExport', () => {
});
});
it('sends the originalInjectedVars augmented with UI Capabilities if the license check result is not available', async () => {
it('sends the originalInjectedVars if the license check result is not available', async () => {
const originalInjectedVars = { a: 1 };
const request = buildRequest();
const server = mockServer();
server.plugins.xpack_main.info.feature().getLicenseCheckResults.returns(undefined);
const newVars = await replaceInjectedVars(originalInjectedVars, request, server);
expect(newVars).to.eql({
...originalInjectedVars,
uiCapabilities: {
mockFeature: {
mockFeatureCapability: true,
},
catalogue: {}
},
});
expect(newVars).to.eql(originalInjectedVars);
});
});

View file

@ -5,20 +5,12 @@
*/
import { getTelemetryOptIn } from './get_telemetry_opt_in';
import { populateUICapabilities } from './populate_ui_capabilities';
export async function replaceInjectedVars(originalInjectedVars, request, server) {
const xpackInfo = server.plugins.xpack_main.info;
const originalInjectedVarsWithUICapabilities = {
...originalInjectedVars,
uiCapabilities: {
...populateUICapabilities(server.plugins.xpack_main, originalInjectedVars.uiCapabilities),
}
};
const withXpackInfo = async () => ({
...originalInjectedVarsWithUICapabilities,
...originalInjectedVars,
telemetryOptedIn: await getTelemetryOptIn(request),
xpackInitialInfo: xpackInfo.isAvailable() ? xpackInfo.toJSON() : undefined,
});
@ -30,12 +22,12 @@ export async function replaceInjectedVars(originalInjectedVars, request, server)
// not enough license info to make decision one way or another
if (!xpackInfo.isAvailable() || !xpackInfo.feature('security').getLicenseCheckResults()) {
return originalInjectedVarsWithUICapabilities;
return originalInjectedVars;
}
// request is not authenticated
if (!await server.plugins.security.isAuthenticated(request)) {
return originalInjectedVarsWithUICapabilities;
return originalInjectedVars;
}
// plugin enabled, license is appropriate, request is authenticated

View file

@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { UICapabilities } from 'ui/capabilities';
import { Feature } from './feature_registry';
import { populateUICapabilities } from './populate_ui_capabilities';
import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features';
function getMockXpackMainPlugin(features: Feature[]) {
return {
@ -14,26 +13,6 @@ function getMockXpackMainPlugin(features: Feature[]) {
};
}
function getMockOriginalInjectedVars() {
return {
uiCapabilities: {
navLinks: {
foo: true,
bar: true,
},
management: {},
catalogue: {
fooEntry: true,
barEntry: true,
},
feature: {
someCapability: true,
},
otherFeature: {},
},
};
}
function createFeaturePrivilege(key: string, capabilities: string[] = []) {
return {
[key]: {
@ -51,28 +30,7 @@ describe('populateUICapabilities', () => {
it('handles no original uiCapabilites and no registered features gracefully', () => {
const xpackMainPlugin = getMockXpackMainPlugin([]);
expect(populateUICapabilities(xpackMainPlugin, {} as UICapabilities)).toEqual({});
});
it('returns the original uiCapabilities untouched when no features are registered', () => {
const xpackMainPlugin = getMockXpackMainPlugin([]);
const originalInjectedVars = getMockOriginalInjectedVars();
expect(populateUICapabilities(xpackMainPlugin, originalInjectedVars.uiCapabilities)).toEqual({
feature: {
someCapability: true,
},
navLinks: {
foo: true,
bar: true,
},
management: {},
catalogue: {
fooEntry: true,
barEntry: true,
},
otherFeature: {},
});
expect(uiCapabilitiesForFeatures(xpackMainPlugin)).toEqual({});
});
it('handles features with no registered capabilities', () => {
@ -86,23 +44,10 @@ describe('populateUICapabilities', () => {
},
},
]);
const originalInjectedVars = getMockOriginalInjectedVars();
expect(populateUICapabilities(xpackMainPlugin, originalInjectedVars.uiCapabilities)).toEqual({
feature: {
someCapability: true,
},
navLinks: {
foo: true,
bar: true,
},
management: {},
catalogue: {
fooEntry: true,
barEntry: true,
},
expect(uiCapabilitiesForFeatures(xpackMainPlugin)).toEqual({
catalogue: {},
newFeature: {},
otherFeature: {},
});
});
@ -118,26 +63,13 @@ describe('populateUICapabilities', () => {
},
},
]);
const originalInjectedVars = getMockOriginalInjectedVars();
expect(populateUICapabilities(xpackMainPlugin, originalInjectedVars.uiCapabilities)).toEqual({
feature: {
someCapability: true,
},
navLinks: {
foo: true,
bar: true,
},
management: {},
catalogue: {
fooEntry: true,
barEntry: true,
},
expect(uiCapabilitiesForFeatures(xpackMainPlugin)).toEqual({
catalogue: {},
newFeature: {
capability1: true,
capability2: true,
},
otherFeature: {},
});
});
@ -156,21 +88,10 @@ describe('populateUICapabilities', () => {
},
},
]);
const originalInjectedVars = getMockOriginalInjectedVars();
expect(populateUICapabilities(xpackMainPlugin, originalInjectedVars.uiCapabilities)).toEqual({
feature: {
someCapability: true,
},
navLinks: {
foo: true,
bar: true,
},
management: {},
expect(uiCapabilitiesForFeatures(xpackMainPlugin)).toEqual({
catalogue: {
fooEntry: true,
anotherFooEntry: true,
barEntry: true,
anotherBarEntry: true,
},
newFeature: {
@ -179,7 +100,6 @@ describe('populateUICapabilities', () => {
capability3: true,
capability4: true,
},
otherFeature: {},
});
});
@ -197,21 +117,9 @@ describe('populateUICapabilities', () => {
},
},
]);
const originalInjectedVars = getMockOriginalInjectedVars();
expect(populateUICapabilities(xpackMainPlugin, originalInjectedVars.uiCapabilities)).toEqual({
feature: {
someCapability: true,
},
navLinks: {
foo: true,
bar: true,
},
management: {},
catalogue: {
fooEntry: true,
barEntry: true,
},
expect(uiCapabilitiesForFeatures(xpackMainPlugin)).toEqual({
catalogue: {},
newFeature: {
capability1: true,
capability2: true,
@ -219,7 +127,6 @@ describe('populateUICapabilities', () => {
capability4: true,
capability5: true,
},
otherFeature: {},
});
});
@ -261,27 +168,15 @@ describe('populateUICapabilities', () => {
},
},
]);
const originalInjectedVars = getMockOriginalInjectedVars();
expect(populateUICapabilities(xpackMainPlugin, originalInjectedVars.uiCapabilities)).toEqual({
expect(uiCapabilitiesForFeatures(xpackMainPlugin)).toEqual({
anotherNewFeature: {
capability1: true,
capability2: true,
capability3: true,
capability4: true,
},
feature: {
someCapability: true,
},
navLinks: {
foo: true,
bar: true,
},
management: {},
catalogue: {
fooEntry: true,
barEntry: true,
},
catalogue: {},
newFeature: {
capability1: true,
capability2: true,
@ -289,7 +184,6 @@ describe('populateUICapabilities', () => {
capability4: true,
capability5: true,
},
otherFeature: {},
yetAnotherNewFeature: {
capability1: true,
capability2: true,

View file

@ -14,15 +14,12 @@ interface FeatureCapabilities {
[featureId: string]: Record<string, boolean>;
}
export function populateUICapabilities(
xpackMainPlugin: Record<string, any>,
uiCapabilities: UICapabilities
): UICapabilities {
export function uiCapabilitiesForFeatures(xpackMainPlugin: Record<string, any>): UICapabilities {
const features: Feature[] = xpackMainPlugin.getFeatures();
const featureCapabilities: FeatureCapabilities[] = features.map(getCapabilitiesFromFeature);
return mergeCapabilities(uiCapabilities || {}, ...featureCapabilities);
return buildCapabilities(...featureCapabilities);
}
function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities {
@ -60,25 +57,28 @@ function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities {
return UIFeatureCapabilities;
}
function mergeCapabilities(
originalCapabilities: UICapabilities,
...allFeatureCapabilities: FeatureCapabilities[]
): UICapabilities {
return allFeatureCapabilities.reduce<UICapabilities>((acc, capabilities) => {
const mergableCapabilities: UICapabilities = _.omit(capabilities, ...ELIGIBLE_FLAT_MERGE_KEYS);
function buildCapabilities(...allFeatureCapabilities: FeatureCapabilities[]): UICapabilities {
return allFeatureCapabilities.reduce<UICapabilities>(
(acc, capabilities) => {
const mergableCapabilities: UICapabilities = _.omit(
capabilities,
...ELIGIBLE_FLAT_MERGE_KEYS
);
const mergedFeatureCapabilities = {
...mergableCapabilities,
...acc,
};
ELIGIBLE_FLAT_MERGE_KEYS.forEach(key => {
mergedFeatureCapabilities[key] = {
...mergedFeatureCapabilities[key],
...capabilities[key],
const mergedFeatureCapabilities = {
...mergableCapabilities,
...acc,
};
});
return mergedFeatureCapabilities;
}, originalCapabilities);
ELIGIBLE_FLAT_MERGE_KEYS.forEach(key => {
mergedFeatureCapabilities[key] = {
...mergedFeatureCapabilities[key],
...capabilities[key],
};
});
return mergedFeatureCapabilities;
},
{} as UICapabilities
);
}

View file

@ -50,6 +50,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
...xPackFunctionalTestsConfig.get('kbnTestServer.serverArgs'),
...disabledPlugins.map(key => `--xpack.${key}.enabled=false`),
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'foo_plugin')}`,
'--optimize.enabled=false',
],
},
};

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
interface NestedBooleanObject {
[key: string]: boolean | NestedBooleanObject;
}
export const assertDeeplyFalse = (obj: NestedBooleanObject, path: string[] = []) => {
Object.keys(obj).forEach(key => {
const value = obj[key];
if (typeof value === 'object' && value !== null) {
assertDeeplyFalse(value, [...path, key]);
} else if (typeof value === 'boolean') {
if (value) {
throw new Error(`${[...path, key].join('.')} is not false: ${value}`);
}
} else {
throw new Error(`Expected nest object with boolean keys. '${key}' is not boolean: ${value}.`);
}
});
return true;
};

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import axios, { AxiosInstance } from 'axios';
import cheerio from 'cheerio';
import { UICapabilities } from 'ui/capabilities';
import { format as formatUrl } from 'url';
import util from 'util';
@ -41,22 +40,23 @@ export class UICapabilitiesService {
});
}
public async get(
credentials: BasicCredentials | null,
spaceId?: string
): Promise<GetUICapabilitiesResult> {
public async get({
credentials,
navLinks,
spaceId,
}: {
credentials?: BasicCredentials;
navLinks?: Record<string, boolean>;
spaceId?: string;
}): Promise<GetUICapabilitiesResult> {
const spaceUrlPrefix = spaceId ? `/s/${spaceId}` : '';
this.log.debug(`requesting ${spaceUrlPrefix}/app/kibana to parse the uiCapabilities`);
const requestHeaders = credentials
? {
Authorization: `Basic ${Buffer.from(
`${credentials.username}:${credentials.password}`
).toString('base64')}`,
}
: {};
const response = await this.axios.get(`${spaceUrlPrefix}/app/kibana`, {
headers: requestHeaders,
});
this.log.debug(`requesting ${spaceUrlPrefix}/api/capabilities to get the uiCapabilities`);
const requestOptions = credentials ? { auth: credentials } : {};
const response = await this.axios.post(
`${spaceUrlPrefix}/api/capabilities`,
{ capabilities: { navLinks } },
requestOptions
);
if (response.status === 302 && response.headers.location === '/') {
return {
@ -65,13 +65,6 @@ export class UICapabilitiesService {
};
}
if (response.status === 404) {
return {
success: false,
failureReason: GetUICapabilitiesFailureReason.NotFound,
};
}
if (response.status !== 200) {
throw new Error(
`Expected status code of 200, received ${response.status} ${
@ -80,25 +73,10 @@ export class UICapabilitiesService {
);
}
const dom = cheerio.load(response.data.toString());
const element = dom('kbn-injected-metadata');
if (!element) {
throw new Error('Unable to find "kbn-injected-metadata" element ');
}
const dataAttrJson = element.attr('data');
try {
const dataAttr = JSON.parse(dataAttrJson);
return {
success: true,
value: dataAttr.vars.uiCapabilities as UICapabilities,
};
} catch (err) {
throw new Error(
`Unable to parse JSON from the kbn-injected-metadata data attribute: ${dataAttrJson}`
);
}
return {
success: true,
value: response.data.capabilities,
};
}
}

View file

@ -7,11 +7,9 @@
import expect from '@kbn/expect';
import { mapValues } from 'lodash';
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
import {
GetUICapabilitiesFailureReason,
UICapabilitiesService,
} from '../../common/services/ui_capabilities';
import { UICapabilitiesService } from '../../common/services/ui_capabilities';
import { UserAtSpaceScenarios } from '../scenarios';
import { assertDeeplyFalse } from '../../common/lib/assert_deeply_false';
// eslint-disable-next-line import/no-default-export
export default function catalogueTests({ getService }: KibanaFunctionalTestDefaultProviders) {
@ -22,14 +20,16 @@ export default function catalogueTests({ getService }: KibanaFunctionalTestDefau
it(`${scenario.id}`, async () => {
const { user, space } = scenario;
const uiCapabilities = await uiCapabilitiesService.get(
{ username: user.username, password: user.password },
space.id
);
const uiCapabilities = await uiCapabilitiesService.get({
credentials: { username: user.username, password: user.password },
spaceId: space.id,
});
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('catalogue');
switch (scenario.id) {
case 'superuser at everything_space': {
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('catalogue');
// everything is enabled
const expected = mapValues(uiCapabilities.value!.catalogue, () => true);
expect(uiCapabilities.value!.catalogue).to.eql(expected);
@ -41,8 +41,6 @@ export default function catalogueTests({ getService }: KibanaFunctionalTestDefau
case 'global_read at everything_space':
case 'dual_privileges_read at everything_space':
case 'everything_space_read at everything_space': {
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('catalogue');
// everything except ml and monitoring is enabled
const expected = mapValues(
uiCapabilities.value!.catalogue,
@ -60,16 +58,13 @@ export default function catalogueTests({ getService }: KibanaFunctionalTestDefau
case 'dual_privileges_read at nothing_space':
case 'nothing_space_all at nothing_space':
case 'nothing_space_read at nothing_space': {
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('catalogue');
// everything is disabled
const expected = mapValues(uiCapabilities.value!.catalogue, () => false);
expect(uiCapabilities.value!.catalogue).to.eql(expected);
break;
}
// if we don't have access at the space itself, we're
// redirected to the space selector and the ui capabilities
// are lagely irrelevant because they won't be consumed
// if we don't have access at the space itself, all ui
// capabilities should be false
case 'no_kibana_privileges at everything_space':
case 'no_kibana_privileges at nothing_space':
case 'legacy_all at everything_space':
@ -78,10 +73,7 @@ export default function catalogueTests({ getService }: KibanaFunctionalTestDefau
case 'everything_space_read at nothing_space':
case 'nothing_space_all at everything_space':
case 'nothing_space_read at everything_space':
expect(uiCapabilities.success).to.be(false);
expect(uiCapabilities.failureReason).to.be(
GetUICapabilitiesFailureReason.RedirectedToRoot
);
assertDeeplyFalse(uiCapabilities.value!.catalogue);
break;
default:
throw new UnreachableError(scenario);

View file

@ -6,11 +6,9 @@
import expect from '@kbn/expect';
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
import {
GetUICapabilitiesFailureReason,
UICapabilitiesService,
} from '../../common/services/ui_capabilities';
import { UICapabilitiesService } from '../../common/services/ui_capabilities';
import { UserAtSpaceScenarios } from '../scenarios';
import { assertDeeplyFalse } from '../../common/lib/assert_deeply_false';
// eslint-disable-next-line import/no-default-export
export default function fooTests({ getService }: KibanaFunctionalTestDefaultProviders) {
@ -21,18 +19,18 @@ export default function fooTests({ getService }: KibanaFunctionalTestDefaultProv
it(`${scenario.id}`, async () => {
const { user, space } = scenario;
const uiCapabilities = await uiCapabilitiesService.get(
{ username: user.username, password: user.password },
space.id
);
const uiCapabilities = await uiCapabilitiesService.get({
credentials: { username: user.username, password: user.password },
spaceId: space.id,
});
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('foo');
switch (scenario.id) {
// these users have a read/write view
case 'superuser at everything_space':
case 'global_all at everything_space':
case 'dual_privileges_all at everything_space':
case 'everything_space_all at everything_space':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('foo');
expect(uiCapabilities.value!.foo).to.eql({
create: true,
edit: true,
@ -44,8 +42,6 @@ export default function fooTests({ getService }: KibanaFunctionalTestDefaultProv
case 'global_read at everything_space':
case 'dual_privileges_read at everything_space':
case 'everything_space_read at everything_space':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('foo');
expect(uiCapabilities.value!.foo).to.eql({
create: false,
edit: false,
@ -62,8 +58,6 @@ export default function fooTests({ getService }: KibanaFunctionalTestDefaultProv
case 'dual_privileges_read at nothing_space':
case 'nothing_space_all at nothing_space':
case 'nothing_space_read at nothing_space':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('foo');
expect(uiCapabilities.value!.foo).to.eql({
create: false,
edit: false,
@ -71,9 +65,8 @@ export default function fooTests({ getService }: KibanaFunctionalTestDefaultProv
show: false,
});
break;
// if we don't have access at the space itself, we're
// redirected to the space selector and the ui capabilities
// are largely irrelevant because they won't be consumed
// if we don't have access at the space itself, all ui
// capabilities should be false
case 'no_kibana_privileges at everything_space':
case 'no_kibana_privileges at nothing_space':
case 'legacy_all at everything_space':
@ -82,10 +75,7 @@ export default function fooTests({ getService }: KibanaFunctionalTestDefaultProv
case 'everything_space_read at nothing_space':
case 'nothing_space_all at everything_space':
case 'nothing_space_read at everything_space':
expect(uiCapabilities.success).to.be(false);
expect(uiCapabilities.failureReason).to.be(
GetUICapabilitiesFailureReason.RedirectedToRoot
);
assertDeeplyFalse(uiCapabilities.value!.foo);
break;
default:
throw new UnreachableError(scenario);

View file

@ -8,10 +8,7 @@ import expect from '@kbn/expect';
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
import { NavLinksBuilder } from '../../common/nav_links_builder';
import { FeaturesService } from '../../common/services';
import {
GetUICapabilitiesFailureReason,
UICapabilitiesService,
} from '../../common/services/ui_capabilities';
import { UICapabilitiesService } from '../../common/services/ui_capabilities';
import { UserAtSpaceScenarios } from '../scenarios';
// eslint-disable-next-line import/no-default-export
@ -30,14 +27,15 @@ export default function navLinksTests({ getService }: KibanaFunctionalTestDefaul
it(`${scenario.id}`, async () => {
const { user, space } = scenario;
const uiCapabilities = await uiCapabilitiesService.get(
{ username: user.username, password: user.password },
space.id
);
const uiCapabilities = await uiCapabilitiesService.get({
credentials: { username: user.username, password: user.password },
navLinks: navLinksBuilder.all(),
spaceId: space.id,
});
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('navLinks');
switch (scenario.id) {
case 'superuser at everything_space':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('navLinks');
expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.all());
break;
case 'global_all at everything_space':
@ -46,8 +44,6 @@ export default function navLinksTests({ getService }: KibanaFunctionalTestDefaul
case 'global_read at everything_space':
case 'everything_space_all at everything_space':
case 'everything_space_read at everything_space':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('navLinks');
expect(uiCapabilities.value!.navLinks).to.eql(
navLinksBuilder.except('ml', 'monitoring')
);
@ -59,10 +55,10 @@ export default function navLinksTests({ getService }: KibanaFunctionalTestDefaul
case 'global_read at nothing_space':
case 'nothing_space_all at nothing_space':
case 'nothing_space_read at nothing_space':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('navLinks');
expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.only('management'));
break;
// these users have no access to any navLinks except management
// which is not a navLink that ever gets disabled.
case 'no_kibana_privileges at everything_space':
case 'no_kibana_privileges at nothing_space':
case 'legacy_all at everything_space':
@ -71,10 +67,7 @@ export default function navLinksTests({ getService }: KibanaFunctionalTestDefaul
case 'everything_space_read at nothing_space':
case 'nothing_space_all at everything_space':
case 'nothing_space_read at everything_space':
expect(uiCapabilities.success).to.be(false);
expect(uiCapabilities.failureReason).to.be(
GetUICapabilitiesFailureReason.RedirectedToRoot
);
expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.only('management'));
break;
default:
throw new UnreachableError(scenario);

View file

@ -7,11 +7,9 @@ import expect from '@kbn/expect';
import { mapValues } from 'lodash';
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
import { SavedObjectsManagementBuilder } from '../../common/saved_objects_management_builder';
import {
GetUICapabilitiesFailureReason,
UICapabilitiesService,
} from '../../common/services/ui_capabilities';
import { UICapabilitiesService } from '../../common/services/ui_capabilities';
import { UserAtSpaceScenarios } from '../scenarios';
import { assertDeeplyFalse } from '../../common/lib/assert_deeply_false';
const savedObjectsManagementBuilder = new SavedObjectsManagementBuilder(true);
@ -26,15 +24,15 @@ export default function savedObjectsManagementTests({
it(`${scenario.id}`, async () => {
const { user, space } = scenario;
const uiCapabilities = await uiCapabilitiesService.get(
{ username: user.username, password: user.password },
space.id
);
const uiCapabilities = await uiCapabilitiesService.get({
credentials: { username: user.username, password: user.password },
spaceId: space.id,
});
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('savedObjectsManagement');
switch (scenario.id) {
case 'superuser at everything_space':
case 'superuser at nothing_space':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('savedObjectsManagement');
const expected = mapValues(uiCapabilities.value!.savedObjectsManagement, () =>
savedObjectsManagementBuilder.uiCapabilities('all')
);
@ -46,8 +44,6 @@ export default function savedObjectsManagementTests({
case 'global_all at nothing_space':
case 'dual_privileges_all at nothing_space':
case 'nothing_space_all at nothing_space':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('savedObjectsManagement');
expect(uiCapabilities.value!.savedObjectsManagement).to.eql(
savedObjectsManagementBuilder.build({
all: [
@ -74,8 +70,6 @@ export default function savedObjectsManagementTests({
case 'dual_privileges_read at nothing_space':
case 'global_read at nothing_space':
case 'nothing_space_read at nothing_space':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('savedObjectsManagement');
expect(uiCapabilities.value!.savedObjectsManagement).to.eql(
savedObjectsManagementBuilder.build({
read: [
@ -95,6 +89,8 @@ export default function savedObjectsManagementTests({
})
);
break;
// if we don't have access at the space itself, all ui
// capabilities should be false
case 'no_kibana_privileges at everything_space':
case 'no_kibana_privileges at nothing_space':
case 'legacy_all at everything_space':
@ -103,10 +99,7 @@ export default function savedObjectsManagementTests({
case 'everything_space_read at nothing_space':
case 'nothing_space_all at everything_space':
case 'nothing_space_read at everything_space':
expect(uiCapabilities.success).to.be(false);
expect(uiCapabilities.failureReason).to.be(
GetUICapabilitiesFailureReason.RedirectedToRoot
);
assertDeeplyFalse(uiCapabilities.value!.savedObjectsManagement);
break;
default:
throw new UnreachableError(scenario);

View file

@ -7,11 +7,9 @@
import expect from '@kbn/expect';
import { mapValues } from 'lodash';
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
import {
GetUICapabilitiesFailureReason,
UICapabilitiesService,
} from '../../common/services/ui_capabilities';
import { UICapabilitiesService } from '../../common/services/ui_capabilities';
import { UserScenarios } from '../scenarios';
import { assertDeeplyFalse } from '../../common/lib/assert_deeply_false';
// eslint-disable-next-line import/no-default-export
export default function catalogueTests({ getService }: KibanaFunctionalTestDefaultProviders) {
@ -21,13 +19,15 @@ export default function catalogueTests({ getService }: KibanaFunctionalTestDefau
UserScenarios.forEach(scenario => {
it(`${scenario.fullName}`, async () => {
const uiCapabilities = await uiCapabilitiesService.get({
username: scenario.username,
password: scenario.password,
credentials: {
username: scenario.username,
password: scenario.password,
},
});
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('catalogue');
switch (scenario.username) {
case 'superuser': {
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('catalogue');
// everything is enabled
const expected = mapValues(uiCapabilities.value!.catalogue, () => true);
expect(uiCapabilities.value!.catalogue).to.eql(expected);
@ -37,8 +37,6 @@ export default function catalogueTests({ getService }: KibanaFunctionalTestDefau
case 'read':
case 'dual_privileges_all':
case 'dual_privileges_read': {
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('catalogue');
// everything except ml and monitoring is enabled
const expected = mapValues(
uiCapabilities.value!.catalogue,
@ -49,8 +47,6 @@ export default function catalogueTests({ getService }: KibanaFunctionalTestDefau
}
case 'foo_all':
case 'foo_read': {
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('catalogue');
// only foo is enabled
const expected = mapValues(
uiCapabilities.value!.catalogue,
@ -59,11 +55,10 @@ export default function catalogueTests({ getService }: KibanaFunctionalTestDefau
expect(uiCapabilities.value!.catalogue).to.eql(expected);
break;
}
// these users have no access to even get the ui capabilities
// these users have no access to any ui capabilities
case 'legacy_all':
case 'no_kibana_privileges':
expect(uiCapabilities.success).to.be(false);
expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound);
assertDeeplyFalse(uiCapabilities.value!.catalogue);
break;
default:
throw new UnreachableError(scenario);

View file

@ -6,11 +6,9 @@
import expect from '@kbn/expect';
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
import {
GetUICapabilitiesFailureReason,
UICapabilitiesService,
} from '../../common/services/ui_capabilities';
import { UICapabilitiesService } from '../../common/services/ui_capabilities';
import { UserScenarios } from '../scenarios';
import { assertDeeplyFalse } from '../../common/lib/assert_deeply_false';
// eslint-disable-next-line import/no-default-export
export default function fooTests({ getService }: KibanaFunctionalTestDefaultProviders) {
@ -20,17 +18,21 @@ export default function fooTests({ getService }: KibanaFunctionalTestDefaultProv
UserScenarios.forEach(scenario => {
it(`${scenario.fullName}`, async () => {
const uiCapabilities = await uiCapabilitiesService.get({
username: scenario.username,
password: scenario.password,
credentials: {
username: scenario.username,
password: scenario.password,
},
});
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('foo');
switch (scenario.username) {
// these users have a read/write view of Foo
case 'superuser':
case 'all':
case 'dual_privileges_all':
case 'foo_all':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('foo');
expect(uiCapabilities.value!.foo).to.eql({
create: true,
edit: true,
@ -42,8 +44,6 @@ export default function fooTests({ getService }: KibanaFunctionalTestDefaultProv
case 'read':
case 'dual_privileges_read':
case 'foo_read':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('foo');
expect(uiCapabilities.value!.foo).to.eql({
create: false,
edit: false,
@ -51,11 +51,10 @@ export default function fooTests({ getService }: KibanaFunctionalTestDefaultProv
show: true,
});
break;
// these users have no access to even get the ui capabilities
// these users have no access to any ui capabilities
case 'legacy_all':
case 'no_kibana_privileges':
expect(uiCapabilities.success).to.be(false);
expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound);
assertDeeplyFalse(uiCapabilities.value!.foo);
break;
// all other users can't do anything with Foo
default:

View file

@ -8,10 +8,7 @@ import expect from '@kbn/expect';
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
import { NavLinksBuilder } from '../../common/nav_links_builder';
import { FeaturesService } from '../../common/services';
import {
GetUICapabilitiesFailureReason,
UICapabilitiesService,
} from '../../common/services/ui_capabilities';
import { UICapabilitiesService } from '../../common/services/ui_capabilities';
import { UserScenarios } from '../scenarios';
// eslint-disable-next-line import/no-default-export
@ -29,37 +26,38 @@ export default function navLinksTests({ getService }: KibanaFunctionalTestDefaul
UserScenarios.forEach(scenario => {
it(`${scenario.fullName}`, async () => {
const uiCapabilities = await uiCapabilitiesService.get({
username: scenario.username,
password: scenario.password,
credentials: {
username: scenario.username,
password: scenario.password,
},
navLinks: navLinksBuilder.all(),
});
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('navLinks');
switch (scenario.username) {
case 'superuser':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('navLinks');
expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.all());
break;
case 'all':
case 'read':
case 'dual_privileges_all':
case 'dual_privileges_read':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('navLinks');
expect(uiCapabilities.value!.navLinks).to.eql(
navLinksBuilder.except('ml', 'monitoring')
);
break;
case 'foo_all':
case 'foo_read':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('navLinks');
expect(uiCapabilities.value!.navLinks).to.eql(
navLinksBuilder.only('management', 'foo')
);
break;
// these users have no access to any navLinks except management
// which is not a navLink that ever gets disabled.
case 'legacy_all':
case 'no_kibana_privileges':
expect(uiCapabilities.success).to.be(false);
expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound);
expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.only('management'));
break;
default:
throw new UnreachableError(scenario);

View file

@ -8,11 +8,9 @@ import expect from '@kbn/expect';
import { mapValues } from 'lodash';
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
import { SavedObjectsManagementBuilder } from '../../common/saved_objects_management_builder';
import {
GetUICapabilitiesFailureReason,
UICapabilitiesService,
} from '../../common/services/ui_capabilities';
import { UICapabilitiesService } from '../../common/services/ui_capabilities';
import { UserScenarios } from '../scenarios';
import { assertDeeplyFalse } from '../../common/lib/assert_deeply_false';
const savedObjectsManagementBuilder = new SavedObjectsManagementBuilder(false);
@ -26,13 +24,15 @@ export default function savedObjectsManagementTests({
UserScenarios.forEach(scenario => {
it(`${scenario.fullName}`, async () => {
const uiCapabilities = await uiCapabilitiesService.get({
username: scenario.username,
password: scenario.password,
credentials: {
username: scenario.username,
password: scenario.password,
},
});
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('savedObjectsManagement');
switch (scenario.username) {
case 'superuser':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('savedObjectsManagement');
const expected = mapValues(uiCapabilities.value!.savedObjectsManagement, () =>
savedObjectsManagementBuilder.uiCapabilities('all')
);
@ -40,8 +40,6 @@ export default function savedObjectsManagementTests({
break;
case 'all':
case 'dual_privileges_all':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('savedObjectsManagement');
expect(uiCapabilities.value!.savedObjectsManagement).to.eql(
savedObjectsManagementBuilder.build({
all: [
@ -64,8 +62,6 @@ export default function savedObjectsManagementTests({
break;
case 'read':
case 'dual_privileges_read':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('savedObjectsManagement');
expect(uiCapabilities.value!.savedObjectsManagement).to.eql(
savedObjectsManagementBuilder.build({
read: [
@ -86,8 +82,6 @@ export default function savedObjectsManagementTests({
);
break;
case 'foo_all':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('savedObjectsManagement');
expect(uiCapabilities.value!.savedObjectsManagement).to.eql(
savedObjectsManagementBuilder.build({
all: ['foo', 'telemetry'],
@ -96,8 +90,6 @@ export default function savedObjectsManagementTests({
);
break;
case 'foo_read':
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('savedObjectsManagement');
expect(uiCapabilities.value!.savedObjectsManagement).to.eql(
savedObjectsManagementBuilder.build({
read: ['foo', 'index-pattern', 'config'],
@ -105,9 +97,10 @@ export default function savedObjectsManagementTests({
);
break;
case 'no_kibana_privileges':
// these users have no access to any ui capabilities
case 'legacy_all':
expect(uiCapabilities.success).to.be(false);
expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound);
case 'no_kibana_privileges':
assertDeeplyFalse(uiCapabilities.value!.savedObjectsManagement);
break;
default:
throw new UnreachableError(scenario);

View file

@ -17,7 +17,7 @@ export default function catalogueTests({ getService }: KibanaFunctionalTestDefau
describe('catalogue', () => {
SpaceScenarios.forEach(scenario => {
it(`${scenario.name}`, async () => {
const uiCapabilities = await uiCapabilitiesService.get(null, scenario.id);
const uiCapabilities = await uiCapabilitiesService.get({ spaceId: scenario.id });
switch (scenario.id) {
case 'everything_space': {
expect(uiCapabilities.success).to.be(true);

View file

@ -16,7 +16,7 @@ export default function fooTests({ getService }: KibanaFunctionalTestDefaultProv
describe('foo', () => {
SpaceScenarios.forEach(scenario => {
it(`${scenario.name}`, async () => {
const uiCapabilities = await uiCapabilitiesService.get(null, scenario.id);
const uiCapabilities = await uiCapabilitiesService.get({ spaceId: scenario.id });
switch (scenario.id) {
case 'everything_space':
expect(uiCapabilities.success).to.be(true);

View file

@ -25,7 +25,10 @@ export default function navLinksTests({ getService }: KibanaFunctionalTestDefaul
SpaceScenarios.forEach(scenario => {
it(`${scenario.name}`, async () => {
const uiCapabilities = await uiCapabilitiesService.get(null, scenario.id);
const uiCapabilities = await uiCapabilitiesService.get({
navLinks: navLinksBuilder.all(),
spaceId: scenario.id,
});
switch (scenario.id) {
case 'everything_space':
expect(uiCapabilities.success).to.be(true);

View file

@ -23,7 +23,7 @@ export default function savedObjectsManagementTests({
SpaceScenarios.forEach(scenario => {
it(`${scenario.name}`, async () => {
// spaces don't affect saved objects management, so we assert the same thing for every scenario
const uiCapabilities = await uiCapabilitiesService.get(null, scenario.id);
const uiCapabilities = await uiCapabilitiesService.get({ spaceId: scenario.id });
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('savedObjectsManagement');
const expected = mapValues(uiCapabilities.value!.savedObjectsManagement, () =>

View file

@ -8,8 +8,8 @@ import 'hapi';
import { CloudPlugin } from '../plugins/cloud';
import { EncryptedSavedObjectsPlugin } from '../plugins/encrypted_saved_objects';
import { SecurityPlugin } from '../plugins/security';
import { XPackMainPlugin } from '../plugins/xpack_main/xpack_main';
import { SecurityPlugin } from '../plugins/security';
declare module 'hapi' {
interface PluginProperties {