mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* create service skeleton * move registerCapabilitiesModifier to capabilities service and rename to registerCapabilitiesSwitcher * starts to move capabilities logic to CapabilitiesService * move capabilities route to service * add initial integration test for capabilities route * capabilitiesMixin now delegates to capability service * use server-side Capabilities import in server code * update generated doc * remove capabilities from injectedMetadatas * use applications sent from client instead of server-registered navLinks * disable authRequired for capabilities route * (temp) exposes two endpoints for capabilities * Add fetch-mock on capabilities call for karma tests * adapt xpack Capabilities test - first attempt * adapt x-pack ui_capabilities test * add '/status' to the list of anonymous pages * Add documentation on Capabilities APIs * move Capabilities to core/types * update generated docs * add service tests * protecting resolveCapabilities against added/removed capabilities * update generated docs * adapt mocks due to rebase * add forgotten exports * improve capabilities routes registering * name capabilities registering methods * resolve conflicts due to merge * address review issues * add comment about reason for exposing two routes * extract createHttpServer test helper * fix merge conflicts * improve documentation * remove `/status` anon registration as now done in NP status plugin * fix merge conflicts
This commit is contained in:
parent
83fb4f6453
commit
bc6189d364
79 changed files with 3358 additions and 2424 deletions
|
@ -337,6 +337,7 @@ module.exports = {
|
|||
'!src/core/server/index.ts',
|
||||
'!src/core/server/mocks.ts',
|
||||
'!src/core/server/types.ts',
|
||||
'!src/core/server/test_utils.ts',
|
||||
// for absolute imports until fixed in
|
||||
// https://github.com/elastic/kibana/issues/36096
|
||||
'!src/core/server/types',
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Capabilities](./kibana-plugin-server.capabilities.md) > [catalogue](./kibana-plugin-server.capabilities.catalogue.md)
|
||||
|
||||
## Capabilities.catalogue property
|
||||
|
||||
Catalogue capabilities. Catalogue entries drive the visibility of the Kibana homepage options.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
catalogue: Record<string, boolean>;
|
||||
```
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Capabilities](./kibana-plugin-server.capabilities.md) > [management](./kibana-plugin-server.capabilities.management.md)
|
||||
|
||||
## Capabilities.management property
|
||||
|
||||
Management section capabilities.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
management: {
|
||||
[sectionId: string]: Record<string, boolean>;
|
||||
};
|
||||
```
|
|
@ -0,0 +1,22 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Capabilities](./kibana-plugin-server.capabilities.md)
|
||||
|
||||
## Capabilities interface
|
||||
|
||||
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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface Capabilities
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [catalogue](./kibana-plugin-server.capabilities.catalogue.md) | <code>Record<string, boolean></code> | Catalogue capabilities. Catalogue entries drive the visibility of the Kibana homepage options. |
|
||||
| [management](./kibana-plugin-server.capabilities.management.md) | <code>{</code><br/><code> [sectionId: string]: Record<string, boolean>;</code><br/><code> }</code> | Management section capabilities. |
|
||||
| [navLinks](./kibana-plugin-server.capabilities.navlinks.md) | <code>Record<string, boolean></code> | Navigation link capabilities. |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Capabilities](./kibana-plugin-server.capabilities.md) > [navLinks](./kibana-plugin-server.capabilities.navlinks.md)
|
||||
|
||||
## Capabilities.navLinks property
|
||||
|
||||
Navigation link capabilities.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
navLinks: Record<string, boolean>;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CapabilitiesProvider](./kibana-plugin-server.capabilitiesprovider.md)
|
||||
|
||||
## CapabilitiesProvider type
|
||||
|
||||
See [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type CapabilitiesProvider = () => Partial<Capabilities>;
|
||||
```
|
|
@ -0,0 +1,27 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md)
|
||||
|
||||
## CapabilitiesSetup interface
|
||||
|
||||
APIs to manage the [Capabilities](./kibana-plugin-server.capabilities.md) that will be used by the application.
|
||||
|
||||
Plugins relying on capabilities to toggle some of their features should register them during the setup phase using the `registerProvider` method.
|
||||
|
||||
Plugins having the responsibility to restrict capabilities depending on a given context should register their capabilities switcher using the `registerSwitcher` method.
|
||||
|
||||
Refers to the methods documentation for complete description and examples.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface CapabilitiesSetup
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| [registerProvider(provider)](./kibana-plugin-server.capabilitiessetup.registerprovider.md) | Register a [CapabilitiesProvider](./kibana-plugin-server.capabilitiesprovider.md) to be used to provide [Capabilities](./kibana-plugin-server.capabilities.md) when resolving them. |
|
||||
| [registerSwitcher(switcher)](./kibana-plugin-server.capabilitiessetup.registerswitcher.md) | Register a [CapabilitiesSwitcher](./kibana-plugin-server.capabilitiesswitcher.md) to be used to change the default state of the [Capabilities](./kibana-plugin-server.capabilities.md) entries when resolving them.<!-- -->A capabilities switcher can only change the state of existing capabilities. Capabilities added or removed when invoking the switcher will be ignored. |
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) > [registerProvider](./kibana-plugin-server.capabilitiessetup.registerprovider.md)
|
||||
|
||||
## CapabilitiesSetup.registerProvider() method
|
||||
|
||||
Register a [CapabilitiesProvider](./kibana-plugin-server.capabilitiesprovider.md) to be used to provide [Capabilities](./kibana-plugin-server.capabilities.md) when resolving them.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
registerProvider(provider: CapabilitiesProvider): void;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| provider | <code>CapabilitiesProvider</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`void`
|
||||
|
||||
## Example
|
||||
|
||||
How to register a plugin's capabilities during setup
|
||||
|
||||
```ts
|
||||
// my-plugin/server/plugin.ts
|
||||
public setup(core: CoreSetup, deps: {}) {
|
||||
core.capabilities.registerProvider(() => {
|
||||
return {
|
||||
catalogue: {
|
||||
myPlugin: true,
|
||||
},
|
||||
myPlugin: {
|
||||
someFeature: true,
|
||||
featureDisabledByDefault: false,
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) > [registerSwitcher](./kibana-plugin-server.capabilitiessetup.registerswitcher.md)
|
||||
|
||||
## CapabilitiesSetup.registerSwitcher() method
|
||||
|
||||
Register a [CapabilitiesSwitcher](./kibana-plugin-server.capabilitiesswitcher.md) to be used to change the default state of the [Capabilities](./kibana-plugin-server.capabilities.md) entries when resolving them.
|
||||
|
||||
A capabilities switcher can only change the state of existing capabilities. Capabilities added or removed when invoking the switcher will be ignored.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
registerSwitcher(switcher: CapabilitiesSwitcher): void;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| switcher | <code>CapabilitiesSwitcher</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`void`
|
||||
|
||||
## Example
|
||||
|
||||
How to restrict some capabilities
|
||||
|
||||
```ts
|
||||
// my-plugin/server/plugin.ts
|
||||
public setup(core: CoreSetup, deps: {}) {
|
||||
core.capabilities.registerSwitcher((request, capabilities) => {
|
||||
if(myPluginApi.shouldRestrictSomePluginBecauseOf(request)) {
|
||||
return {
|
||||
somePlugin: {
|
||||
featureEnabledByDefault: false // `featureEnabledByDefault` will be disabled. All other capabilities will remain unchanged.
|
||||
}
|
||||
}
|
||||
}
|
||||
return {}; // All capabilities will remain unchanged.
|
||||
});
|
||||
}
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CapabilitiesStart](./kibana-plugin-server.capabilitiesstart.md)
|
||||
|
||||
## CapabilitiesStart interface
|
||||
|
||||
APIs to access the application [Capabilities](./kibana-plugin-server.capabilities.md)<!-- -->.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface CapabilitiesStart
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| [resolveCapabilities(request)](./kibana-plugin-server.capabilitiesstart.resolvecapabilities.md) | Resolve the [Capabilities](./kibana-plugin-server.capabilities.md) to be used for given request |
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CapabilitiesStart](./kibana-plugin-server.capabilitiesstart.md) > [resolveCapabilities](./kibana-plugin-server.capabilitiesstart.resolvecapabilities.md)
|
||||
|
||||
## CapabilitiesStart.resolveCapabilities() method
|
||||
|
||||
Resolve the [Capabilities](./kibana-plugin-server.capabilities.md) to be used for given request
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
resolveCapabilities(request: KibanaRequest): Promise<Capabilities>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| request | <code>KibanaRequest</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`Promise<Capabilities>`
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CapabilitiesSwitcher](./kibana-plugin-server.capabilitiesswitcher.md)
|
||||
|
||||
## CapabilitiesSwitcher type
|
||||
|
||||
See [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type CapabilitiesSwitcher = (request: KibanaRequest, uiCapabilities: Capabilities) => Partial<Capabilities> | Promise<Partial<Capabilities>>;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) > [capabilities](./kibana-plugin-server.coresetup.capabilities.md)
|
||||
|
||||
## CoreSetup.capabilities property
|
||||
|
||||
[CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
capabilities: CapabilitiesSetup;
|
||||
```
|
|
@ -16,6 +16,7 @@ export interface CoreSetup
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [capabilities](./kibana-plugin-server.coresetup.capabilities.md) | <code>CapabilitiesSetup</code> | [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) |
|
||||
| [context](./kibana-plugin-server.coresetup.context.md) | <code>ContextSetup</code> | [ContextSetup](./kibana-plugin-server.contextsetup.md) |
|
||||
| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | <code>ElasticsearchServiceSetup</code> | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) |
|
||||
| [http](./kibana-plugin-server.coresetup.http.md) | <code>HttpServiceSetup</code> | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreStart](./kibana-plugin-server.corestart.md) > [capabilities](./kibana-plugin-server.corestart.capabilities.md)
|
||||
|
||||
## CoreStart.capabilities property
|
||||
|
||||
[CapabilitiesStart](./kibana-plugin-server.capabilitiesstart.md)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
capabilities: CapabilitiesStart;
|
||||
```
|
|
@ -16,5 +16,6 @@ export interface CoreStart
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [capabilities](./kibana-plugin-server.corestart.capabilities.md) | <code>CapabilitiesStart</code> | [CapabilitiesStart](./kibana-plugin-server.capabilitiesstart.md) |
|
||||
| [savedObjects](./kibana-plugin-server.corestart.savedobjects.md) | <code>SavedObjectsServiceStart</code> | [SavedObjectsServiceStart](./kibana-plugin-server.savedobjectsservicestart.md) |
|
||||
|
||||
|
|
|
@ -43,6 +43,9 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [AuthResultParams](./kibana-plugin-server.authresultparams.md) | Result of an incoming request authentication. |
|
||||
| [AuthToolkit](./kibana-plugin-server.authtoolkit.md) | A tool set defining an outcome of Auth interceptor for incoming request. |
|
||||
| [CallAPIOptions](./kibana-plugin-server.callapioptions.md) | The set of options that defines how API call should be made and result be processed. |
|
||||
| [Capabilities](./kibana-plugin-server.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. |
|
||||
| [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) | APIs to manage the [Capabilities](./kibana-plugin-server.capabilities.md) that will be used by the application.<!-- -->Plugins relying on capabilities to toggle some of their features should register them during the setup phase using the <code>registerProvider</code> method.<!-- -->Plugins having the responsibility to restrict capabilities depending on a given context should register their capabilities switcher using the <code>registerSwitcher</code> method.<!-- -->Refers to the methods documentation for complete description and examples. |
|
||||
| [CapabilitiesStart](./kibana-plugin-server.capabilitiesstart.md) | APIs to access the application [Capabilities](./kibana-plugin-server.capabilities.md)<!-- -->. |
|
||||
| [ContextSetup](./kibana-plugin-server.contextsetup.md) | An object that handles registration of context providers and configuring handlers with context. |
|
||||
| [CoreSetup](./kibana-plugin-server.coresetup.md) | Context passed to the plugins <code>setup</code> method. |
|
||||
| [CoreStart](./kibana-plugin-server.corestart.md) | Context passed to the plugins <code>start</code> method. |
|
||||
|
@ -144,6 +147,8 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [AuthenticationHandler](./kibana-plugin-server.authenticationhandler.md) | See [AuthToolkit](./kibana-plugin-server.authtoolkit.md)<!-- -->. |
|
||||
| [AuthHeaders](./kibana-plugin-server.authheaders.md) | Auth Headers map |
|
||||
| [AuthResult](./kibana-plugin-server.authresult.md) | |
|
||||
| [CapabilitiesProvider](./kibana-plugin-server.capabilitiesprovider.md) | See [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) |
|
||||
| [CapabilitiesSwitcher](./kibana-plugin-server.capabilitiesswitcher.md) | See [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) |
|
||||
| [ConfigPath](./kibana-plugin-server.configpath.md) | |
|
||||
| [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | |
|
||||
| [GetAuthHeaders](./kibana-plugin-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. |
|
||||
|
|
|
@ -136,7 +136,7 @@ describe('#start()', () => {
|
|||
expect(MockCapabilitiesService.start).toHaveBeenCalledWith({
|
||||
apps: new Map([['app1', { id: 'app1' }]]),
|
||||
legacyApps: new Map(),
|
||||
injectedMetadata,
|
||||
http,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -153,7 +153,7 @@ describe('#start()', () => {
|
|||
expect(MockCapabilitiesService.start).toHaveBeenCalledWith({
|
||||
apps: new Map(),
|
||||
legacyApps: new Map([['legacyApp1', { id: 'legacyApp1' }]]),
|
||||
injectedMetadata,
|
||||
http,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -111,9 +111,9 @@ export class ApplicationService {
|
|||
const legacyMode = injectedMetadata.getLegacyMode();
|
||||
const currentAppId$ = new BehaviorSubject<string | undefined>(undefined);
|
||||
const { availableApps, availableLegacyApps, capabilities } = await this.capabilities.start({
|
||||
http,
|
||||
apps: new Map([...this.apps$.value].map(([id, { app }]) => [id, app])),
|
||||
legacyApps: this.legacyApps$.value,
|
||||
injectedMetadata,
|
||||
});
|
||||
|
||||
// Only setup history if we're not in legacy mode
|
||||
|
|
|
@ -17,32 +17,35 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { InjectedMetadataService } from '../../injected_metadata';
|
||||
import { httpServiceMock, HttpSetupMock } from '../../http/http_service.mock';
|
||||
import { CapabilitiesService } from './capabilities_service';
|
||||
import { LegacyApp, App } from '../types';
|
||||
|
||||
const mockedCapabilities = {
|
||||
catalogue: {},
|
||||
management: {},
|
||||
navLinks: {
|
||||
app1: true,
|
||||
app2: false,
|
||||
legacyApp1: true,
|
||||
legacyApp2: false,
|
||||
},
|
||||
foo: { feature: true },
|
||||
bar: { feature: true },
|
||||
};
|
||||
|
||||
describe('#start', () => {
|
||||
const injectedMetadata = new InjectedMetadataService({
|
||||
injectedMetadata: {
|
||||
version: 'kibanaVersion',
|
||||
capabilities: {
|
||||
catalogue: {},
|
||||
management: {},
|
||||
navLinks: {
|
||||
app1: true,
|
||||
app2: false,
|
||||
legacyApp1: true,
|
||||
legacyApp2: false,
|
||||
},
|
||||
foo: { feature: true },
|
||||
bar: { feature: true },
|
||||
},
|
||||
} as any,
|
||||
}).start();
|
||||
let http: HttpSetupMock;
|
||||
|
||||
beforeEach(() => {
|
||||
http = httpServiceMock.createStartContract();
|
||||
http.post.mockReturnValue(Promise.resolve(mockedCapabilities));
|
||||
});
|
||||
|
||||
const apps = new Map([
|
||||
['app1', { id: 'app1' }],
|
||||
['app2', { id: 'app2', capabilities: { app2: { feature: true } } }],
|
||||
['appMissingInCapabilities', { id: 'appMissingInCapabilities' }],
|
||||
] as Array<[string, App]>);
|
||||
const legacyApps = new Map([
|
||||
['legacyApp1', { id: 'legacyApp1' }],
|
||||
|
@ -51,8 +54,13 @@ describe('#start', () => {
|
|||
|
||||
it('filters available apps based on returned navLinks', async () => {
|
||||
const service = new CapabilitiesService();
|
||||
const startContract = await service.start({ apps, legacyApps, injectedMetadata });
|
||||
expect(startContract.availableApps).toEqual(new Map([['app1', { id: 'app1' }]]));
|
||||
const startContract = await service.start({ apps, legacyApps, http });
|
||||
expect(startContract.availableApps).toEqual(
|
||||
new Map([
|
||||
['app1', { id: 'app1' }],
|
||||
['appMissingInCapabilities', { id: 'appMissingInCapabilities' }],
|
||||
])
|
||||
);
|
||||
expect(startContract.availableLegacyApps).toEqual(
|
||||
new Map([['legacyApp1', { id: 'legacyApp1' }]])
|
||||
);
|
||||
|
@ -63,7 +71,7 @@ describe('#start', () => {
|
|||
const { capabilities } = await service.start({
|
||||
apps,
|
||||
legacyApps,
|
||||
injectedMetadata,
|
||||
http,
|
||||
});
|
||||
|
||||
// @ts-ignore TypeScript knows this shouldn't be possible
|
||||
|
|
|
@ -17,38 +17,18 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Capabilities } from '../../../types/capabilities';
|
||||
import { deepFreeze, RecursiveReadonly } from '../../../utils';
|
||||
import { LegacyApp, App } from '../types';
|
||||
import { InjectedMetadataStart } from '../../injected_metadata';
|
||||
import { HttpStart } from '../../http';
|
||||
|
||||
interface StartDeps {
|
||||
apps: ReadonlyMap<string, App>;
|
||||
legacyApps: ReadonlyMap<string, LegacyApp>;
|
||||
injectedMetadata: InjectedMetadataStart;
|
||||
http: HttpStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface Capabilities {
|
||||
/** Navigation link capabilities. */
|
||||
navLinks: Record<string, boolean>;
|
||||
|
||||
/** Management section capabilities. */
|
||||
management: {
|
||||
[sectionId: string]: Record<string, boolean>;
|
||||
};
|
||||
|
||||
/** Catalogue capabilities. Catalogue entries drive the visibility of the Kibana homepage options. */
|
||||
catalogue: Record<string, boolean>;
|
||||
|
||||
/** Custom capabilities, registered by plugins. */
|
||||
[key: string]: Record<string, boolean | Record<string, boolean>>;
|
||||
}
|
||||
export { Capabilities };
|
||||
|
||||
/** @internal */
|
||||
export interface CapabilitiesStart {
|
||||
|
@ -62,12 +42,9 @@ export interface CapabilitiesStart {
|
|||
* @internal
|
||||
*/
|
||||
export class CapabilitiesService {
|
||||
public async start({
|
||||
apps,
|
||||
legacyApps,
|
||||
injectedMetadata,
|
||||
}: StartDeps): Promise<CapabilitiesStart> {
|
||||
const capabilities = deepFreeze(injectedMetadata.getCapabilities());
|
||||
public async start({ apps, legacyApps, http }: StartDeps): Promise<CapabilitiesStart> {
|
||||
const capabilities = await this.fetchCapabilities(http, [...apps.keys(), ...legacyApps.keys()]);
|
||||
|
||||
const availableApps = new Map(
|
||||
[...apps].filter(
|
||||
([appId]) =>
|
||||
|
@ -88,4 +65,18 @@ export class CapabilitiesService {
|
|||
capabilities,
|
||||
};
|
||||
}
|
||||
|
||||
private async fetchCapabilities(http: HttpStart, appIds: string[]): Promise<Capabilities> {
|
||||
const payload = JSON.stringify({
|
||||
applications: appIds,
|
||||
});
|
||||
|
||||
const url = http.anonymousPaths.isAnonymous(window.location.pathname)
|
||||
? '/api/core/capabilities/defaults'
|
||||
: '/api/core/capabilities';
|
||||
const capabilities = await http.post(url, {
|
||||
body: payload,
|
||||
});
|
||||
return deepFreeze(capabilities);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,11 +23,11 @@ import { BehaviorSubject } from 'rxjs';
|
|||
import { BasePath } from './base_path_service';
|
||||
import { AnonymousPaths } from './anonymous_paths';
|
||||
|
||||
type ServiceSetupMockType = jest.Mocked<HttpSetup> & {
|
||||
export type HttpSetupMock = jest.Mocked<HttpSetup> & {
|
||||
basePath: BasePath;
|
||||
};
|
||||
|
||||
const createServiceMock = ({ basePath = '' } = {}): ServiceSetupMockType => ({
|
||||
const createServiceMock = ({ basePath = '' } = {}): HttpSetupMock => ({
|
||||
fetch: jest.fn(),
|
||||
get: jest.fn(),
|
||||
head: jest.fn(),
|
||||
|
|
|
@ -23,7 +23,6 @@ const createSetupContractMock = () => {
|
|||
getBasePath: jest.fn(),
|
||||
getKibanaVersion: jest.fn(),
|
||||
getKibanaBranch: jest.fn(),
|
||||
getCapabilities: jest.fn(),
|
||||
getCspConfig: jest.fn(),
|
||||
getLegacyMode: jest.fn(),
|
||||
getLegacyMetadata: jest.fn(),
|
||||
|
@ -32,7 +31,6 @@ const createSetupContractMock = () => {
|
|||
getInjectedVars: jest.fn(),
|
||||
getKibanaBuildNumber: jest.fn(),
|
||||
};
|
||||
setupContract.getCapabilities.mockReturnValue({} as any);
|
||||
setupContract.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true });
|
||||
setupContract.getKibanaVersion.mockReturnValue('kibanaVersion');
|
||||
setupContract.getLegacyMode.mockReturnValue(true);
|
||||
|
|
|
@ -26,7 +26,6 @@ import {
|
|||
UserProvidedValues,
|
||||
} from '../../server/types';
|
||||
import { deepFreeze } from '../../utils/';
|
||||
import { Capabilities } from '..';
|
||||
|
||||
/** @public */
|
||||
export interface LegacyNavLink {
|
||||
|
@ -64,7 +63,6 @@ export interface InjectedMetadataParams {
|
|||
packageInfo: Readonly<PackageInfo>;
|
||||
};
|
||||
uiPlugins: InjectedPluginMetadata[];
|
||||
capabilities: Capabilities;
|
||||
legacyMode: boolean;
|
||||
legacyMetadata: {
|
||||
app: unknown;
|
||||
|
@ -114,10 +112,6 @@ export class InjectedMetadataService {
|
|||
return this.state.version;
|
||||
},
|
||||
|
||||
getCapabilities: () => {
|
||||
return this.state.capabilities;
|
||||
},
|
||||
|
||||
getCspConfig: () => {
|
||||
return this.state.csp;
|
||||
},
|
||||
|
@ -163,7 +157,6 @@ export interface InjectedMetadataSetup {
|
|||
getKibanaBuildNumber: () => number;
|
||||
getKibanaBranch: () => string;
|
||||
getKibanaVersion: () => string;
|
||||
getCapabilities: () => Capabilities;
|
||||
getCspConfig: () => {
|
||||
warnLegacyBrowsers: boolean;
|
||||
};
|
||||
|
|
50
src/core/server/capabilities/capabilities_service.mock.ts
Normal file
50
src/core/server/capabilities/capabilities_service.mock.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { CapabilitiesService, CapabilitiesSetup, CapabilitiesStart } from './capabilities_service';
|
||||
|
||||
const createSetupContractMock = () => {
|
||||
const setupContract: jest.Mocked<CapabilitiesSetup> = {
|
||||
registerProvider: jest.fn(),
|
||||
registerSwitcher: jest.fn(),
|
||||
};
|
||||
return setupContract;
|
||||
};
|
||||
|
||||
const createStartContractMock = () => {
|
||||
const setupContract: jest.Mocked<CapabilitiesStart> = {
|
||||
resolveCapabilities: jest.fn().mockReturnValue(Promise.resolve({})),
|
||||
};
|
||||
return setupContract;
|
||||
};
|
||||
|
||||
type CapabilitiesServiceContract = PublicMethodsOf<CapabilitiesService>;
|
||||
const createMock = () => {
|
||||
const mocked: jest.Mocked<CapabilitiesServiceContract> = {
|
||||
setup: jest.fn().mockReturnValue(createSetupContractMock()),
|
||||
start: jest.fn().mockReturnValue(createStartContractMock()),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
||||
export const capabilitiesServiceMock = {
|
||||
create: createMock,
|
||||
createSetupContract: createSetupContractMock,
|
||||
createStartContract: createStartContractMock,
|
||||
};
|
188
src/core/server/capabilities/capabilities_service.test.ts
Normal file
188
src/core/server/capabilities/capabilities_service.test.ts
Normal file
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* 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 { httpServiceMock, HttpServiceSetupMock } from '../http/http_service.mock';
|
||||
import { mockRouter, RouterMock } from '../http/router/router.mock';
|
||||
import { CapabilitiesService, CapabilitiesSetup } from './capabilities_service';
|
||||
import { mockCoreContext } from '../core_context.mock';
|
||||
|
||||
describe('CapabilitiesService', () => {
|
||||
let http: HttpServiceSetupMock;
|
||||
let service: CapabilitiesService;
|
||||
let setup: CapabilitiesSetup;
|
||||
let router: RouterMock;
|
||||
|
||||
beforeEach(() => {
|
||||
http = httpServiceMock.createSetupContract();
|
||||
router = mockRouter.create();
|
||||
http.createRouter.mockReturnValue(router);
|
||||
service = new CapabilitiesService(mockCoreContext.create());
|
||||
});
|
||||
|
||||
describe('#setup()', () => {
|
||||
beforeEach(() => {
|
||||
setup = service.setup({ http });
|
||||
});
|
||||
|
||||
it('registers the capabilities routes', async () => {
|
||||
expect(http.createRouter).toHaveBeenCalledWith('/api/core/capabilities');
|
||||
expect(router.post).toHaveBeenCalledTimes(2);
|
||||
expect(router.post).toHaveBeenCalledWith(expect.any(Object), expect.any(Function));
|
||||
});
|
||||
|
||||
it('allows to register a capabilities provider', async () => {
|
||||
setup.registerProvider(() => ({
|
||||
navLinks: { myLink: true },
|
||||
catalogue: { myPlugin: true },
|
||||
}));
|
||||
const start = service.start();
|
||||
expect(await start.resolveCapabilities({} as any)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"catalogue": Object {
|
||||
"myPlugin": true,
|
||||
},
|
||||
"management": Object {},
|
||||
"navLinks": Object {
|
||||
"myLink": true,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('allows to register multiple capabilities providers', async () => {
|
||||
setup.registerProvider(() => ({
|
||||
navLinks: { A: true },
|
||||
catalogue: { A: true },
|
||||
}));
|
||||
setup.registerProvider(() => ({
|
||||
navLinks: { B: true },
|
||||
catalogue: { B: true },
|
||||
}));
|
||||
setup.registerProvider(() => ({
|
||||
navLinks: { C: true },
|
||||
customSection: {
|
||||
C: true,
|
||||
},
|
||||
}));
|
||||
const start = service.start();
|
||||
expect(await start.resolveCapabilities({} as any)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"catalogue": Object {
|
||||
"A": true,
|
||||
"B": true,
|
||||
},
|
||||
"customSection": Object {
|
||||
"C": true,
|
||||
},
|
||||
"management": Object {},
|
||||
"navLinks": Object {
|
||||
"A": true,
|
||||
"B": true,
|
||||
"C": true,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('allows to register capabilities switchers', async () => {
|
||||
setup.registerProvider(() => ({
|
||||
catalogue: { a: true, b: true, c: true },
|
||||
}));
|
||||
setup.registerSwitcher((req, capabilities) => {
|
||||
return {
|
||||
...capabilities,
|
||||
catalogue: {
|
||||
...capabilities.catalogue,
|
||||
b: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
const start = service.start();
|
||||
expect(await start.resolveCapabilities({} as any)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"catalogue": Object {
|
||||
"a": true,
|
||||
"b": false,
|
||||
"c": true,
|
||||
},
|
||||
"management": Object {},
|
||||
"navLinks": Object {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('allows to register multiple providers and switchers', async () => {
|
||||
setup.registerProvider(() => ({
|
||||
navLinks: { a: true },
|
||||
catalogue: { a: true },
|
||||
}));
|
||||
setup.registerProvider(() => ({
|
||||
navLinks: { b: true },
|
||||
catalogue: { b: true },
|
||||
}));
|
||||
setup.registerProvider(() => ({
|
||||
navLinks: { c: true },
|
||||
catalogue: { c: true },
|
||||
customSection: {
|
||||
c: true,
|
||||
},
|
||||
}));
|
||||
setup.registerSwitcher((req, capabilities) => {
|
||||
return {
|
||||
catalogue: {
|
||||
b: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
setup.registerSwitcher((req, capabilities) => {
|
||||
return {
|
||||
navLinks: { c: false },
|
||||
};
|
||||
});
|
||||
setup.registerSwitcher((req, capabilities) => {
|
||||
return {
|
||||
customSection: {
|
||||
c: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const start = service.start();
|
||||
expect(await start.resolveCapabilities({} as any)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"catalogue": Object {
|
||||
"a": true,
|
||||
"b": false,
|
||||
"c": true,
|
||||
},
|
||||
"customSection": Object {
|
||||
"c": false,
|
||||
},
|
||||
"management": Object {},
|
||||
"navLinks": Object {
|
||||
"a": true,
|
||||
"b": true,
|
||||
"c": false,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
156
src/core/server/capabilities/capabilities_service.ts
Normal file
156
src/core/server/capabilities/capabilities_service.ts
Normal file
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* 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, CapabilitiesProvider, CapabilitiesSwitcher } from './types';
|
||||
import { CoreContext } from '../core_context';
|
||||
import { Logger } from '../logging';
|
||||
import { InternalHttpServiceSetup, KibanaRequest } from '../http';
|
||||
import { mergeCapabilities } from './merge_capabilities';
|
||||
import { getCapabilitiesResolver, CapabilitiesResolver } from './resolve_capabilities';
|
||||
import { registerRoutes } from './routes';
|
||||
|
||||
/**
|
||||
* APIs to manage the {@link Capabilities} that will be used by the application.
|
||||
*
|
||||
* Plugins relying on capabilities to toggle some of their features should register them during the setup phase
|
||||
* using the `registerProvider` method.
|
||||
*
|
||||
* Plugins having the responsibility to restrict capabilities depending on a given context should register
|
||||
* their capabilities switcher using the `registerSwitcher` method.
|
||||
*
|
||||
* Refers to the methods documentation for complete description and examples.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface CapabilitiesSetup {
|
||||
/**
|
||||
* Register a {@link CapabilitiesProvider} to be used to provide {@link Capabilities}
|
||||
* when resolving them.
|
||||
*
|
||||
* @example
|
||||
* How to register a plugin's capabilities during setup
|
||||
* ```ts
|
||||
* // my-plugin/server/plugin.ts
|
||||
* public setup(core: CoreSetup, deps: {}) {
|
||||
* core.capabilities.registerProvider(() => {
|
||||
* return {
|
||||
* catalogue: {
|
||||
* myPlugin: true,
|
||||
* },
|
||||
* myPlugin: {
|
||||
* someFeature: true,
|
||||
* featureDisabledByDefault: false,
|
||||
* },
|
||||
* }
|
||||
* });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
registerProvider(provider: CapabilitiesProvider): void;
|
||||
|
||||
/**
|
||||
* Register a {@link CapabilitiesSwitcher} to be used to change the default state
|
||||
* of the {@link Capabilities} entries when resolving them.
|
||||
*
|
||||
* A capabilities switcher can only change the state of existing capabilities.
|
||||
* Capabilities added or removed when invoking the switcher will be ignored.
|
||||
*
|
||||
* @example
|
||||
* How to restrict some capabilities
|
||||
* ```ts
|
||||
* // my-plugin/server/plugin.ts
|
||||
* public setup(core: CoreSetup, deps: {}) {
|
||||
* core.capabilities.registerSwitcher((request, capabilities) => {
|
||||
* if(myPluginApi.shouldRestrictSomePluginBecauseOf(request)) {
|
||||
* return {
|
||||
* somePlugin: {
|
||||
* featureEnabledByDefault: false // `featureEnabledByDefault` will be disabled. All other capabilities will remain unchanged.
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* return {}; // All capabilities will remain unchanged.
|
||||
* });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
registerSwitcher(switcher: CapabilitiesSwitcher): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* APIs to access the application {@link Capabilities}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface CapabilitiesStart {
|
||||
/**
|
||||
* Resolve the {@link Capabilities} to be used for given request
|
||||
*/
|
||||
resolveCapabilities(request: KibanaRequest): Promise<Capabilities>;
|
||||
}
|
||||
|
||||
interface SetupDeps {
|
||||
http: InternalHttpServiceSetup;
|
||||
}
|
||||
|
||||
const defaultCapabilities: Capabilities = {
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {},
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export class CapabilitiesService {
|
||||
private readonly logger: Logger;
|
||||
private readonly capabilitiesProviders: CapabilitiesProvider[] = [];
|
||||
private readonly capabilitiesSwitchers: CapabilitiesSwitcher[] = [];
|
||||
private readonly resolveCapabilities: CapabilitiesResolver;
|
||||
|
||||
constructor(core: CoreContext) {
|
||||
this.logger = core.logger.get('capabilities-service');
|
||||
this.resolveCapabilities = getCapabilitiesResolver(
|
||||
() =>
|
||||
mergeCapabilities(
|
||||
defaultCapabilities,
|
||||
...this.capabilitiesProviders.map(provider => provider())
|
||||
),
|
||||
() => this.capabilitiesSwitchers
|
||||
);
|
||||
}
|
||||
|
||||
public setup(setupDeps: SetupDeps): CapabilitiesSetup {
|
||||
this.logger.debug('Setting up capabilities service');
|
||||
|
||||
registerRoutes(setupDeps.http, this.resolveCapabilities);
|
||||
|
||||
return {
|
||||
registerProvider: (provider: CapabilitiesProvider) => {
|
||||
this.capabilitiesProviders.push(provider);
|
||||
},
|
||||
registerSwitcher: (switcher: CapabilitiesSwitcher) => {
|
||||
this.capabilitiesSwitchers.push(switcher);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public start(): CapabilitiesStart {
|
||||
return {
|
||||
resolveCapabilities: request => this.resolveCapabilities(request, []),
|
||||
};
|
||||
}
|
||||
}
|
21
src/core/server/capabilities/index.ts
Normal file
21
src/core/server/capabilities/index.ts
Normal 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 { CapabilitiesService, CapabilitiesSetup, CapabilitiesStart } from './capabilities_service';
|
||||
export { Capabilities, CapabilitiesSwitcher, CapabilitiesProvider } from './types';
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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 supertest from 'supertest';
|
||||
import { HttpService, InternalHttpServiceSetup } from '../../http';
|
||||
import { contextServiceMock } from '../../context/context_service.mock';
|
||||
import { loggingServiceMock } from '../../logging/logging_service.mock';
|
||||
import { Env } from '../../config';
|
||||
import { getEnvOptions } from '../../config/__mocks__/env';
|
||||
import { CapabilitiesService, CapabilitiesSetup } from '..';
|
||||
import { createHttpServer } from '../../http/test_utils';
|
||||
|
||||
const coreId = Symbol('core');
|
||||
const env = Env.createDefault(getEnvOptions());
|
||||
|
||||
describe('CapabilitiesService', () => {
|
||||
let server: HttpService;
|
||||
let httpSetup: InternalHttpServiceSetup;
|
||||
|
||||
let service: CapabilitiesService;
|
||||
let serviceSetup: CapabilitiesSetup;
|
||||
|
||||
beforeEach(async () => {
|
||||
server = createHttpServer();
|
||||
httpSetup = await server.setup({
|
||||
context: contextServiceMock.createSetupContract(),
|
||||
});
|
||||
service = new CapabilitiesService({
|
||||
coreId,
|
||||
env,
|
||||
logger: loggingServiceMock.create(),
|
||||
configService: {} as any,
|
||||
});
|
||||
serviceSetup = await service.setup({ http: httpSetup });
|
||||
await server.start();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.stop();
|
||||
});
|
||||
|
||||
describe('/api/core/capabilities route', () => {
|
||||
it('is exposed', async () => {
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.post('/api/core/capabilities')
|
||||
.send({ applications: [] })
|
||||
.expect(200);
|
||||
expect(result.body).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"catalogue": Object {},
|
||||
"management": Object {},
|
||||
"navLinks": Object {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('uses the service capabilities providers', async () => {
|
||||
serviceSetup.registerProvider(() => ({
|
||||
catalogue: {
|
||||
something: true,
|
||||
},
|
||||
}));
|
||||
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.post('/api/core/capabilities')
|
||||
.send({ applications: [] })
|
||||
.expect(200);
|
||||
expect(result.body).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"catalogue": Object {
|
||||
"something": true,
|
||||
},
|
||||
"management": Object {},
|
||||
"navLinks": Object {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
76
src/core/server/capabilities/merge_capabilities.test.ts
Normal file
76
src/core/server/capabilities/merge_capabilities.test.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 { mergeCapabilities } from './merge_capabilities';
|
||||
|
||||
describe('mergeCapabilities', () => {
|
||||
it('merges empty object with non-empty object', () => {
|
||||
const capabilities = mergeCapabilities({ foo: {} }, { foo: { bar: true } });
|
||||
expect(capabilities).toEqual({ foo: { bar: true } });
|
||||
});
|
||||
|
||||
it('merges nested object properties', () => {
|
||||
const capabilities = mergeCapabilities({ foo: { baz: true } }, { foo: { bar: true } });
|
||||
expect(capabilities).toEqual({ foo: { bar: true, baz: true } });
|
||||
});
|
||||
|
||||
it('merges all object properties', () => {
|
||||
const capabilities = mergeCapabilities({ foo: { bar: true } }, { hello: { dolly: true } });
|
||||
expect(capabilities).toEqual({ foo: { bar: true }, hello: { dolly: true } });
|
||||
});
|
||||
|
||||
it('merges boolean as same path if they are equals', () => {
|
||||
const capabilities = mergeCapabilities(
|
||||
{ foo: { bar: true, dolly: false, a: true } },
|
||||
{ foo: { bar: true, dolly: false, b: false } }
|
||||
);
|
||||
expect(capabilities).toEqual({ foo: { bar: true, dolly: false, a: true, b: false } });
|
||||
});
|
||||
|
||||
it('throws if boolean at same path are not equals', () => {
|
||||
expect(() => {
|
||||
mergeCapabilities({ foo: { bar: false } }, { foo: { bar: true } });
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"conflict trying to merge booleans with different values"`
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
mergeCapabilities({ foo: { bar: true } }, { foo: { bar: false } });
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"conflict trying to merge booleans with different values"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if value as same path is boolean on left and object on right', () => {
|
||||
expect(() => {
|
||||
mergeCapabilities({ foo: { bar: false } }, { foo: { bar: {} } });
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"conflict trying to merge boolean with object"`);
|
||||
expect(() => {
|
||||
mergeCapabilities({ foo: { bar: false } }, { foo: { bar: { baz: false } } });
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"conflict trying to merge boolean with object"`);
|
||||
});
|
||||
|
||||
it('should not alter the input capabilities', () => {
|
||||
const left = { foo: { bar: true } };
|
||||
const right = { hello: { dolly: true } };
|
||||
mergeCapabilities(left, right);
|
||||
expect(left).toEqual({ foo: { bar: true } });
|
||||
expect(right).toEqual({ hello: { dolly: true } });
|
||||
});
|
||||
});
|
|
@ -17,28 +17,19 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import typeDetect from 'type-detect';
|
||||
import { merge } from 'lodash';
|
||||
import { Capabilities } from '../../../core/public';
|
||||
import { Capabilities } from './types';
|
||||
|
||||
export const mergeCapabilities = (...sources: Array<Partial<Capabilities>>): Capabilities =>
|
||||
merge(
|
||||
{
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {},
|
||||
},
|
||||
...sources,
|
||||
(a: any, b: any) => {
|
||||
if (
|
||||
(typeDetect(a) === 'boolean' && typeDetect(b) === 'Object') ||
|
||||
(typeDetect(b) === 'boolean' && typeDetect(a) === 'Object')
|
||||
) {
|
||||
throw new Error(`a boolean and an object can't be merged`);
|
||||
}
|
||||
|
||||
if (typeDetect(a) === 'boolean' && typeDetect(b) === 'boolean' && a !== b) {
|
||||
throw new Error(`"true" and "false" can't be merged`);
|
||||
}
|
||||
merge({}, ...sources, (a: any, b: any) => {
|
||||
if (
|
||||
(typeof a === 'boolean' && typeof b === 'object') ||
|
||||
(typeof a === 'object' && typeof b === 'boolean')
|
||||
) {
|
||||
throw new Error(`conflict trying to merge boolean with object`);
|
||||
}
|
||||
);
|
||||
|
||||
if (typeof a === 'boolean' && typeof b === 'boolean' && a !== b) {
|
||||
throw new Error(`conflict trying to merge booleans with different values`);
|
||||
}
|
||||
});
|
164
src/core/server/capabilities/resolve_capabilities.test.ts
Normal file
164
src/core/server/capabilities/resolve_capabilities.test.ts
Normal file
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* 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 './types';
|
||||
import { resolveCapabilities } from './resolve_capabilities';
|
||||
import { KibanaRequest } from '../http';
|
||||
import { httpServerMock } from '../http/http_server.mocks';
|
||||
|
||||
describe('resolveCapabilities', () => {
|
||||
let defaultCaps: Capabilities;
|
||||
let request: KibanaRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
defaultCaps = {
|
||||
navLinks: {},
|
||||
catalogue: {},
|
||||
management: {},
|
||||
};
|
||||
request = httpServerMock.createKibanaRequest();
|
||||
});
|
||||
|
||||
it('returns the initial capabilities if no switcher are used', async () => {
|
||||
const result = await resolveCapabilities(defaultCaps, [], request, []);
|
||||
expect(result).toEqual(defaultCaps);
|
||||
});
|
||||
|
||||
it('applies the switcher to the capabilities ', async () => {
|
||||
const caps = {
|
||||
...defaultCaps,
|
||||
catalogue: {
|
||||
A: true,
|
||||
B: true,
|
||||
},
|
||||
};
|
||||
const switcher = (req: KibanaRequest, capabilities: Capabilities) => ({
|
||||
...capabilities,
|
||||
catalogue: {
|
||||
...capabilities.catalogue,
|
||||
A: false,
|
||||
},
|
||||
});
|
||||
const result = await resolveCapabilities(caps, [switcher], request, []);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"catalogue": Object {
|
||||
"A": false,
|
||||
"B": true,
|
||||
},
|
||||
"management": Object {},
|
||||
"navLinks": Object {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not mutate the input capabilities', async () => {
|
||||
const caps = {
|
||||
...defaultCaps,
|
||||
catalogue: {
|
||||
A: true,
|
||||
B: true,
|
||||
},
|
||||
};
|
||||
const switcher = (req: KibanaRequest, capabilities: Capabilities) => ({
|
||||
...capabilities,
|
||||
catalogue: {
|
||||
...capabilities.catalogue,
|
||||
A: false,
|
||||
},
|
||||
});
|
||||
await resolveCapabilities(caps, [switcher], request, []);
|
||||
expect(caps.catalogue).toEqual({
|
||||
A: true,
|
||||
B: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores any added capability from the switcher', async () => {
|
||||
const caps = {
|
||||
...defaultCaps,
|
||||
catalogue: {
|
||||
A: true,
|
||||
B: true,
|
||||
},
|
||||
};
|
||||
const switcher = (req: KibanaRequest, capabilities: Capabilities) => ({
|
||||
...capabilities,
|
||||
catalogue: {
|
||||
...capabilities.catalogue,
|
||||
C: false,
|
||||
},
|
||||
});
|
||||
const result = await resolveCapabilities(caps, [switcher], request, []);
|
||||
expect(result.catalogue).toEqual({
|
||||
A: true,
|
||||
B: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores any removed capability from the switcher', async () => {
|
||||
const caps = {
|
||||
...defaultCaps,
|
||||
catalogue: {
|
||||
A: true,
|
||||
B: true,
|
||||
C: true,
|
||||
},
|
||||
};
|
||||
const switcher = (req: KibanaRequest, capabilities: Capabilities) => ({
|
||||
...capabilities,
|
||||
catalogue: Object.entries(capabilities.catalogue)
|
||||
.filter(([key]) => key !== 'B')
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
});
|
||||
const result = await resolveCapabilities(caps, [switcher], request, []);
|
||||
expect(result.catalogue).toEqual({
|
||||
A: true,
|
||||
B: true,
|
||||
C: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores any capability type mutation from the switcher', async () => {
|
||||
const caps = {
|
||||
...defaultCaps,
|
||||
section: {
|
||||
boolean: true,
|
||||
record: {
|
||||
entry: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const switcher = (req: KibanaRequest, capabilities: Capabilities) => ({
|
||||
section: {
|
||||
boolean: {
|
||||
entry: false,
|
||||
},
|
||||
record: false,
|
||||
},
|
||||
});
|
||||
const result = await resolveCapabilities(caps, [switcher], request, []);
|
||||
expect(result.section).toEqual({
|
||||
boolean: true,
|
||||
record: {
|
||||
entry: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
85
src/core/server/capabilities/resolve_capabilities.ts
Normal file
85
src/core/server/capabilities/resolve_capabilities.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { Capabilities, CapabilitiesSwitcher } from './types';
|
||||
import { KibanaRequest } from '../http';
|
||||
|
||||
export type CapabilitiesResolver = (
|
||||
request: KibanaRequest,
|
||||
applications: string[]
|
||||
) => Promise<Capabilities>;
|
||||
|
||||
export const getCapabilitiesResolver = (
|
||||
capabilities: () => Capabilities,
|
||||
switchers: () => CapabilitiesSwitcher[]
|
||||
): CapabilitiesResolver => async (
|
||||
request: KibanaRequest,
|
||||
applications: string[]
|
||||
): Promise<Capabilities> => {
|
||||
return resolveCapabilities(capabilities(), switchers(), request, applications);
|
||||
};
|
||||
|
||||
export const resolveCapabilities = async (
|
||||
capabilities: Capabilities,
|
||||
switchers: CapabilitiesSwitcher[],
|
||||
request: KibanaRequest,
|
||||
applications: string[]
|
||||
): Promise<Capabilities> => {
|
||||
const mergedCaps = cloneDeep({
|
||||
...capabilities,
|
||||
navLinks: applications.reduce(
|
||||
(acc, app) => ({
|
||||
...acc,
|
||||
[app]: true,
|
||||
}),
|
||||
capabilities.navLinks
|
||||
),
|
||||
});
|
||||
return switchers.reduce(async (caps, switcher) => {
|
||||
const resolvedCaps = await caps;
|
||||
const changes = await switcher(request, resolvedCaps);
|
||||
return recursiveApplyChanges(resolvedCaps, changes);
|
||||
}, Promise.resolve(mergedCaps));
|
||||
};
|
||||
|
||||
function recursiveApplyChanges<
|
||||
TDestination extends Record<string, any>,
|
||||
TSource extends Record<string, any>
|
||||
>(destination: TDestination, source: TSource): TDestination {
|
||||
return Object.keys(destination)
|
||||
.map(key => {
|
||||
const orig = destination[key];
|
||||
const changed = source[key];
|
||||
if (changed == null) {
|
||||
return [key, orig];
|
||||
}
|
||||
if (typeof orig === 'object' && typeof changed === 'object') {
|
||||
return [key, recursiveApplyChanges(orig, changed)];
|
||||
}
|
||||
return [key, typeof orig === typeof changed ? changed : orig];
|
||||
})
|
||||
.reduce(
|
||||
(acc, [key, value]) => ({
|
||||
...acc,
|
||||
[key]: value,
|
||||
}),
|
||||
{} as TDestination
|
||||
);
|
||||
}
|
27
src/core/server/capabilities/routes/index.ts
Normal file
27
src/core/server/capabilities/routes/index.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { CapabilitiesResolver } from '../resolve_capabilities';
|
||||
import { InternalHttpServiceSetup } from '../../http';
|
||||
import { registerCapabilitiesRoutes } from './resolve_capabilities';
|
||||
|
||||
export function registerRoutes(http: InternalHttpServiceSetup, resolver: CapabilitiesResolver) {
|
||||
const router = http.createRouter('/api/core/capabilities');
|
||||
registerCapabilitiesRoutes(router, resolver);
|
||||
}
|
51
src/core/server/capabilities/routes/resolve_capabilities.ts
Normal file
51
src/core/server/capabilities/routes/resolve_capabilities.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { IRouter } from '../../http';
|
||||
import { CapabilitiesResolver } from '../resolve_capabilities';
|
||||
|
||||
export function registerCapabilitiesRoutes(router: IRouter, resolver: CapabilitiesResolver) {
|
||||
// Capabilities are fetched on both authenticated and anonymous routes.
|
||||
// However when `authRequired` is false, authentication is not performed
|
||||
// and only default capabilities are returned (all disabled), even for authenticated users.
|
||||
// So we need two endpoints to handle both scenarios.
|
||||
[true, false].forEach(authRequired => {
|
||||
router.post(
|
||||
{
|
||||
path: authRequired ? '' : '/defaults',
|
||||
options: {
|
||||
authRequired,
|
||||
},
|
||||
validate: {
|
||||
body: schema.object({
|
||||
applications: schema.arrayOf(schema.string()),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
const { applications } = req.body;
|
||||
const capabilities = await resolver(req, applications);
|
||||
return res.ok({
|
||||
body: capabilities,
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
|
@ -17,23 +17,22 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Capabilities } from './capabilities_service';
|
||||
import { Capabilities } from '../../types/capabilities';
|
||||
import { KibanaRequest } from '../http';
|
||||
|
||||
export const mergeCapabilities = (...sources: Array<Partial<Capabilities>>) =>
|
||||
sources.reduce(
|
||||
(capabilities, source) => {
|
||||
Object.entries(source).forEach(([key, value]) => {
|
||||
capabilities[key] = {
|
||||
...value,
|
||||
...capabilities[key],
|
||||
};
|
||||
});
|
||||
export { Capabilities };
|
||||
|
||||
return capabilities;
|
||||
},
|
||||
{
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {},
|
||||
}
|
||||
);
|
||||
/**
|
||||
* See {@link CapabilitiesSetup}
|
||||
* @public
|
||||
*/
|
||||
export type CapabilitiesProvider = () => Partial<Capabilities>;
|
||||
|
||||
/**
|
||||
* See {@link CapabilitiesSetup}
|
||||
* @public
|
||||
*/
|
||||
export type CapabilitiesSwitcher = (
|
||||
request: KibanaRequest,
|
||||
uiCapabilities: Capabilities
|
||||
) => Partial<Capabilities> | Promise<Partial<Capabilities>>;
|
|
@ -18,15 +18,15 @@
|
|||
*/
|
||||
|
||||
import { Server } from 'hapi';
|
||||
import { mockRouter } from './router/router.mock';
|
||||
import { InternalHttpServiceSetup } from './types';
|
||||
import { HttpService } from './http_service';
|
||||
import { OnPreAuthToolkit } from './lifecycle/on_pre_auth';
|
||||
import { AuthToolkit } from './lifecycle/auth';
|
||||
import { sessionStorageMock } from './cookie_session_storage.mocks';
|
||||
import { IRouter } from './router';
|
||||
import { OnPostAuthToolkit } from './lifecycle/on_post_auth';
|
||||
|
||||
type ServiceSetupMockType = jest.Mocked<InternalHttpServiceSetup> & {
|
||||
export type HttpServiceSetupMock = jest.Mocked<InternalHttpServiceSetup> & {
|
||||
basePath: jest.Mocked<InternalHttpServiceSetup['basePath']>;
|
||||
};
|
||||
|
||||
|
@ -38,19 +38,8 @@ const createBasePathMock = (): jest.Mocked<InternalHttpServiceSetup['basePath']>
|
|||
remove: jest.fn(),
|
||||
});
|
||||
|
||||
const createRouterMock = (): jest.Mocked<IRouter> => ({
|
||||
routerPath: '/',
|
||||
get: jest.fn(),
|
||||
post: jest.fn(),
|
||||
put: jest.fn(),
|
||||
patch: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
getRoutes: jest.fn(),
|
||||
handleLegacyErrors: jest.fn().mockImplementation(handler => handler),
|
||||
});
|
||||
|
||||
const createSetupContractMock = () => {
|
||||
const setupContract: ServiceSetupMockType = {
|
||||
const setupContract: HttpServiceSetupMock = {
|
||||
// we can mock other hapi server methods when we need it
|
||||
server: ({
|
||||
route: jest.fn(),
|
||||
|
@ -62,7 +51,7 @@ const createSetupContractMock = () => {
|
|||
registerAuth: jest.fn(),
|
||||
registerOnPostAuth: jest.fn(),
|
||||
registerRouteHandlerContext: jest.fn(),
|
||||
createRouter: jest.fn(),
|
||||
createRouter: jest.fn().mockImplementation(() => mockRouter.create({})),
|
||||
basePath: createBasePathMock(),
|
||||
auth: {
|
||||
get: jest.fn(),
|
||||
|
@ -75,7 +64,7 @@ const createSetupContractMock = () => {
|
|||
setupContract.createCookieSessionStorageFactory.mockResolvedValue(
|
||||
sessionStorageMock.createFactory()
|
||||
);
|
||||
setupContract.createRouter.mockImplementation(createRouterMock);
|
||||
setupContract.createRouter.mockImplementation(() => mockRouter.create());
|
||||
return setupContract;
|
||||
};
|
||||
|
||||
|
@ -110,5 +99,5 @@ export const httpServiceMock = {
|
|||
createOnPreAuthToolkit: createOnPreAuthToolkitMock,
|
||||
createOnPostAuthToolkit: createOnPostAuthToolkitMock,
|
||||
createAuthToolkit: createAuthToolkitMock,
|
||||
createRouter: createRouterMock,
|
||||
createRouter: mockRouter.create,
|
||||
};
|
||||
|
|
|
@ -18,49 +18,28 @@
|
|||
*/
|
||||
|
||||
import supertest from 'supertest';
|
||||
import { ByteSizeValue } from '@kbn/config-schema';
|
||||
import request from 'request';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { ensureRawRequest } from '../router';
|
||||
import { HttpService } from '../http_service';
|
||||
|
||||
import { CoreContext } from '../../core_context';
|
||||
import { Env } from '../../config';
|
||||
import { getEnvOptions } from '../../config/__mocks__/env';
|
||||
import { configServiceMock } from '../../config/config_service.mock';
|
||||
import { contextServiceMock } from '../../context/context_service.mock';
|
||||
import { loggingServiceMock } from '../../logging/logging_service.mock';
|
||||
import { createHttpServer } from '../test_utils';
|
||||
|
||||
let server: HttpService;
|
||||
|
||||
let logger: ReturnType<typeof loggingServiceMock.create>;
|
||||
let env: Env;
|
||||
let coreContext: CoreContext;
|
||||
const configService = configServiceMock.create();
|
||||
|
||||
const contextSetup = contextServiceMock.createSetupContract();
|
||||
|
||||
const setupDeps = {
|
||||
context: contextSetup,
|
||||
};
|
||||
configService.atPath.mockReturnValue(
|
||||
new BehaviorSubject({
|
||||
hosts: ['localhost'],
|
||||
maxPayload: new ByteSizeValue(1024),
|
||||
autoListen: true,
|
||||
ssl: {
|
||||
enabled: false,
|
||||
},
|
||||
compression: { enabled: true },
|
||||
} as any)
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
logger = loggingServiceMock.create();
|
||||
env = Env.createDefault(getEnvOptions());
|
||||
|
||||
coreContext = { coreId: Symbol('core'), env, logger, configService: configService as any };
|
||||
server = new HttpService(coreContext);
|
||||
server = createHttpServer({ logger });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
|
@ -18,49 +18,28 @@
|
|||
*/
|
||||
import { Stream } from 'stream';
|
||||
import Boom from 'boom';
|
||||
|
||||
import supertest from 'supertest';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { ByteSizeValue, schema } from '@kbn/config-schema';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
import { HttpService } from '../http_service';
|
||||
|
||||
import { CoreContext } from '../../core_context';
|
||||
import { Env } from '../../config';
|
||||
import { getEnvOptions } from '../../config/__mocks__/env';
|
||||
import { configServiceMock } from '../../config/config_service.mock';
|
||||
import { contextServiceMock } from '../../context/context_service.mock';
|
||||
import { loggingServiceMock } from '../../logging/logging_service.mock';
|
||||
import { createHttpServer } from '../test_utils';
|
||||
|
||||
let server: HttpService;
|
||||
|
||||
let logger: ReturnType<typeof loggingServiceMock.create>;
|
||||
let env: Env;
|
||||
let coreContext: CoreContext;
|
||||
const configService = configServiceMock.create();
|
||||
const contextSetup = contextServiceMock.createSetupContract();
|
||||
|
||||
const setupDeps = {
|
||||
context: contextSetup,
|
||||
};
|
||||
configService.atPath.mockReturnValue(
|
||||
new BehaviorSubject({
|
||||
hosts: ['localhost'],
|
||||
maxPayload: new ByteSizeValue(1024),
|
||||
autoListen: true,
|
||||
ssl: {
|
||||
enabled: false,
|
||||
},
|
||||
compression: { enabled: true },
|
||||
} as any)
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
logger = loggingServiceMock.create();
|
||||
env = Env.createDefault(getEnvOptions());
|
||||
|
||||
coreContext = { coreId: Symbol('core'), env, logger, configService: configService as any };
|
||||
server = new HttpService(coreContext);
|
||||
server = createHttpServer({ logger });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
|
@ -17,18 +17,23 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Request } from 'hapi';
|
||||
import { IRouter } from './router';
|
||||
|
||||
import { Capabilities } from '../../../core/public';
|
||||
import { mergeCapabilities } from './merge_capabilities';
|
||||
import { CapabilitiesModifier } from './capabilities_mixin';
|
||||
export type RouterMock = DeeplyMockedKeys<IRouter>;
|
||||
|
||||
export const resolveCapabilities = (
|
||||
request: Request,
|
||||
modifiers: CapabilitiesModifier[],
|
||||
...capabilities: Array<Partial<Capabilities>>
|
||||
) =>
|
||||
modifiers.reduce(
|
||||
async (resolvedCaps, modifier) => modifier(request, await resolvedCaps),
|
||||
Promise.resolve(mergeCapabilities(...capabilities))
|
||||
);
|
||||
function create({ routerPath = '' }: { routerPath?: string } = {}): RouterMock {
|
||||
return {
|
||||
routerPath,
|
||||
get: jest.fn(),
|
||||
post: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
put: jest.fn(),
|
||||
patch: jest.fn(),
|
||||
getRoutes: jest.fn(),
|
||||
handleLegacyErrors: jest.fn().mockImplementation(handler => handler),
|
||||
};
|
||||
}
|
||||
|
||||
export const mockRouter = {
|
||||
create,
|
||||
};
|
63
src/core/server/http/test_utils.ts
Normal file
63
src/core/server/http/test_utils.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { BehaviorSubject } from 'rxjs';
|
||||
import { ByteSizeValue } from '@kbn/config-schema';
|
||||
import { Env } from '../config';
|
||||
import { getEnvOptions } from '../config/__mocks__/env';
|
||||
import { HttpService } from './http_service';
|
||||
import { CoreContext } from '../core_context';
|
||||
import { configServiceMock } from '../config/config_service.mock';
|
||||
import { loggingServiceMock } from '../logging/logging_service.mock';
|
||||
|
||||
const coreId = Symbol('core');
|
||||
const env = Env.createDefault(getEnvOptions());
|
||||
|
||||
const logger = loggingServiceMock.create();
|
||||
|
||||
const configService = configServiceMock.create();
|
||||
configService.atPath.mockReturnValue(
|
||||
new BehaviorSubject({
|
||||
hosts: ['localhost'],
|
||||
maxPayload: new ByteSizeValue(1024),
|
||||
autoListen: true,
|
||||
ssl: {
|
||||
enabled: false,
|
||||
},
|
||||
compression: { enabled: true },
|
||||
} as any)
|
||||
);
|
||||
|
||||
const defaultContext: CoreContext = {
|
||||
coreId,
|
||||
env,
|
||||
logger,
|
||||
configService,
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a concrete HttpServer with a mocked context.
|
||||
*/
|
||||
export const createHttpServer = (overrides: Partial<CoreContext> = {}): HttpService => {
|
||||
const context = {
|
||||
...defaultContext,
|
||||
...overrides,
|
||||
};
|
||||
return new HttpService(context);
|
||||
};
|
|
@ -46,8 +46,10 @@ import { ContextSetup } from './context';
|
|||
import { IUiSettingsClient, UiSettingsServiceSetup } from './ui_settings';
|
||||
import { SavedObjectsClientContract } from './saved_objects/types';
|
||||
import { SavedObjectsServiceSetup, SavedObjectsServiceStart } from './saved_objects';
|
||||
import { CapabilitiesSetup, CapabilitiesStart } from './capabilities';
|
||||
|
||||
export { bootstrap } from './bootstrap';
|
||||
export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities';
|
||||
export { ConfigPath, ConfigService, EnvironmentMode, PackageInfo } from './config';
|
||||
export {
|
||||
IContextContainer,
|
||||
|
@ -240,6 +242,8 @@ export interface RequestHandlerContext {
|
|||
* @public
|
||||
*/
|
||||
export interface CoreSetup {
|
||||
/** {@link CapabilitiesSetup} */
|
||||
capabilities: CapabilitiesSetup;
|
||||
/** {@link ContextSetup} */
|
||||
context: ContextSetup;
|
||||
/** {@link ElasticsearchServiceSetup} */
|
||||
|
@ -258,8 +262,17 @@ export interface CoreSetup {
|
|||
* @public
|
||||
*/
|
||||
export interface CoreStart {
|
||||
/** {@link CapabilitiesStart} */
|
||||
capabilities: CapabilitiesStart;
|
||||
/** {@link SavedObjectsServiceStart} */
|
||||
savedObjects: SavedObjectsServiceStart;
|
||||
}
|
||||
|
||||
export { ContextSetup, PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId };
|
||||
export {
|
||||
CapabilitiesSetup,
|
||||
CapabilitiesStart,
|
||||
ContextSetup,
|
||||
PluginsServiceSetup,
|
||||
PluginsServiceStart,
|
||||
PluginOpaqueId,
|
||||
};
|
||||
|
|
|
@ -25,9 +25,11 @@ import {
|
|||
InternalSavedObjectsServiceStart,
|
||||
InternalSavedObjectsServiceSetup,
|
||||
} from './saved_objects';
|
||||
import { CapabilitiesSetup, CapabilitiesStart } from './capabilities';
|
||||
|
||||
/** @internal */
|
||||
export interface InternalCoreSetup {
|
||||
capabilities: CapabilitiesSetup;
|
||||
context: ContextSetup;
|
||||
http: InternalHttpServiceSetup;
|
||||
elasticsearch: InternalElasticsearchServiceSetup;
|
||||
|
@ -39,5 +41,6 @@ export interface InternalCoreSetup {
|
|||
* @internal
|
||||
*/
|
||||
export interface InternalCoreStart {
|
||||
capabilities: CapabilitiesStart;
|
||||
savedObjects: InternalSavedObjectsServiceStart;
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ import { DiscoveredPlugin } from '../plugins';
|
|||
import { httpServiceMock } from '../http/http_service.mock';
|
||||
import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock';
|
||||
import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock';
|
||||
import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock';
|
||||
|
||||
const MockKbnServer: jest.Mock<KbnServer> = KbnServer as any;
|
||||
|
||||
|
@ -69,6 +70,7 @@ beforeEach(() => {
|
|||
|
||||
setupDeps = {
|
||||
core: {
|
||||
capabilities: capabilitiesServiceMock.createSetupContract(),
|
||||
context: contextServiceMock.createSetupContract(),
|
||||
elasticsearch: { legacy: {} } as any,
|
||||
uiSettings: uiSettingsServiceMock.createSetupContract(),
|
||||
|
@ -93,6 +95,7 @@ beforeEach(() => {
|
|||
|
||||
startDeps = {
|
||||
core: {
|
||||
capabilities: capabilitiesServiceMock.createStartContract(),
|
||||
savedObjects: savedObjectsServiceMock.createStartContract(),
|
||||
plugins: { contracts: new Map() },
|
||||
},
|
||||
|
|
|
@ -241,6 +241,7 @@ export class LegacyService implements CoreService {
|
|||
}
|
||||
) {
|
||||
const coreSetup: CoreSetup = {
|
||||
capabilities: setupDeps.core.capabilities,
|
||||
context: setupDeps.core.context,
|
||||
elasticsearch: {
|
||||
adminClient$: setupDeps.core.elasticsearch.adminClient$,
|
||||
|
@ -271,6 +272,7 @@ export class LegacyService implements CoreService {
|
|||
},
|
||||
};
|
||||
const coreStart: CoreStart = {
|
||||
capabilities: startDeps.core.capabilities,
|
||||
savedObjects: { getScopedClient: startDeps.core.savedObjects.getScopedClient },
|
||||
};
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import { contextServiceMock } from './context/context_service.mock';
|
|||
import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock';
|
||||
import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
|
||||
import { InternalCoreSetup, InternalCoreStart } from './internal_types';
|
||||
import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock';
|
||||
|
||||
export { httpServerMock } from './http/http_server.mocks';
|
||||
export { sessionStorageMock } from './http/cookie_session_storage.mocks';
|
||||
|
@ -86,6 +87,7 @@ function createCoreSetupMock() {
|
|||
register: uiSettingsServiceMock.createSetupContract().register,
|
||||
};
|
||||
const mock: MockedKeys<CoreSetup> = {
|
||||
capabilities: capabilitiesServiceMock.createSetupContract(),
|
||||
context: contextServiceMock.createSetupContract(),
|
||||
elasticsearch: elasticsearchServiceMock.createSetupContract(),
|
||||
http: httpMock,
|
||||
|
@ -98,6 +100,7 @@ function createCoreSetupMock() {
|
|||
|
||||
function createCoreStartMock() {
|
||||
const mock: MockedKeys<CoreStart> = {
|
||||
capabilities: capabilitiesServiceMock.createStartContract(),
|
||||
savedObjects: savedObjectsServiceMock.createStartContract(),
|
||||
};
|
||||
|
||||
|
@ -106,6 +109,7 @@ function createCoreStartMock() {
|
|||
|
||||
function createInternalCoreSetupMock() {
|
||||
const setupDeps: InternalCoreSetup = {
|
||||
capabilities: capabilitiesServiceMock.createSetupContract(),
|
||||
context: contextServiceMock.createSetupContract(),
|
||||
elasticsearch: elasticsearchServiceMock.createSetupContract(),
|
||||
http: httpServiceMock.createSetupContract(),
|
||||
|
@ -117,6 +121,7 @@ function createInternalCoreSetupMock() {
|
|||
|
||||
function createInternalCoreStartMock() {
|
||||
const startDeps: InternalCoreStart = {
|
||||
capabilities: capabilitiesServiceMock.createStartContract(),
|
||||
savedObjects: savedObjectsServiceMock.createStartContract(),
|
||||
};
|
||||
return startDeps;
|
||||
|
|
|
@ -102,6 +102,10 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
|
|||
plugin: PluginWrapper<TPlugin, TPluginDependencies>
|
||||
): CoreSetup {
|
||||
return {
|
||||
capabilities: {
|
||||
registerProvider: deps.capabilities.registerProvider,
|
||||
registerSwitcher: deps.capabilities.registerSwitcher,
|
||||
},
|
||||
context: {
|
||||
createContextContainer: deps.context.createContextContainer,
|
||||
},
|
||||
|
@ -153,6 +157,9 @@ export function createPluginStartContext<TPlugin, TPluginDependencies>(
|
|||
plugin: PluginWrapper<TPlugin, TPluginDependencies>
|
||||
): CoreStart {
|
||||
return {
|
||||
capabilities: {
|
||||
resolveCapabilities: deps.capabilities.resolveCapabilities,
|
||||
},
|
||||
savedObjects: { getScopedClient: deps.savedObjects.getScopedClient },
|
||||
};
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -40,11 +40,13 @@ import { mapToObject } from '../utils/';
|
|||
import { ContextService } from './context';
|
||||
import { RequestHandlerContext } from '.';
|
||||
import { InternalCoreSetup } from './internal_types';
|
||||
import { CapabilitiesService } from './capabilities';
|
||||
|
||||
const coreId = Symbol('core');
|
||||
|
||||
export class Server {
|
||||
public readonly configService: ConfigService;
|
||||
private readonly capabilities: CapabilitiesService;
|
||||
private readonly context: ContextService;
|
||||
private readonly elasticsearch: ElasticsearchService;
|
||||
private readonly http: HttpService;
|
||||
|
@ -70,6 +72,7 @@ export class Server {
|
|||
this.elasticsearch = new ElasticsearchService(core);
|
||||
this.savedObjects = new SavedObjectsService(core);
|
||||
this.uiSettings = new UiSettingsService(core);
|
||||
this.capabilities = new CapabilitiesService(core);
|
||||
}
|
||||
|
||||
public async setup() {
|
||||
|
@ -95,6 +98,8 @@ export class Server {
|
|||
|
||||
this.registerDefaultRoute(httpSetup);
|
||||
|
||||
const capabilitiesSetup = this.capabilities.setup({ http: httpSetup });
|
||||
|
||||
const elasticsearchServiceSetup = await this.elasticsearch.setup({
|
||||
http: httpSetup,
|
||||
});
|
||||
|
@ -109,6 +114,7 @@ export class Server {
|
|||
});
|
||||
|
||||
const coreSetup: InternalCoreSetup = {
|
||||
capabilities: capabilitiesSetup,
|
||||
context: contextServiceSetup,
|
||||
elasticsearch: elasticsearchServiceSetup,
|
||||
http: httpSetup,
|
||||
|
@ -131,10 +137,14 @@ export class Server {
|
|||
public async start() {
|
||||
this.log.debug('starting server');
|
||||
const savedObjectsStart = await this.savedObjects.start({});
|
||||
|
||||
const pluginsStart = await this.plugins.start({ savedObjects: savedObjectsStart });
|
||||
const capabilitiesStart = this.capabilities.start();
|
||||
const pluginsStart = await this.plugins.start({
|
||||
capabilities: capabilitiesStart,
|
||||
savedObjects: savedObjectsStart,
|
||||
});
|
||||
|
||||
const coreStart = {
|
||||
capabilities: capabilitiesStart,
|
||||
savedObjects: savedObjectsStart,
|
||||
plugins: pluginsStart,
|
||||
};
|
||||
|
|
|
@ -17,7 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export const mockRegisterCapabilitiesRoute = jest.fn();
|
||||
jest.mock('./capabilities_route', () => ({
|
||||
registerCapabilitiesRoute: mockRegisterCapabilitiesRoute,
|
||||
}));
|
||||
export { createHttpServer } from './http/test_utils';
|
41
src/core/types/capabilities.ts
Normal file
41
src/core/types/capabilities.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface Capabilities {
|
||||
/** Navigation link capabilities. */
|
||||
navLinks: Record<string, boolean>;
|
||||
|
||||
/** Management section capabilities. */
|
||||
management: {
|
||||
[sectionId: string]: Record<string, boolean>;
|
||||
};
|
||||
|
||||
/** Catalogue capabilities. Catalogue entries drive the visibility of the Kibana homepage options. */
|
||||
catalogue: Record<string, boolean>;
|
||||
|
||||
/** Custom capabilities, registered by plugins. */
|
||||
[key: string]: Record<string, boolean | Record<string, boolean>>;
|
||||
}
|
|
@ -22,3 +22,4 @@
|
|||
* types are stripped.
|
||||
*/
|
||||
export * from './core_service';
|
||||
export * from './capabilities';
|
||||
|
|
|
@ -70,9 +70,9 @@ const uiCapabilities = {
|
|||
|
||||
// Mock fetch for CoreSystem calls.
|
||||
fetchMock.config.fallbackToNetwork = true;
|
||||
fetchMock.post(/\\/api\\/capabilities/, {
|
||||
fetchMock.post(/\\/api\\/core\\/capabilities/, {
|
||||
status: 200,
|
||||
body: JSON.stringify({ capabilities: uiCapabilities }),
|
||||
body: JSON.stringify(uiCapabilities),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { Server } from '../../server/kbn_server';
|
||||
import { Capabilities } from '../../../core/public';
|
||||
import { Capabilities } from '../../../core/server';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { SavedObjectsManagementDefinition } from '../../../core/server/saved_objects/management';
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { Server } from '../server/kbn_server';
|
||||
import { Capabilities } from '../../core/public';
|
||||
import { Capabilities } from '../../core/server';
|
||||
// Disable lint errors for imports from src/core/* until SavedObjects migration is complete
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { SavedObjectsSchemaDefinition } from '../../core/server/saved_objects/schema';
|
||||
|
|
|
@ -19,81 +19,47 @@
|
|||
|
||||
import { Server } from 'hapi';
|
||||
import KbnServer from '../kbn_server';
|
||||
import { mockRegisterCapabilitiesRoute } from './capabilities_mixin.test.mocks';
|
||||
|
||||
import { capabilitiesMixin } from './capabilities_mixin';
|
||||
|
||||
describe('capabilitiesMixin', () => {
|
||||
let registerMock: jest.Mock;
|
||||
|
||||
const getKbnServer = (pluginSpecs: any[] = []) => {
|
||||
return {
|
||||
return ({
|
||||
afterPluginsInit: (callback: () => void) => callback(),
|
||||
pluginSpecs,
|
||||
} as KbnServer;
|
||||
newPlatform: {
|
||||
setup: {
|
||||
core: {
|
||||
capabilities: {
|
||||
registerProvider: registerMock,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown) as KbnServer;
|
||||
};
|
||||
|
||||
let server: Server;
|
||||
beforeEach(() => {
|
||||
server = new Server();
|
||||
server.getUiNavLinks = () => [];
|
||||
registerMock = jest.fn();
|
||||
});
|
||||
|
||||
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 } },
|
||||
}),
|
||||
},
|
||||
]);
|
||||
it('calls capabilities#registerCapabilitiesProvider for each legacy plugin specs', async () => {
|
||||
const getPluginSpec = (provider: () => any) => ({
|
||||
getUiCapabilitiesProvider: () => provider,
|
||||
});
|
||||
|
||||
const capaA = { catalogue: { A: true } };
|
||||
const capaB = { catalogue: { B: true } };
|
||||
const kbnServer = getKbnServer([getPluginSpec(() => capaA), getPluginSpec(() => capaB)]);
|
||||
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]);
|
||||
});
|
||||
|
||||
it('exposes request#getCapabilities for retrieving legacy capabilities', async () => {
|
||||
const kbnServer = getKbnServer();
|
||||
jest.spyOn(server, 'decorate');
|
||||
await capabilitiesMixin(kbnServer, server);
|
||||
expect(server.decorate).toHaveBeenCalledWith(
|
||||
'request',
|
||||
'getCapabilities',
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(registerMock).toHaveBeenCalledTimes(2);
|
||||
expect(registerMock.mock.calls[0][0]()).toEqual(capaA);
|
||||
expect(registerMock.mock.calls[1][0]()).toEqual(capaB);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,51 +17,26 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Server, Request } from 'hapi';
|
||||
|
||||
import { Capabilities } from '../../../core/public';
|
||||
import { Server } from 'hapi';
|
||||
import KbnServer from '../kbn_server';
|
||||
import { registerCapabilitiesRoute } from './capabilities_route';
|
||||
import { mergeCapabilities } from './merge_capabilities';
|
||||
import { resolveCapabilities } from './resolve_capabilities';
|
||||
|
||||
export type CapabilitiesModifier = (
|
||||
request: Request,
|
||||
uiCapabilities: Capabilities
|
||||
) => Capabilities | Promise<Capabilities>;
|
||||
|
||||
export async function capabilitiesMixin(kbnServer: KbnServer, server: Server) {
|
||||
const modifiers: CapabilitiesModifier[] = [];
|
||||
const registerLegacyCapabilities = async () => {
|
||||
const capabilitiesList = await Promise.all(
|
||||
kbnServer.pluginSpecs
|
||||
.map(spec => spec.getUiCapabilitiesProvider())
|
||||
.filter(provider => !!provider)
|
||||
.map(provider => provider(server))
|
||||
);
|
||||
|
||||
server.decorate('server', 'registerCapabilitiesModifier', (provider: CapabilitiesModifier) => {
|
||||
modifiers.push(provider);
|
||||
});
|
||||
capabilitiesList.forEach(capabilities => {
|
||||
kbnServer.newPlatform.setup.core.capabilities.registerProvider(() => capabilities);
|
||||
});
|
||||
};
|
||||
|
||||
// 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))
|
||||
))
|
||||
);
|
||||
|
||||
server.decorate('request', 'getCapabilities', function() {
|
||||
// Get legacy nav links
|
||||
const navLinks = server.getUiNavLinks().reduce(
|
||||
(acc, spec) => ({
|
||||
...acc,
|
||||
[spec._id]: true,
|
||||
}),
|
||||
{} as Record<string, boolean>
|
||||
);
|
||||
|
||||
return resolveCapabilities(this, modifiers, defaultCapabilities, { navLinks });
|
||||
});
|
||||
|
||||
registerCapabilitiesRoute(server, defaultCapabilities, modifiers);
|
||||
await registerLegacyCapabilities();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,137 +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 { 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);
|
||||
});
|
||||
});
|
|
@ -1,54 +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 Joi from 'joi';
|
||||
import { Server } from 'hapi';
|
||||
|
||||
import { Capabilities } from '../../../core/public';
|
||||
import { CapabilitiesModifier } from './capabilities_mixin';
|
||||
import { resolveCapabilities } from './resolve_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) {
|
||||
const { capabilities } = request.payload as { capabilities: Capabilities };
|
||||
return {
|
||||
capabilities: await resolveCapabilities(
|
||||
request,
|
||||
modifiers,
|
||||
defaultCapabilities,
|
||||
capabilities
|
||||
),
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { CapabilitiesModifier, capabilitiesMixin } from './capabilities_mixin';
|
||||
export { capabilitiesMixin } from './capabilities_mixin';
|
||||
|
|
|
@ -1,84 +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 { mergeCapabilities } from './merge_capabilities';
|
||||
|
||||
const defaultProps = {
|
||||
catalogue: {},
|
||||
management: {},
|
||||
navLinks: {},
|
||||
};
|
||||
|
||||
test(`"{ foo: {} }" doesn't clobber "{ foo: { bar: true } }"`, () => {
|
||||
const output1 = mergeCapabilities({ foo: { bar: true } }, { foo: {} });
|
||||
expect(output1).toEqual({ ...defaultProps, foo: { bar: true } });
|
||||
|
||||
const output2 = mergeCapabilities({ foo: { bar: true } }, { foo: {} });
|
||||
expect(output2).toEqual({ ...defaultProps, foo: { bar: true } });
|
||||
});
|
||||
|
||||
test(`"{ foo: { bar: true } }" doesn't clobber "{ baz: { quz: true } }"`, () => {
|
||||
const output1 = mergeCapabilities({ foo: { bar: true } }, { baz: { quz: true } });
|
||||
expect(output1).toEqual({ ...defaultProps, foo: { bar: true }, baz: { quz: true } });
|
||||
|
||||
const output2 = mergeCapabilities({ baz: { quz: true } }, { foo: { bar: true } });
|
||||
expect(output2).toEqual({ ...defaultProps, foo: { bar: true }, baz: { quz: true } });
|
||||
});
|
||||
|
||||
test(`"{ foo: { bar: { baz: true } } }" doesn't clobber "{ foo: { bar: { quz: true } } }"`, () => {
|
||||
const output1 = mergeCapabilities(
|
||||
{ foo: { bar: { baz: true } } },
|
||||
{ foo: { bar: { quz: true } } }
|
||||
);
|
||||
expect(output1).toEqual({ ...defaultProps, foo: { bar: { baz: true, quz: true } } });
|
||||
|
||||
const output2 = mergeCapabilities(
|
||||
{ foo: { bar: { quz: true } } },
|
||||
{ foo: { bar: { baz: true } } }
|
||||
);
|
||||
expect(output2).toEqual({ ...defaultProps, foo: { bar: { baz: true, quz: true } } });
|
||||
});
|
||||
|
||||
test(`error is thrown if boolean and object clash`, () => {
|
||||
expect(() => {
|
||||
mergeCapabilities({ foo: { bar: { baz: true } } }, { foo: { bar: true } });
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"a boolean and an object can't be merged"`);
|
||||
|
||||
expect(() => {
|
||||
mergeCapabilities({ foo: { bar: true } }, { foo: { bar: { baz: true } } });
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"a boolean and an object can't be merged"`);
|
||||
});
|
||||
|
||||
test(`supports duplicates as long as the booleans are the same`, () => {
|
||||
const output1 = mergeCapabilities({ foo: { bar: true } }, { foo: { bar: true } });
|
||||
expect(output1).toEqual({ ...defaultProps, foo: { bar: true } });
|
||||
|
||||
const output2 = mergeCapabilities({ foo: { bar: false } }, { foo: { bar: false } });
|
||||
expect(output2).toEqual({ ...defaultProps, foo: { bar: false } });
|
||||
});
|
||||
|
||||
test(`error is thrown if merging "true" and "false"`, () => {
|
||||
expect(() => {
|
||||
mergeCapabilities({ foo: { bar: false } }, { foo: { bar: true } });
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"\\"true\\" and \\"false\\" can't be merged"`);
|
||||
|
||||
expect(() => {
|
||||
mergeCapabilities({ foo: { bar: true } }, { foo: { bar: false } });
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"\\"true\\" and \\"false\\" can't be merged"`);
|
||||
});
|
9
src/legacy/server/kbn_server.d.ts
vendored
9
src/legacy/server/kbn_server.d.ts
vendored
|
@ -20,7 +20,7 @@
|
|||
import { ResponseObject, Server } from 'hapi';
|
||||
import { UnwrapPromise } from '@kbn/utility-types';
|
||||
|
||||
import { SavedObjectsClientProviderOptions, CoreSetup } from 'src/core/server';
|
||||
import { SavedObjectsClientProviderOptions, CoreSetup, CoreStart } from 'src/core/server';
|
||||
import {
|
||||
ConfigService,
|
||||
ElasticsearchServiceSetup,
|
||||
|
@ -39,9 +39,8 @@ import { SavedObjectsManagement } from '../../core/server/saved_objects/manageme
|
|||
import { ApmOssPlugin } from '../core_plugins/apm_oss';
|
||||
import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch';
|
||||
import { UsageCollectionSetup } from '../../plugins/usage_collection/server';
|
||||
import { CapabilitiesModifier } from './capabilities';
|
||||
import { IndexPatternsServiceFactory } from './index_patterns';
|
||||
import { Capabilities } from '../../core/public';
|
||||
import { Capabilities } from '../../core/server';
|
||||
import { UiSettingsServiceFactoryOptions } from '../../legacy/ui/ui_settings/ui_settings_service_factory';
|
||||
|
||||
export interface KibanaConfig {
|
||||
|
@ -69,7 +68,6 @@ declare module 'hapi' {
|
|||
savedObjects: SavedObjectsLegacyService;
|
||||
injectUiAppVars: (pluginName: string, getAppVars: () => { [key: string]: any }) => void;
|
||||
getHiddenUiAppById(appId: string): UiApp;
|
||||
registerCapabilitiesModifier: (provider: CapabilitiesModifier) => void;
|
||||
addScopedTutorialContextFactory: (
|
||||
scopedTutorialContextFactory: (...args: any[]) => any
|
||||
) => void;
|
||||
|
@ -90,7 +88,6 @@ declare module 'hapi' {
|
|||
getBasePath(): string;
|
||||
getDefaultRoute(): Promise<string>;
|
||||
getUiSettingsService(): IUiSettingsClient;
|
||||
getCapabilities(): Promise<Capabilities>;
|
||||
}
|
||||
|
||||
interface ResponseToolkit {
|
||||
|
@ -127,7 +124,7 @@ export default class KbnServer {
|
|||
plugins: PluginsSetup;
|
||||
};
|
||||
start: {
|
||||
core: CoreSetup;
|
||||
core: CoreStart;
|
||||
plugins: Record<string, object>;
|
||||
};
|
||||
stop: null;
|
||||
|
|
|
@ -279,8 +279,6 @@ export function uiRenderMixin(kbnServer, server, config) {
|
|||
uiPlugins,
|
||||
|
||||
legacyMetadata,
|
||||
|
||||
capabilities: await request.getCapabilities(),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -149,7 +149,6 @@ export const security = (kibana) => new kibana.Plugin({
|
|||
isSystemAPIRequest: server.plugins.kibana.systemApi.isSystemApiRequest.bind(
|
||||
server.plugins.kibana.systemApi
|
||||
),
|
||||
capabilities: { registerCapabilitiesModifier: server.registerCapabilitiesModifier },
|
||||
cspRules: createCSPRuleString(config.get('csp.rules')),
|
||||
kibanaIndexName: config.get('kibana.index'),
|
||||
});
|
||||
|
|
|
@ -129,9 +129,6 @@ export const spaces = (kibana: Record<string, any>) =>
|
|||
tutorial: {
|
||||
addScopedTutorialContextFactory: server.addScopedTutorialContextFactory,
|
||||
},
|
||||
capabilities: {
|
||||
registerCapabilitiesModifier: server.registerCapabilitiesModifier,
|
||||
},
|
||||
auditLogger: {
|
||||
create: (pluginId: string) =>
|
||||
new AuditLogger(server, pluginId, server.config(), server.plugins.xpack_main.info),
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import Joi from 'joi';
|
||||
|
||||
import { difference } from 'lodash';
|
||||
import { Capabilities as UICapabilities } from '../../../../src/core/public';
|
||||
import { Capabilities as UICapabilities } from '../../../../src/core/server';
|
||||
import { FeatureWithAllOrReadPrivileges } from './feature';
|
||||
|
||||
// Each feature gets its own property on the UICapabilities object,
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
PluginInitializerContext,
|
||||
RecursiveReadonly,
|
||||
} from '../../../../src/core/server';
|
||||
import { Capabilities as UICapabilities } from '../../../../src/core/public';
|
||||
import { Capabilities as UICapabilities } from '../../../../src/core/server';
|
||||
import { deepFreeze } from '../../../../src/core/utils';
|
||||
import { XPackInfo } from '../../../legacy/plugins/xpack_main/server/lib/xpack_info';
|
||||
import { PluginSetupContract as TimelionSetupContract } from '../../../../src/plugins/timelion/server';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { Capabilities as UICapabilities } from '../../../../src/core/public';
|
||||
import { Capabilities as UICapabilities } from '../../../../src/core/server';
|
||||
import { Feature } from './feature';
|
||||
|
||||
const ELIGIBLE_FLAT_MERGE_KEYS = ['catalogue'];
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { isString } from 'lodash';
|
||||
import { Capabilities as UICapabilities } from '../../../../../../src/core/public';
|
||||
import { Capabilities as UICapabilities } from '../../../../../../src/core/server';
|
||||
import { uiCapabilitiesRegex } from '../../../../features/server';
|
||||
|
||||
export class UIActions {
|
||||
|
|
|
@ -20,7 +20,6 @@ import { deepFreeze } from '../../../../src/core/utils';
|
|||
import { SpacesPluginSetup } from '../../spaces/server';
|
||||
import { PluginSetupContract as FeaturesSetupContract } from '../../features/server';
|
||||
import { LicensingPluginSetup } from '../../licensing/server';
|
||||
import { CapabilitiesModifier } from '../../../../src/legacy/server/capabilities';
|
||||
|
||||
import { Authentication, setupAuthentication } from './authentication';
|
||||
import { Authorization, setupAuthorization } from './authorization';
|
||||
|
@ -44,7 +43,6 @@ export type FeaturesService = Pick<FeaturesSetupContract, 'getFeatures'>;
|
|||
export interface LegacyAPI {
|
||||
serverConfig: { protocol: string; hostname: string; port: number };
|
||||
isSystemAPIRequest: (request: KibanaRequest) => boolean;
|
||||
capabilities: { registerCapabilitiesModifier: (provider: CapabilitiesModifier) => void };
|
||||
kibanaIndexName: string;
|
||||
cspRules: string;
|
||||
savedObjects: SavedObjectsLegacyService<KibanaRequest | LegacyRequest>;
|
||||
|
@ -157,6 +155,8 @@ export class Plugin {
|
|||
featuresService: features,
|
||||
});
|
||||
|
||||
core.capabilities.registerSwitcher(authz.disableUnauthorizedCapabilities);
|
||||
|
||||
defineRoutes({
|
||||
router: core.http.createRouter(),
|
||||
basePath: core.http.basePath,
|
||||
|
@ -196,10 +196,6 @@ export class Plugin {
|
|||
authz,
|
||||
legacyAPI,
|
||||
});
|
||||
|
||||
legacyAPI.capabilities.registerCapabilitiesModifier((request, capabilities) =>
|
||||
authz.disableUnauthorizedCapabilities(KibanaRequest.from(request), capabilities)
|
||||
);
|
||||
},
|
||||
|
||||
registerPrivilegesWithCluster: async () => await authz.registerPrivilegesWithCluster(),
|
||||
|
|
|
@ -6,13 +6,11 @@
|
|||
|
||||
import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { CapabilitiesModifier } from 'src/legacy/server/capabilities';
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
|
||||
import { HomeServerPluginSetup } from 'src/plugins/home/server';
|
||||
import {
|
||||
SavedObjectsLegacyService,
|
||||
CoreSetup,
|
||||
KibanaRequest,
|
||||
Logger,
|
||||
PluginInitializerContext,
|
||||
} from '../../../../src/core/server';
|
||||
|
@ -42,9 +40,6 @@ export interface LegacyAPI {
|
|||
tutorial: {
|
||||
addScopedTutorialContextFactory: (factory: any) => void;
|
||||
};
|
||||
capabilities: {
|
||||
registerCapabilitiesModifier: (provider: CapabilitiesModifier) => void;
|
||||
};
|
||||
auditLogger: {
|
||||
create: (pluginId: string) => AuditLogger;
|
||||
};
|
||||
|
@ -132,6 +127,16 @@ export class Plugin {
|
|||
features: plugins.features,
|
||||
});
|
||||
|
||||
core.capabilities.registerSwitcher(async (request, uiCapabilities) => {
|
||||
try {
|
||||
const activeSpace = await spacesService.getActiveSpace(request);
|
||||
const features = plugins.features.getFeatures();
|
||||
return toggleUICapabilities(features, uiCapabilities, activeSpace);
|
||||
} catch (e) {
|
||||
return uiCapabilities;
|
||||
}
|
||||
});
|
||||
|
||||
if (plugins.security) {
|
||||
plugins.security.registerSpacesService(spacesService);
|
||||
}
|
||||
|
@ -189,14 +194,5 @@ export class Plugin {
|
|||
features: featuresSetup,
|
||||
licensing: licensingSetup,
|
||||
});
|
||||
legacyAPI.capabilities.registerCapabilitiesModifier(async (request, uiCapabilities) => {
|
||||
try {
|
||||
const activeSpace = await spacesService.getActiveSpace(KibanaRequest.from(request));
|
||||
const features = featuresSetup.getFeatures();
|
||||
return toggleUICapabilities(features, uiCapabilities, activeSpace);
|
||||
} catch (e) {
|
||||
return uiCapabilities;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,7 +104,6 @@ export const createLegacyAPI = ({
|
|||
kibanaIndex: '',
|
||||
},
|
||||
auditLogger: {} as any,
|
||||
capabilities: {} as any,
|
||||
tutorial: {} as any,
|
||||
xpackMain: {} as any,
|
||||
savedObjects: savedObjectsService,
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
* 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';
|
||||
import { ToolingLog } from '@kbn/dev-utils';
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
import { FeaturesService, FeaturesProvider } from './features';
|
||||
|
||||
export interface BasicCredentials {
|
||||
username: string;
|
||||
|
@ -30,8 +30,9 @@ interface GetUICapabilitiesResult {
|
|||
export class UICapabilitiesService {
|
||||
private readonly log: ToolingLog;
|
||||
private readonly axios: AxiosInstance;
|
||||
private readonly featureService: FeaturesService;
|
||||
|
||||
constructor(url: string, log: ToolingLog) {
|
||||
constructor(url: string, log: ToolingLog, featureService: FeaturesService) {
|
||||
this.log = log;
|
||||
this.axios = axios.create({
|
||||
headers: { 'kbn-xsrf': 'x-pack/ftr/services/ui_capabilities' },
|
||||
|
@ -39,6 +40,7 @@ export class UICapabilitiesService {
|
|||
maxRedirects: 0,
|
||||
validateStatus: () => true, // we'll handle our own statusCodes and throw informative errors
|
||||
});
|
||||
this.featureService = featureService;
|
||||
}
|
||||
|
||||
public async get({
|
||||
|
@ -48,8 +50,15 @@ export class UICapabilitiesService {
|
|||
credentials?: BasicCredentials;
|
||||
spaceId?: string;
|
||||
}): Promise<GetUICapabilitiesResult> {
|
||||
const features = await this.featureService.get();
|
||||
const applications = Object.values(features)
|
||||
.map(feature => feature.navLinkId)
|
||||
.filter(link => !!link);
|
||||
|
||||
const spaceUrlPrefix = spaceId ? `/s/${spaceId}` : '';
|
||||
this.log.debug(`requesting ${spaceUrlPrefix}/app/kibana to parse the uiCapabilities`);
|
||||
this.log.debug(
|
||||
`requesting ${spaceUrlPrefix}/api/core/capabilities to parse the uiCapabilities`
|
||||
);
|
||||
const requestHeaders = credentials
|
||||
? {
|
||||
Authorization: `Basic ${Buffer.from(
|
||||
|
@ -57,9 +66,13 @@ export class UICapabilitiesService {
|
|||
).toString('base64')}`,
|
||||
}
|
||||
: {};
|
||||
const response = await this.axios.get(`${spaceUrlPrefix}/app/kibana`, {
|
||||
headers: requestHeaders,
|
||||
});
|
||||
const response = await this.axios.post(
|
||||
`${spaceUrlPrefix}/api/core/capabilities`,
|
||||
{ applications: [...applications, 'kibana:management'] },
|
||||
{
|
||||
headers: requestHeaders,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 302 && response.headers.location === '/spaces/space_selector') {
|
||||
return {
|
||||
|
@ -83,35 +96,20 @@ 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.capabilities 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function UICapabilitiesProvider({ getService }: FtrProviderContext) {
|
||||
const log = getService('log');
|
||||
const config = getService('config');
|
||||
export function UICapabilitiesProvider(context: FtrProviderContext) {
|
||||
const log = context.getService('log');
|
||||
const config = context.getService('config');
|
||||
const noAuthUrl = formatUrl({
|
||||
...config.get('servers.kibana'),
|
||||
auth: undefined,
|
||||
});
|
||||
|
||||
return new UICapabilitiesService(noAuthUrl, log);
|
||||
return new UICapabilitiesService(noAuthUrl, log, FeaturesProvider(context));
|
||||
}
|
||||
|
|
|
@ -7,10 +7,7 @@
|
|||
import expect from '@kbn/expect';
|
||||
import { mapValues } from 'lodash';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
GetUICapabilitiesFailureReason,
|
||||
UICapabilitiesService,
|
||||
} from '../../common/services/ui_capabilities';
|
||||
import { UICapabilitiesService } from '../../common/services/ui_capabilities';
|
||||
import { UserAtSpaceScenarios } from '../scenarios';
|
||||
|
||||
export default function catalogueTests({ getService }: FtrProviderContext) {
|
||||
|
@ -58,15 +55,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) {
|
|||
case 'dual_privileges_all at nothing_space':
|
||||
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, security interceptor responds with 404.
|
||||
case 'nothing_space_read at nothing_space':
|
||||
case 'no_kibana_privileges at everything_space':
|
||||
case 'no_kibana_privileges at nothing_space':
|
||||
case 'legacy_all at everything_space':
|
||||
|
@ -74,10 +63,14 @@ export default function catalogueTests({ getService }: FtrProviderContext) {
|
|||
case 'everything_space_all at nothing_space':
|
||||
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.NotFound);
|
||||
case 'nothing_space_read at everything_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;
|
||||
}
|
||||
default:
|
||||
throw new UnreachableError(scenario);
|
||||
}
|
||||
|
|
|
@ -6,10 +6,7 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
GetUICapabilitiesFailureReason,
|
||||
UICapabilitiesService,
|
||||
} from '../../common/services/ui_capabilities';
|
||||
import { UICapabilitiesService } from '../../common/services/ui_capabilities';
|
||||
import { UserAtSpaceScenarios } from '../scenarios';
|
||||
|
||||
export default function fooTests({ getService }: FtrProviderContext) {
|
||||
|
@ -61,6 +58,14 @@ export default function fooTests({ getService }: FtrProviderContext) {
|
|||
case 'dual_privileges_read at nothing_space':
|
||||
case 'nothing_space_all at nothing_space':
|
||||
case 'nothing_space_read at nothing_space':
|
||||
case 'no_kibana_privileges at everything_space':
|
||||
case 'no_kibana_privileges at nothing_space':
|
||||
case 'legacy_all at everything_space':
|
||||
case 'legacy_all at nothing_space':
|
||||
case 'everything_space_all at nothing_space':
|
||||
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(true);
|
||||
expect(uiCapabilities.value).to.have.property('foo');
|
||||
expect(uiCapabilities.value!.foo).to.eql({
|
||||
|
@ -70,18 +75,6 @@ export default function fooTests({ getService }: FtrProviderContext) {
|
|||
show: false,
|
||||
});
|
||||
break;
|
||||
// if we don't have access at the space itself, security interceptor responds with 404.
|
||||
case 'no_kibana_privileges at everything_space':
|
||||
case 'no_kibana_privileges at nothing_space':
|
||||
case 'legacy_all at everything_space':
|
||||
case 'legacy_all at nothing_space':
|
||||
case 'everything_space_all at nothing_space':
|
||||
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.NotFound);
|
||||
break;
|
||||
default:
|
||||
throw new UnreachableError(scenario);
|
||||
}
|
||||
|
|
|
@ -8,10 +8,7 @@ import expect from '@kbn/expect';
|
|||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
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';
|
||||
|
||||
export default function navLinksTests({ getService }: FtrProviderContext) {
|
||||
|
@ -58,11 +55,6 @@ export default function navLinksTests({ getService }: FtrProviderContext) {
|
|||
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;
|
||||
// if we don't have access at the space itself, security interceptor responds with 404.
|
||||
case 'no_kibana_privileges at everything_space':
|
||||
case 'no_kibana_privileges at nothing_space':
|
||||
case 'legacy_all at everything_space':
|
||||
|
@ -71,8 +63,9 @@ export default function navLinksTests({ getService }: FtrProviderContext) {
|
|||
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.NotFound);
|
||||
expect(uiCapabilities.success).to.be(true);
|
||||
expect(uiCapabilities.value).to.have.property('navLinks');
|
||||
expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.only('management'));
|
||||
break;
|
||||
default:
|
||||
throw new UnreachableError(scenario);
|
||||
|
|
|
@ -7,10 +7,7 @@
|
|||
import expect from '@kbn/expect';
|
||||
import { mapValues } from 'lodash';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
GetUICapabilitiesFailureReason,
|
||||
UICapabilitiesService,
|
||||
} from '../../common/services/ui_capabilities';
|
||||
import { UICapabilitiesService } from '../../common/services/ui_capabilities';
|
||||
import { UserScenarios } from '../scenarios';
|
||||
|
||||
export default function catalogueTests({ getService }: FtrProviderContext) {
|
||||
|
@ -63,8 +60,11 @@ export default function catalogueTests({ getService }: FtrProviderContext) {
|
|||
// these users have no access to even get the ui capabilities
|
||||
case 'legacy_all':
|
||||
case 'no_kibana_privileges':
|
||||
expect(uiCapabilities.success).to.be(false);
|
||||
expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound);
|
||||
expect(uiCapabilities.success).to.be(true);
|
||||
expect(uiCapabilities.value).to.have.property('catalogue');
|
||||
// only foo is enabled
|
||||
const expected = mapValues(uiCapabilities.value!.catalogue, () => false);
|
||||
expect(uiCapabilities.value!.catalogue).to.eql(expected);
|
||||
break;
|
||||
default:
|
||||
throw new UnreachableError(scenario);
|
||||
|
|
|
@ -6,10 +6,7 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
GetUICapabilitiesFailureReason,
|
||||
UICapabilitiesService,
|
||||
} from '../../common/services/ui_capabilities';
|
||||
import { UICapabilitiesService } from '../../common/services/ui_capabilities';
|
||||
import { UserScenarios } from '../scenarios';
|
||||
|
||||
export default function fooTests({ getService }: FtrProviderContext) {
|
||||
|
@ -55,8 +52,14 @@ export default function fooTests({ getService }: FtrProviderContext) {
|
|||
// these users have no access to even get the ui capabilities
|
||||
case 'legacy_all':
|
||||
case 'no_kibana_privileges':
|
||||
expect(uiCapabilities.success).to.be(false);
|
||||
expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound);
|
||||
expect(uiCapabilities.success).to.be(true);
|
||||
expect(uiCapabilities.value).to.have.property('foo');
|
||||
expect(uiCapabilities.value!.foo).to.eql({
|
||||
create: false,
|
||||
edit: false,
|
||||
delete: false,
|
||||
show: false,
|
||||
});
|
||||
break;
|
||||
// all other users can't do anything with Foo
|
||||
default:
|
||||
|
|
|
@ -8,10 +8,7 @@ import expect from '@kbn/expect';
|
|||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
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';
|
||||
|
||||
export default function navLinksTests({ getService }: FtrProviderContext) {
|
||||
|
@ -59,8 +56,9 @@ export default function navLinksTests({ getService }: FtrProviderContext) {
|
|||
break;
|
||||
case 'legacy_all':
|
||||
case 'no_kibana_privileges':
|
||||
expect(uiCapabilities.success).to.be(false);
|
||||
expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound);
|
||||
expect(uiCapabilities.success).to.be(true);
|
||||
expect(uiCapabilities.value).to.have.property('navLinks');
|
||||
expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.only('management'));
|
||||
break;
|
||||
default:
|
||||
throw new UnreachableError(scenario);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue