Migrate Capabilities to new platform (#51438) (#52051)

* 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:
Pierre Gayvallet 2019-12-03 11:08:28 +01:00 committed by GitHub
parent 83fb4f6453
commit bc6189d364
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
79 changed files with 3358 additions and 2424 deletions

View file

@ -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',

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [Capabilities](./kibana-plugin-server.capabilities.md) &gt; [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>;
```

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [Capabilities](./kibana-plugin-server.capabilities.md) &gt; [management](./kibana-plugin-server.capabilities.management.md)
## Capabilities.management property
Management section capabilities.
<b>Signature:</b>
```typescript
management: {
[sectionId: string]: Record<string, boolean>;
};
```

View file

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [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&lt;string, boolean&gt;</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&lt;string, boolean&gt;;</code><br/><code> }</code> | Management section capabilities. |
| [navLinks](./kibana-plugin-server.capabilities.navlinks.md) | <code>Record&lt;string, boolean&gt;</code> | Navigation link capabilities. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [Capabilities](./kibana-plugin-server.capabilities.md) &gt; [navLinks](./kibana-plugin-server.capabilities.navlinks.md)
## Capabilities.navLinks property
Navigation link capabilities.
<b>Signature:</b>
```typescript
navLinks: Record<string, boolean>;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [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>;
```

View file

@ -0,0 +1,27 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [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. |

View file

@ -0,0 +1,46 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) &gt; [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,
},
}
});
}
```

View file

@ -0,0 +1,47 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) &gt; [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.
});
}
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [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 |

View file

@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [CapabilitiesStart](./kibana-plugin-server.capabilitiesstart.md) &gt; [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>`

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [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>>;
```

View file

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

View file

@ -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) |

View file

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

View file

@ -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) |

View file

@ -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. |

View file

@ -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,
});
});

View file

@ -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

View file

@ -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

View file

@ -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);
}
}

View file

@ -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(),

View file

@ -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);

View file

@ -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;
};

View 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,
};

View 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,
},
}
`);
});
});
});

View 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, []),
};
}
}

View file

@ -0,0 +1,21 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { CapabilitiesService, CapabilitiesSetup, CapabilitiesStart } from './capabilities_service';
export { Capabilities, CapabilitiesSwitcher, CapabilitiesProvider } from './types';

View file

@ -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 {},
}
`);
});
});
});

View 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 } });
});
});

View file

@ -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`);
}
});

View 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,
},
});
});
});

View 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
);
}

View 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);
}

View 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,
});
}
);
});
}

View file

@ -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>>;

View file

@ -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,
};

View file

@ -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 () => {

View file

@ -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 () => {

View file

@ -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,
};

View 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);
};

View file

@ -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,
};

View file

@ -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;
}

View file

@ -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() },
},

View file

@ -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 },
};

View file

@ -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;

View file

@ -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

View file

@ -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,
};

View file

@ -17,7 +17,4 @@
* under the License.
*/
export const mockRegisterCapabilitiesRoute = jest.fn();
jest.mock('./capabilities_route', () => ({
registerCapabilitiesRoute: mockRegisterCapabilitiesRoute,
}));
export { createHttpServer } from './http/test_utils';

View 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>>;
}

View file

@ -22,3 +22,4 @@
* types are stripped.
*/
export * from './core_service';
export * from './capabilities';

View file

@ -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' },
});

View file

@ -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';

View file

@ -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';

View file

@ -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);
});
});

View file

@ -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();
});
}

View file

@ -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);
});
});

View file

@ -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
),
};
},
});
};

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { CapabilitiesModifier, capabilitiesMixin } from './capabilities_mixin';
export { capabilitiesMixin } from './capabilities_mixin';

View file

@ -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"`);
});

View file

@ -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;

View file

@ -279,8 +279,6 @@ export function uiRenderMixin(kbnServer, server, config) {
uiPlugins,
legacyMetadata,
capabilities: await request.getCapabilities(),
},
});

View file

@ -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'),
});

View file

@ -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),

View file

@ -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,

View file

@ -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';

View file

@ -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'];

View file

@ -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 {

View file

@ -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(),

View file

@ -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;
}
});
}
}

View file

@ -104,7 +104,6 @@ export const createLegacyAPI = ({
kibanaIndex: '',
},
auditLogger: {} as any,
capabilities: {} as any,
tutorial: {} as any,
xpackMain: {} as any,
savedObjects: savedObjectsService,

View file

@ -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));
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);

View file

@ -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);

View file

@ -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:

View file

@ -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);