mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Backports the following commits to 6.x: - Spaces Phase 1 (#21408)
This commit is contained in:
parent
dfe08d1d9b
commit
632c63ab21
479 changed files with 31606 additions and 5045 deletions
|
@ -26,7 +26,7 @@ entirely.
|
|||
|
||||
[float]
|
||||
== APIs
|
||||
|
||||
* <<spaces-api>>
|
||||
* <<role-management-api>>
|
||||
* <<saved-objects-api>>
|
||||
* <<dashboard-import-api>>
|
||||
|
@ -34,6 +34,7 @@ entirely.
|
|||
* <<url-shortening-api>>
|
||||
--
|
||||
|
||||
include::api/spaces-management.asciidoc[]
|
||||
include::api/role-management.asciidoc[]
|
||||
include::api/saved-objects.asciidoc[]
|
||||
include::api/dashboard-import.asciidoc[]
|
||||
|
|
|
@ -30,7 +30,7 @@ that begin with `_` are reserved for system usage.
|
|||
`elasticsearch`:: (object) Optional {es} cluster and index privileges, valid keys are
|
||||
`cluster`, `indices` and `run_as`. For more information, see {xpack-ref}/defining-roles.html[Defining Roles].
|
||||
|
||||
`kibana`:: (list) A list of objects that specify the <<kibana-privileges>>.
|
||||
`kibana`:: (object) An object that specifies the <<kibana-privileges>>. Valid keys are `global` and `space`. Privileges defined in the `global` key will apply to all spaces within Kibana, and will take precedent over any privileges defined in the `space` key. For example, specifying `global: ["all"]` will grant full access to all spaces within Kibana, even if the role indicates that a specific space should only have `read` privileges.
|
||||
|
||||
===== Example
|
||||
|
||||
|
@ -52,9 +52,9 @@ PUT /api/security/role/my_kibana_role
|
|||
"query" : "{\"match\": {\"title\": \"foo\"}}"
|
||||
} ],
|
||||
},
|
||||
"kibana": [ {
|
||||
"privileges": [ "all" ]
|
||||
} ],
|
||||
"kibana": {
|
||||
"global": ["all"]
|
||||
}
|
||||
}
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
@ -62,3 +62,37 @@ PUT /api/security/role/my_kibana_role
|
|||
==== Response
|
||||
|
||||
A successful call returns a response code of `204` and no response body.
|
||||
|
||||
|
||||
==== Granting access to specific spaces
|
||||
To grant access to individual spaces within {kib}, specify the space identifier within the `kibana` object.
|
||||
|
||||
Note: granting access
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
PUT /api/security/role/my_kibana_role
|
||||
{
|
||||
"metadata" : {
|
||||
"version" : 1
|
||||
},
|
||||
"elasticsearch": {
|
||||
"cluster" : [ "all" ],
|
||||
"indices" : [ {
|
||||
"names" : [ "index1", "index2" ],
|
||||
"privileges" : [ "all" ],
|
||||
"field_security" : {
|
||||
"grant" : [ "title", "body" ]
|
||||
},
|
||||
"query" : "{\"match\": {\"title\": \"foo\"}}"
|
||||
} ],
|
||||
},
|
||||
"kibana": {
|
||||
"global": [],
|
||||
"space": {
|
||||
"marketing": ["all"],
|
||||
"engineering": ["read"]
|
||||
}
|
||||
}
|
||||
}
|
||||
--------------------------------------------------
|
17
docs/api/spaces-management.asciidoc
Normal file
17
docs/api/spaces-management.asciidoc
Normal file
|
@ -0,0 +1,17 @@
|
|||
[role="xpack"]
|
||||
[[spaces-api]]
|
||||
== Kibana Spaces API
|
||||
|
||||
experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.]
|
||||
|
||||
The spaces API allows people to manage their spaces within {kib}.
|
||||
|
||||
* <<spaces-api-post>>
|
||||
* <<spaces-api-put>>
|
||||
* <<spaces-api-get>>
|
||||
* <<spaces-api-delete>>
|
||||
|
||||
include::spaces-management/post.asciidoc[]
|
||||
include::spaces-management/put.asciidoc[]
|
||||
include::spaces-management/get.asciidoc[]
|
||||
include::spaces-management/delete.asciidoc[]
|
25
docs/api/spaces-management/delete.asciidoc
Normal file
25
docs/api/spaces-management/delete.asciidoc
Normal file
|
@ -0,0 +1,25 @@
|
|||
[[spaces-api-delete]]
|
||||
=== Delete space
|
||||
|
||||
experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.]
|
||||
|
||||
[WARNING]
|
||||
==================================================
|
||||
Deleting a space will automatically delete all saved objects that belong to that space. This operation cannot be undone!
|
||||
==================================================
|
||||
|
||||
==== Request
|
||||
|
||||
To delete a space, submit a DELETE request to the `/api/spaces/space/<space_id>`
|
||||
endpoint:
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
DELETE /api/spaces/space/marketing
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
==== Response
|
||||
|
||||
If the space is successfully deleted, the response code is `204`; otherwise, the response
|
||||
code is 404.
|
77
docs/api/spaces-management/get.asciidoc
Normal file
77
docs/api/spaces-management/get.asciidoc
Normal file
|
@ -0,0 +1,77 @@
|
|||
[[spaces-api-get]]
|
||||
=== Get Space
|
||||
|
||||
experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.]
|
||||
|
||||
Retrieves all {kib} spaces, or a specific space.
|
||||
|
||||
==== Get all {kib} spaces
|
||||
|
||||
===== Request
|
||||
|
||||
To retrieve all spaces, issue a GET request to the
|
||||
/api/spaces/space endpoint.
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
GET /api/spaces/space
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
===== Response
|
||||
|
||||
A successful call returns a response code of `200` and a response body containing a JSON
|
||||
representation of the spaces.
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
[
|
||||
{
|
||||
"id": "default",
|
||||
"name": "Default",
|
||||
"description" : "This is the Default Space",
|
||||
"_reserved": true
|
||||
},
|
||||
{
|
||||
"id": "marketing",
|
||||
"name": "Marketing",
|
||||
"description" : "This is the Marketing Space",
|
||||
"color": "#aabbcc",
|
||||
"initials": "MK"
|
||||
},
|
||||
{
|
||||
"id": "sales",
|
||||
"name": "Sales",
|
||||
"initials": "MK"
|
||||
},
|
||||
]
|
||||
--------------------------------------------------
|
||||
|
||||
==== Get a specific space
|
||||
|
||||
===== Request
|
||||
|
||||
To retrieve a specific space, issue a GET request to
|
||||
the `/api/spaces/space/<space_id>` endpoint:
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
GET /api/spaces/space/marketing
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
===== Response
|
||||
|
||||
A successful call returns a response code of `200` and a response body containing a JSON
|
||||
representation of the space.
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
{
|
||||
"id": "marketing",
|
||||
"name": "Marketing",
|
||||
"description" : "This is the Marketing Space",
|
||||
"color": "#aabbcc",
|
||||
"initials": "MK"
|
||||
}
|
||||
--------------------------------------------------
|
50
docs/api/spaces-management/post.asciidoc
Normal file
50
docs/api/spaces-management/post.asciidoc
Normal file
|
@ -0,0 +1,50 @@
|
|||
[[spaces-api-post]]
|
||||
=== Create Space
|
||||
|
||||
experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.]
|
||||
|
||||
Creates a new {kib} space. To update an existing space, use the PUT command.
|
||||
|
||||
==== Request
|
||||
|
||||
To create a space, issue a POST request to the
|
||||
`/api/spaces/space` endpoint.
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
POST /api/spaces/space
|
||||
--------------------------------------------------
|
||||
|
||||
==== Request Body
|
||||
|
||||
The following parameters can be specified in the body of a POST request to create a space:
|
||||
|
||||
`id`:: (string) Required identifier for the space. This identifier becomes part of Kibana's URL when inside the space. This cannot be changed by the update operation.
|
||||
|
||||
`name`:: (string) Required display name for the space.
|
||||
|
||||
`description`:: (string) Optional description for the space.
|
||||
|
||||
`initials`:: (string) Optionally specify the initials shown in the Space Avatar for this space. By default, the initials will be automatically generated from the space name.
|
||||
If specified, initials should be either 1 or 2 characters.
|
||||
|
||||
`color`:: (string) Optioanlly specify the hex color code used in the Space Avatar for this space. By default, the color will be automatically generated from the space name.
|
||||
|
||||
===== Example
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
POST /api/spaces/space
|
||||
{
|
||||
"id": "marketing",
|
||||
"name": "Marketing",
|
||||
"description" : "This is the Marketing Space",
|
||||
"color": "#aabbcc",
|
||||
"initials": "MK"
|
||||
}
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
==== Response
|
||||
|
||||
A successful call returns a response code of `200` with the created Space.
|
50
docs/api/spaces-management/put.asciidoc
Normal file
50
docs/api/spaces-management/put.asciidoc
Normal file
|
@ -0,0 +1,50 @@
|
|||
[[spaces-api-put]]
|
||||
=== Update Space
|
||||
|
||||
experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.]
|
||||
|
||||
Updates an existing {kib} space. To create a new space, use the POST command.
|
||||
|
||||
==== Request
|
||||
|
||||
To update a space, issue a PUT request to the
|
||||
`/api/spaces/space/<space_id>` endpoint.
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
PUT /api/spaces/space/<space_id>
|
||||
--------------------------------------------------
|
||||
|
||||
==== Request Body
|
||||
|
||||
The following parameters can be specified in the body of a PUT request to update a space:
|
||||
|
||||
`id`:: (string) Required identifier for the space. This identifier becomes part of Kibana's URL when inside the space. This cannot be changed by the update operation.
|
||||
|
||||
`name`:: (string) Required display name for the space.
|
||||
|
||||
`description`:: (string) Optional description for the space.
|
||||
|
||||
`initials`:: (string) Optionally specify the initials shown in the Space Avatar for this space. By default, the initials will be automatically generated from the space name.
|
||||
If specified, initials should be either 1 or 2 characters.
|
||||
|
||||
`color`:: (string) Optioanlly specify the hex color code used in the Space Avatar for this space. By default, the color will be automatically generated from the space name.
|
||||
|
||||
===== Example
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
PUT /api/spaces/space/marketing
|
||||
{
|
||||
"id": "marketing",
|
||||
"name": "Marketing",
|
||||
"description" : "This is the Marketing Space",
|
||||
"color": "#aabbcc",
|
||||
"initials": "MK"
|
||||
}
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
==== Response
|
||||
|
||||
A successful call returns a response code of `200` with the updated Space.
|
|
@ -48,15 +48,15 @@ $http.get(chrome.addBasePath('/api/plugin/things'));
|
|||
[float]
|
||||
==== Server side
|
||||
|
||||
Append `config.get('server.basePath')` to any absolute URL path.
|
||||
Append `request.getBasePath()` to any absolute URL path.
|
||||
|
||||
["source","shell"]
|
||||
-----------
|
||||
const basePath = server.config().get('server.basePath');
|
||||
server.route({
|
||||
path: '/redirect',
|
||||
handler(req, reply) {
|
||||
reply.redirect(`${basePath}/otherLocation`);
|
||||
handler(request, reply) {
|
||||
reply.redirect(`${request.getBasePath()}/otherLocation`);
|
||||
}
|
||||
});
|
||||
-----------
|
||||
|
|
|
@ -52,6 +52,8 @@ include::monitoring/index.asciidoc[]
|
|||
|
||||
include::management.asciidoc[]
|
||||
|
||||
include::spaces/index.asciidoc[]
|
||||
|
||||
include::security/index.asciidoc[]
|
||||
|
||||
include::management/watcher-ui/index.asciidoc[]
|
||||
|
|
|
@ -2,10 +2,13 @@
|
|||
[[xpack-security-authorization]]
|
||||
=== Authorization
|
||||
|
||||
Authorizing users to use {kib} in most configurations is as simple as assigning the user
|
||||
Authorizing users to use {kib} in simple configurations is as easy as assigning the user
|
||||
either the `kibana_user` or `kibana_dashboard_only_user` reserved role. If you're running
|
||||
a single tenant of {kib} against your {es} cluster, this is sufficient and no other
|
||||
action is required.
|
||||
a single tenant of {kib} against your {es} cluster, and you're not controlling access to individual spaces, then this is sufficient and no other action is required.
|
||||
|
||||
==== Spaces
|
||||
|
||||
If you want to control individual spaces in {kib}, do **not** use the `kibana_user` or `kibana_dashboard_only_user` roles. Users with these roles are able to access all spaces in Kibana. Instead, create your own roles that grant access to specific spaces.
|
||||
|
||||
==== Multi-tenant {kib}
|
||||
|
||||
|
@ -15,6 +18,8 @@ either the *Management / Security / Roles* page in {kib} or the <<role-managemen
|
|||
to assign a specific <<kibana-privileges, Kibana privilege>> at that tenant. After creating the
|
||||
custom role, you should assign this role to the user(s) that you wish to have access.
|
||||
|
||||
While multi-tenant installations are supported, the recommended approach to securing access to segments of {kib} is to grant users access to specific spaces.
|
||||
|
||||
==== Legacy roles
|
||||
|
||||
Prior to {kib} 6.4, {kib} users required index privileges to the `kibana.index`
|
||||
|
|
8
docs/spaces/getting-started.asciidoc
Normal file
8
docs/spaces/getting-started.asciidoc
Normal file
|
@ -0,0 +1,8 @@
|
|||
[role="xpack"]
|
||||
[[spaces-getting-started]]
|
||||
=== Getting Started
|
||||
|
||||
Spaces are automatically enabled in {kib}. If you don't wish to use this feature, you can disable it
|
||||
by setting `xpack.spaces.enabled` to `false` in your `kibana.yml` configuration file.
|
||||
|
||||
{kib} automatically creates a default space for you. If you are upgrading from another version of {kib}, then the default space will contain all of your existing saved objects. Although you can't delete the default space, you can customize it to your liking.
|
BIN
docs/spaces/images/delete-space.png
Normal file
BIN
docs/spaces/images/delete-space.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 144 KiB |
BIN
docs/spaces/images/edit-space.png
Normal file
BIN
docs/spaces/images/edit-space.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 147 KiB |
BIN
docs/spaces/images/securing-spaces.png
Normal file
BIN
docs/spaces/images/securing-spaces.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 201 KiB |
BIN
docs/spaces/images/space-management.png
Normal file
BIN
docs/spaces/images/space-management.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 157 KiB |
BIN
docs/spaces/images/space-selector.png
Normal file
BIN
docs/spaces/images/space-selector.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 148 KiB |
17
docs/spaces/index.asciidoc
Normal file
17
docs/spaces/index.asciidoc
Normal file
|
@ -0,0 +1,17 @@
|
|||
[role="xpack"]
|
||||
[[xpack-spaces]]
|
||||
== Spaces
|
||||
|
||||
With spaces, you can organize your dashboards and other saved objects into meaningful categories.
|
||||
After creating your own spaces, you will be asked to choose a space when you enter {kib}. Once inside a space,
|
||||
you will only see the dashboards and other saved objects that belong to that space. You can change your active space at any time.
|
||||
|
||||
With security enabled, you can control which users have access to individual spaces.
|
||||
|
||||
[role="screenshot"]
|
||||
image::spaces/images/space-selector.png["Space selector screen"]
|
||||
|
||||
include::getting-started.asciidoc[]
|
||||
include::managing-spaces.asciidoc[]
|
||||
include::securing-spaces.asciidoc[]
|
||||
include::moving-saved-objects.asciidoc[]
|
25
docs/spaces/managing-spaces.asciidoc
Normal file
25
docs/spaces/managing-spaces.asciidoc
Normal file
|
@ -0,0 +1,25 @@
|
|||
[role="xpack"]
|
||||
[[spaces-managing]]
|
||||
=== Managing spaces
|
||||
You can manage spaces from the **Management > Spaces** page. Here you can create, edit, and delete your spaces.
|
||||
|
||||
[NOTE]
|
||||
{kib} has an <<spaces-api, experimental API>> if you want to create your spaces programatically.
|
||||
|
||||
[role="screenshot"]
|
||||
image::spaces/images/space-management.png["Space Management"]
|
||||
|
||||
==== Creating and updating spaces
|
||||
You can create as many spaces as you like, but each space must have a unique space identifier. The space identifier is a short string of text that is part of the {kib} URL when you are inside that space. {kib} automatically suggests a space identifier based on the name of your space, but you are free to customize this to your liking.
|
||||
|
||||
[NOTE]
|
||||
You cannot change the space identifier once the space is created.
|
||||
|
||||
[role="screenshot"]
|
||||
image::spaces/images/edit-space.png["Updating a space"]
|
||||
|
||||
==== Deleting spaces
|
||||
Deleting a space is a destructive operation, which cannot be undone. When you delete a space, all of the saved objects that belong to that space are also deleted.
|
||||
|
||||
[role="screenshot"]
|
||||
image::spaces/images/delete-space.png["Deleting a space"]
|
14
docs/spaces/moving-saved-objects.asciidoc
Normal file
14
docs/spaces/moving-saved-objects.asciidoc
Normal file
|
@ -0,0 +1,14 @@
|
|||
[role="xpack"]
|
||||
[[spaces-moving-objects]]
|
||||
=== Moving saved objects between spaces
|
||||
You can use {kib}'s <<managing-saved-objects-export-objects, import/export>> interface to copy objects from one space to another:
|
||||
|
||||
1. Navigate to the space that contains your saved objects.
|
||||
2. Export your saved objects via the <<managing-saved-objects-export-objects, import/export>> interface.
|
||||
3. Navigate to the space you are importing to.
|
||||
4. Import your saved objects via the <<managing-saved-objects-export-objects, import/export>> interface.
|
||||
5. (optional) Delete the saved objects from the space you exported from, if you don't want to keep a copy there.
|
||||
|
||||
|
||||
[NOTE]
|
||||
{kib} also has experimental <<dashboard-import-api-import, import>> and <<dashboard-import-api-export, export>> dashboard APIs if you are looking for a dashboard-centric way to automate this process.
|
7
docs/spaces/securing-spaces.asciidoc
Normal file
7
docs/spaces/securing-spaces.asciidoc
Normal file
|
@ -0,0 +1,7 @@
|
|||
[role="xpack"]
|
||||
[[spaces-securing]]
|
||||
=== Securing spaces
|
||||
|
||||
With security enabled, you can control who has access to specific spaces. You can manage access in **Management > Roles**.
|
||||
|
||||
image::spaces/images/securing-spaces.png["Securing spaces"]
|
|
@ -25,12 +25,15 @@ import { setupUsers, DEFAULT_SUPERUSER_PASS } from './auth';
|
|||
|
||||
export async function runElasticsearch({ config, options }) {
|
||||
const { log, esFrom } = options;
|
||||
const isOss = config.get('esTestCluster.license') === 'oss';
|
||||
const license = config.get('esTestCluster.license');
|
||||
const isTrialLicense = config.get('esTestCluster.license') === 'trial';
|
||||
|
||||
const cluster = createEsTestCluster({
|
||||
port: config.get('servers.elasticsearch.port'),
|
||||
password: !isOss ? DEFAULT_SUPERUSER_PASS : config.get('servers.elasticsearch.password'),
|
||||
license: config.get('esTestCluster.license'),
|
||||
password: isTrialLicense
|
||||
? DEFAULT_SUPERUSER_PASS
|
||||
: config.get('servers.elasticsearch.password'),
|
||||
license,
|
||||
log,
|
||||
basePath: resolve(KIBANA_ROOT, '.es'),
|
||||
esFrom: esFrom || config.get('esTestCluster.from'),
|
||||
|
@ -40,7 +43,7 @@ export async function runElasticsearch({ config, options }) {
|
|||
|
||||
await cluster.start(esArgs);
|
||||
|
||||
if (!isOss) {
|
||||
if (isTrialLicense) {
|
||||
await setupUsers(log, config);
|
||||
}
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ exports[`AdvancedSettings should render normally 1`] = `
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<advanced_settings_page_subtitle />
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
|
@ -396,6 +397,7 @@ exports[`AdvancedSettings should render specific setting if given setting key 1`
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<advanced_settings_page_subtitle />
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
|
|
|
@ -35,7 +35,12 @@ import { Form } from './components/form';
|
|||
import { getAriaName, toEditableConfig, DEFAULT_CATEGORY } from './lib';
|
||||
|
||||
import './advanced_settings.less';
|
||||
import { registerDefaultComponents, PAGE_TITLE_COMPONENT, PAGE_FOOTER_COMPONENT } from './components/default_component_registry';
|
||||
import {
|
||||
registerDefaultComponents,
|
||||
PAGE_TITLE_COMPONENT,
|
||||
PAGE_SUBTITLE_COMPONENT,
|
||||
PAGE_FOOTER_COMPONENT
|
||||
} from './components/default_component_registry';
|
||||
import { getSettingsComponent } from './components/component_registry';
|
||||
|
||||
export class AdvancedSettings extends Component {
|
||||
|
@ -145,6 +150,7 @@ export class AdvancedSettings extends Component {
|
|||
const { filteredSettings, query, footerQueryMatched } = this.state;
|
||||
|
||||
const PageTitle = getSettingsComponent(PAGE_TITLE_COMPONENT);
|
||||
const PageSubtitle = getSettingsComponent(PAGE_SUBTITLE_COMPONENT);
|
||||
const PageFooter = getSettingsComponent(PAGE_FOOTER_COMPONENT);
|
||||
|
||||
return (
|
||||
|
@ -161,6 +167,7 @@ export class AdvancedSettings extends Component {
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<PageSubtitle />
|
||||
<EuiSpacer size="m" />
|
||||
<CallOuts />
|
||||
<EuiSpacer size="m" />
|
||||
|
|
|
@ -19,12 +19,15 @@
|
|||
|
||||
import { tryRegisterSettingsComponent } from './component_registry';
|
||||
import { PageTitle } from './page_title';
|
||||
import { PageSubtitle } from './page_subtitle';
|
||||
import { PageFooter } from './page_footer';
|
||||
|
||||
export const PAGE_TITLE_COMPONENT = 'advanced_settings_page_title';
|
||||
export const PAGE_SUBTITLE_COMPONENT = 'advanced_settings_page_subtitle';
|
||||
export const PAGE_FOOTER_COMPONENT = 'advanced_settings_page_footer';
|
||||
|
||||
export function registerDefaultComponents() {
|
||||
tryRegisterSettingsComponent(PAGE_TITLE_COMPONENT, PageTitle);
|
||||
tryRegisterSettingsComponent(PAGE_SUBTITLE_COMPONENT, PageSubtitle);
|
||||
tryRegisterSettingsComponent(PAGE_FOOTER_COMPONENT, PageFooter);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PageSubtitle should render normally 1`] = `""`;
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export { PageSubtitle } from './page_subtitle';
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const PageSubtitle = () => null;
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { PageSubtitle } from './page_subtitle';
|
||||
|
||||
describe('PageSubtitle', () => {
|
||||
it('should render normally', () => {
|
||||
expect(shallow(<PageSubtitle />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -26,6 +26,7 @@ import { Project } from './project';
|
|||
export const PROJECTS = [
|
||||
new Project(resolve(REPO_ROOT, 'tsconfig.json')),
|
||||
new Project(resolve(REPO_ROOT, 'x-pack/tsconfig.json')),
|
||||
new Project(resolve(REPO_ROOT, 'x-pack/test/tsconfig.json'), 'x-pack/test'),
|
||||
|
||||
// NOTE: using glob.sync rather than glob-all or globby
|
||||
// because it takes less than 10 ms, while the other modules
|
||||
|
|
|
@ -22,9 +22,9 @@ import { resolve } from 'path';
|
|||
import _ from 'lodash';
|
||||
import Boom from 'boom';
|
||||
import Hapi from 'hapi';
|
||||
import getDefaultRoute from './get_default_route';
|
||||
import { setupVersionCheck } from './version_check';
|
||||
import { registerHapiPlugins } from './register_hapi_plugins';
|
||||
import { setupBasePathProvider } from './setup_base_path_provider';
|
||||
import { setupXsrf } from './xsrf';
|
||||
|
||||
export default async function (kbnServer, server, config) {
|
||||
|
@ -33,6 +33,8 @@ export default async function (kbnServer, server, config) {
|
|||
|
||||
server.connection(kbnServer.core.serverOptions);
|
||||
|
||||
setupBasePathProvider(server, config);
|
||||
|
||||
registerHapiPlugins(server);
|
||||
|
||||
// provide a simple way to expose static directories
|
||||
|
@ -86,11 +88,10 @@ export default async function (kbnServer, server, config) {
|
|||
server.route({
|
||||
path: '/',
|
||||
method: 'GET',
|
||||
handler: function (req, reply) {
|
||||
return reply.view('root_redirect', {
|
||||
hashRoute: `${config.get('server.basePath')}/app/kibana`,
|
||||
defaultRoute: getDefaultRoute(kbnServer),
|
||||
});
|
||||
handler(req, reply) {
|
||||
const basePath = req.getBasePath();
|
||||
const defaultRoute = config.get('server.defaultRoute');
|
||||
reply.redirect(`${basePath}${defaultRoute}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -102,7 +103,7 @@ export default async function (kbnServer, server, config) {
|
|||
if (path === '/' || path.charAt(path.length - 1) !== '/') {
|
||||
return reply(Boom.notFound());
|
||||
}
|
||||
const pathPrefix = config.get('server.basePath') ? `${config.get('server.basePath')}/` : '';
|
||||
const pathPrefix = req.getBasePath() ? `${req.getBasePath()}/` : '';
|
||||
return reply.redirect(format({
|
||||
search: req.url.search,
|
||||
pathname: pathPrefix + path.slice(0, -1),
|
||||
|
|
38
src/server/http/setup_base_path_provider.js
Normal file
38
src/server/http/setup_base_path_provider.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 function setupBasePathProvider(server, config) {
|
||||
|
||||
server.decorate('request', 'setBasePath', function (basePath) {
|
||||
const request = this;
|
||||
if (request.app._basePath) {
|
||||
throw new Error(`Request basePath was previously set. Setting multiple times is not supported.`);
|
||||
}
|
||||
request.app._basePath = basePath;
|
||||
});
|
||||
|
||||
server.decorate('request', 'getBasePath', function () {
|
||||
const request = this;
|
||||
|
||||
const serverBasePath = config.get('server.basePath');
|
||||
const requestBasePath = request.app._basePath || '';
|
||||
|
||||
return `${serverBasePath}${requestBasePath}`;
|
||||
});
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`1, 1 throws Error 1`] = `"Already have entry with this priority"`;
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { PriorityCollection } from './priority_collection';
|
||||
|
||||
test(`1, 2, 3`, () => {
|
||||
const priorityCollection = new PriorityCollection();
|
||||
priorityCollection.add(1, 1);
|
||||
priorityCollection.add(2, 2);
|
||||
priorityCollection.add(3, 3);
|
||||
expect(priorityCollection.toPrioritizedArray()).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test(`3, 2, 1`, () => {
|
||||
const priorityCollection = new PriorityCollection();
|
||||
priorityCollection.add(3, 3);
|
||||
priorityCollection.add(2, 2);
|
||||
priorityCollection.add(1, 1);
|
||||
expect(priorityCollection.toPrioritizedArray()).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test(`2, 3, 1`, () => {
|
||||
const priorityCollection = new PriorityCollection();
|
||||
priorityCollection.add(2, 2);
|
||||
priorityCollection.add(3, 3);
|
||||
priorityCollection.add(1, 1);
|
||||
expect(priorityCollection.toPrioritizedArray()).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test(`Number.MAX_VALUE, NUMBER.MIN_VALUE, 1`, () => {
|
||||
const priorityCollection = new PriorityCollection();
|
||||
priorityCollection.add(Number.MAX_VALUE, 3);
|
||||
priorityCollection.add(Number.MIN_VALUE, 1);
|
||||
priorityCollection.add(1, 2);
|
||||
expect(priorityCollection.toPrioritizedArray()).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test(`1, 1 throws Error`, () => {
|
||||
const priorityCollection = new PriorityCollection();
|
||||
priorityCollection.add(1, 1);
|
||||
expect(() => priorityCollection.add(1, 1)).toThrowErrorMatchingSnapshot();
|
||||
});
|
44
src/server/saved_objects/service/lib/priority_collection.ts
Normal file
44
src/server/saved_objects/service/lib/priority_collection.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
interface PriorityCollectionEntry<T> {
|
||||
priority: number;
|
||||
value: T;
|
||||
}
|
||||
|
||||
export class PriorityCollection<T> {
|
||||
private readonly array: Array<PriorityCollectionEntry<T>> = [];
|
||||
|
||||
public add(priority: number, value: T) {
|
||||
const foundIndex = this.array.findIndex(current => {
|
||||
if (priority === current.priority) {
|
||||
throw new Error('Already have entry with this priority');
|
||||
}
|
||||
|
||||
return priority < current.priority;
|
||||
});
|
||||
|
||||
const spliceIndex = foundIndex === -1 ? this.array.length : foundIndex;
|
||||
this.array.splice(spliceIndex, 0, { priority, value });
|
||||
}
|
||||
|
||||
public toPrioritizedArray(): T[] {
|
||||
return this.array.map(entry => entry.value);
|
||||
}
|
||||
}
|
|
@ -49,6 +49,7 @@ export class SavedObjectsRepository {
|
|||
this._migrator = migrator;
|
||||
this._index = index;
|
||||
this._mappings = mappings;
|
||||
this._schema = schema;
|
||||
this._type = getRootType(this._mappings);
|
||||
this._onBeforeWrite = onBeforeWrite;
|
||||
this._unwrappedCallCluster = callCluster;
|
||||
|
|
|
@ -16,13 +16,14 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { PriorityCollection } from './priority_collection';
|
||||
|
||||
/**
|
||||
* Provider for the Scoped Saved Object Client.
|
||||
*/
|
||||
export class ScopedSavedObjectsClientProvider {
|
||||
|
||||
_wrapperFactories = [];
|
||||
_wrapperFactories = new PriorityCollection();
|
||||
|
||||
constructor({
|
||||
defaultClientFactory
|
||||
|
@ -30,16 +31,8 @@ export class ScopedSavedObjectsClientProvider {
|
|||
this._originalClientFactory = this._clientFactory = defaultClientFactory;
|
||||
}
|
||||
|
||||
// the client wrapper factories are put at the front of the array, so that
|
||||
// when we use `reduce` below they're invoked in LIFO order. This is so that
|
||||
// if multiple plugins register their client wrapper factories, then we can use
|
||||
// the plugin dependencies/optionalDependencies to implicitly control the order
|
||||
// in which these are used. For example, if we have a plugin a that declares a
|
||||
// dependency on plugin b, that means that plugin b's client wrapper would want
|
||||
// to be able to run first when the SavedObjectClient methods are invoked to
|
||||
// provide additional context to plugin a's client wrapper.
|
||||
addClientWrapperFactory(wrapperFactory) {
|
||||
this._wrapperFactories.unshift(wrapperFactory);
|
||||
addClientWrapperFactory(priority, wrapperFactory) {
|
||||
this._wrapperFactories.add(priority, wrapperFactory);
|
||||
}
|
||||
|
||||
setClientFactory(customClientFactory) {
|
||||
|
@ -55,11 +48,13 @@ export class ScopedSavedObjectsClientProvider {
|
|||
request,
|
||||
});
|
||||
|
||||
return this._wrapperFactories.reduce((clientToWrap, wrapperFactory) => {
|
||||
return wrapperFactory({
|
||||
request,
|
||||
client: clientToWrap,
|
||||
});
|
||||
}, client);
|
||||
return this._wrapperFactories
|
||||
.toPrioritizedArray()
|
||||
.reduceRight((clientToWrap, wrapperFactory) => {
|
||||
return wrapperFactory({
|
||||
request,
|
||||
client: clientToWrap,
|
||||
});
|
||||
}, client);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,40 +64,20 @@ test(`throws error when more than one scoped saved objects client factory is set
|
|||
}).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test(`invokes and uses instance from single added wrapper factory`, () => {
|
||||
test(`invokes and uses wrappers in specified order`, () => {
|
||||
const defaultClient = Symbol();
|
||||
const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient);
|
||||
const clientProvider = new ScopedSavedObjectsClientProvider({
|
||||
defaultClientFactory: defaultClientFactoryMock
|
||||
});
|
||||
const wrappedClient = Symbol();
|
||||
const clientWrapperFactoryMock = jest.fn().mockReturnValue(wrappedClient);
|
||||
const request = Symbol();
|
||||
|
||||
clientProvider.addClientWrapperFactory(clientWrapperFactoryMock);
|
||||
const actualClient = clientProvider.getClient(request);
|
||||
|
||||
expect(actualClient).toBe(wrappedClient);
|
||||
expect(clientWrapperFactoryMock).toHaveBeenCalledWith({
|
||||
request,
|
||||
client: defaultClient
|
||||
});
|
||||
});
|
||||
|
||||
test(`invokes and uses wrappers in LIFO order`, () => {
|
||||
const defaultClient = Symbol();
|
||||
const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient);
|
||||
const clientProvider = new ScopedSavedObjectsClientProvider({
|
||||
defaultClientFactory: defaultClientFactoryMock
|
||||
});
|
||||
const firstWrappedClient = Symbol();
|
||||
const firstWrappedClient = Symbol('first client');
|
||||
const firstClientWrapperFactoryMock = jest.fn().mockReturnValue(firstWrappedClient);
|
||||
const secondWrapperClient = Symbol();
|
||||
const secondWrapperClient = Symbol('second client');
|
||||
const secondClientWrapperFactoryMock = jest.fn().mockReturnValue(secondWrapperClient);
|
||||
const request = Symbol();
|
||||
|
||||
clientProvider.addClientWrapperFactory(firstClientWrapperFactoryMock);
|
||||
clientProvider.addClientWrapperFactory(secondClientWrapperFactoryMock);
|
||||
clientProvider.addClientWrapperFactory(1, secondClientWrapperFactoryMock);
|
||||
clientProvider.addClientWrapperFactory(0, firstClientWrapperFactoryMock);
|
||||
const actualClient = clientProvider.getClient(request);
|
||||
|
||||
expect(actualClient).toBe(firstWrappedClient);
|
||||
|
|
|
@ -27,6 +27,7 @@ const basePath = '/someBasePath';
|
|||
|
||||
function init(customInternals = { basePath }) {
|
||||
const chrome = {
|
||||
addBasePath: (path) => path,
|
||||
getBasePath: () => customInternals.basePath || '',
|
||||
};
|
||||
const internals = {
|
||||
|
@ -39,7 +40,7 @@ function init(customInternals = { basePath }) {
|
|||
|
||||
describe('chrome nav apis', function () {
|
||||
describe('#getNavLinkById', () => {
|
||||
it ('retrieves the correct nav link, given its ID', () => {
|
||||
it('retrieves the correct nav link, given its ID', () => {
|
||||
const appUrlStore = new StubBrowserStorage();
|
||||
const nav = [
|
||||
{ id: 'kibana:discover', title: 'Discover' }
|
||||
|
@ -52,7 +53,7 @@ describe('chrome nav apis', function () {
|
|||
expect(navLink).to.eql(nav[0]);
|
||||
});
|
||||
|
||||
it ('throws an error if the nav link with the given ID is not found', () => {
|
||||
it('throws an error if the nav link with the given ID is not found', () => {
|
||||
const appUrlStore = new StubBrowserStorage();
|
||||
const nav = [
|
||||
{ id: 'kibana:discover', title: 'Discover' }
|
||||
|
|
|
@ -131,8 +131,8 @@ export function initChromeNavApi(chrome, internals) {
|
|||
};
|
||||
|
||||
internals.nav.forEach(link => {
|
||||
link.url = relativeToAbsolute(link.url);
|
||||
link.subUrlBase = relativeToAbsolute(link.subUrlBase);
|
||||
link.url = relativeToAbsolute(chrome.addBasePath(link.url));
|
||||
link.subUrlBase = relativeToAbsolute(chrome.addBasePath(link.subUrlBase));
|
||||
});
|
||||
|
||||
// simulate a possible change in url to initialize the
|
||||
|
|
1
src/ui/public/chrome/index.d.ts
vendored
1
src/ui/public/chrome/index.d.ts
vendored
|
@ -28,6 +28,7 @@ declare class Chrome {
|
|||
public getXsrfToken(): string;
|
||||
public getKibanaVersion(): string;
|
||||
public getUiSettingsClient(): any;
|
||||
public setVisible(visible: boolean): any;
|
||||
public getInjected(key: string, defaultValue?: any): any;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
jest.mock('ui/chrome',
|
||||
() => ({
|
||||
getBasePath: () => `/some/base/path`,
|
||||
getUiSettingsClient: () => {
|
||||
return {
|
||||
get: (key) => {
|
||||
|
|
|
@ -21,6 +21,7 @@ import { ManagementSection } from './section';
|
|||
|
||||
export {
|
||||
PAGE_TITLE_COMPONENT,
|
||||
PAGE_SUBTITLE_COMPONENT,
|
||||
PAGE_FOOTER_COMPONENT,
|
||||
} from '../../../core_plugins/kibana/public/management/sections/settings/components/default_component_registry';
|
||||
|
||||
|
|
31
src/ui/public/persisted_log/create_log_key.js
Normal file
31
src/ui/public/persisted_log/create_log_key.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { Sha256 } from '../crypto';
|
||||
|
||||
export function createLogKey(type, optionalIdentifier) {
|
||||
const baseKey = `kibana.history.${type}`;
|
||||
|
||||
if (!optionalIdentifier) {
|
||||
return baseKey;
|
||||
}
|
||||
|
||||
const protectedIdentifier = new Sha256().update(optionalIdentifier, 'utf8').digest('base64');
|
||||
return `${baseKey}-${protectedIdentifier}`;
|
||||
}
|
35
src/ui/public/persisted_log/create_log_key.test.js
Normal file
35
src/ui/public/persisted_log/create_log_key.test.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { createLogKey } from './create_log_key';
|
||||
|
||||
describe('createLogKey', () => {
|
||||
it('should create a key starting with "kibana.history"', () => {
|
||||
expect(createLogKey('foo', 'bar')).toMatch(/^kibana\.history/);
|
||||
});
|
||||
|
||||
it('should include a hashed suffix of the identifier when present', () => {
|
||||
const expectedSuffix = `/N4rLtula/QIYB+3If6bXDONEO5CnqBPrlURto+/j7k=`;
|
||||
expect(createLogKey('foo', 'bar')).toMatch(`kibana.history.foo-${expectedSuffix}`);
|
||||
});
|
||||
|
||||
it('should not include a hashed suffix if the identifier is not present', () => {
|
||||
expect(createLogKey('foo')).toEqual('kibana.history.foo');
|
||||
});
|
||||
});
|
|
@ -22,6 +22,12 @@ import sinon from 'sinon';
|
|||
import expect from 'expect.js';
|
||||
import { PersistedLog } from './';
|
||||
|
||||
jest.mock('ui/chrome', () => {
|
||||
return {
|
||||
getBasePath: () => `/some/base/path`
|
||||
};
|
||||
});
|
||||
|
||||
const historyName = 'testHistory';
|
||||
const historyLimit = 10;
|
||||
const payload = [
|
||||
|
|
|
@ -16,8 +16,9 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import { PersistedLog } from './';
|
||||
import { createLogKey } from './create_log_key';
|
||||
|
||||
class RecentlyAccessed {
|
||||
constructor() {
|
||||
|
@ -28,7 +29,8 @@ class RecentlyAccessed {
|
|||
return oldItem.id === newItem.id;
|
||||
}
|
||||
};
|
||||
this.history = new PersistedLog('kibana.history.recentlyAccessed', historyOptions);
|
||||
const logKey = createLogKey('recentlyAccessed', chrome.getBasePath());
|
||||
this.history = new PersistedLog(logKey, historyOptions);
|
||||
}
|
||||
|
||||
add(link, label, id) {
|
||||
|
|
|
@ -19,10 +19,11 @@
|
|||
|
||||
jest.mock('ui/chrome',
|
||||
() => ({
|
||||
getBasePath: () => `/some/base/path`,
|
||||
getUiSettingsClient: () => {
|
||||
return {
|
||||
get: (key) => {
|
||||
switch(key) {
|
||||
switch (key) {
|
||||
case 'timepicker:timeDefaults':
|
||||
return { from: 'now-15m', to: 'now', mode: 'quick' };
|
||||
case 'timepicker:refreshIntervalDefaults':
|
||||
|
@ -107,7 +108,7 @@ describe('setRefreshInterval', () => {
|
|||
let update;
|
||||
let fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(() => {
|
||||
update = sinon.spy();
|
||||
fetch = sinon.spy();
|
||||
timefilter.setRefreshInterval({
|
||||
|
@ -191,7 +192,7 @@ describe('setRefreshInterval', () => {
|
|||
describe('isTimeRangeSelectorEnabled', () => {
|
||||
let update;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(() => {
|
||||
update = sinon.spy();
|
||||
timefilter.on('enabledUpdated', update);
|
||||
});
|
||||
|
@ -212,7 +213,7 @@ describe('isTimeRangeSelectorEnabled', () => {
|
|||
describe('isAutoRefreshSelectorEnabled', () => {
|
||||
let update;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(() => {
|
||||
update = sinon.spy();
|
||||
timefilter.on('enabledUpdated', update);
|
||||
});
|
||||
|
|
|
@ -19,10 +19,11 @@
|
|||
|
||||
jest.mock('ui/chrome',
|
||||
() => ({
|
||||
getBasePath: () => `/some/base/path`,
|
||||
getUiSettingsClient: () => {
|
||||
return {
|
||||
get: (key) => {
|
||||
switch(key) {
|
||||
switch (key) {
|
||||
case 'timepicker:timeDefaults':
|
||||
return { from: 'now-15m', to: 'now', mode: 'quick' };
|
||||
case 'timepicker:refreshIntervalDefaults':
|
||||
|
|
|
@ -60,7 +60,7 @@ export class UiApp {
|
|||
// unless an app is hidden it gets a navlink, but we only respond to `getNavLink()`
|
||||
// if the app is also listed. This means that all apps in the kibanaPayload will
|
||||
// have a navLink property since that list includes all normally accessible apps
|
||||
this._navLink = new UiNavLink(kbnServer.config.get('server.basePath'), {
|
||||
this._navLink = new UiNavLink({
|
||||
id: this._id,
|
||||
title: this._title,
|
||||
order: this._order,
|
||||
|
|
|
@ -24,7 +24,6 @@ import { UiNavLink } from '../ui_nav_link';
|
|||
describe('UiNavLink', () => {
|
||||
describe('constructor', () => {
|
||||
it('initializes the object properties as expected', () => {
|
||||
const urlBasePath = 'http://localhost:5601/rnd';
|
||||
const spec = {
|
||||
id: 'kibana:discover',
|
||||
title: 'Discover',
|
||||
|
@ -36,13 +35,13 @@ describe('UiNavLink', () => {
|
|||
disabled: true
|
||||
};
|
||||
|
||||
const link = new UiNavLink(urlBasePath, spec);
|
||||
const link = new UiNavLink(spec);
|
||||
expect(link.toJSON()).to.eql({
|
||||
id: spec.id,
|
||||
title: spec.title,
|
||||
order: spec.order,
|
||||
url: `${urlBasePath}${spec.url}`,
|
||||
subUrlBase: `${urlBasePath}${spec.url}`,
|
||||
url: spec.url,
|
||||
subUrlBase: spec.url,
|
||||
description: spec.description,
|
||||
icon: spec.icon,
|
||||
hidden: spec.hidden,
|
||||
|
@ -54,22 +53,7 @@ describe('UiNavLink', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('initializes the url property without a base path when one is not specified in the spec', () => {
|
||||
const urlBasePath = undefined;
|
||||
const spec = {
|
||||
id: 'kibana:discover',
|
||||
title: 'Discover',
|
||||
order: -1003,
|
||||
url: '/app/kibana#/discover',
|
||||
description: 'interactively explore your data',
|
||||
icon: 'plugins/kibana/assets/discover.svg',
|
||||
};
|
||||
const link = new UiNavLink(urlBasePath, spec);
|
||||
expect(link.toJSON()).to.have.property('url', spec.url);
|
||||
});
|
||||
|
||||
it('initializes the order property to 0 when order is not specified in the spec', () => {
|
||||
const urlBasePath = undefined;
|
||||
const spec = {
|
||||
id: 'kibana:discover',
|
||||
title: 'Discover',
|
||||
|
@ -77,13 +61,12 @@ describe('UiNavLink', () => {
|
|||
description: 'interactively explore your data',
|
||||
icon: 'plugins/kibana/assets/discover.svg',
|
||||
};
|
||||
const link = new UiNavLink(urlBasePath, spec);
|
||||
const link = new UiNavLink(spec);
|
||||
|
||||
expect(link.toJSON()).to.have.property('order', 0);
|
||||
});
|
||||
|
||||
it('initializes the linkToLastSubUrl property to false when false is specified in the spec', () => {
|
||||
const urlBasePath = undefined;
|
||||
const spec = {
|
||||
id: 'kibana:discover',
|
||||
title: 'Discover',
|
||||
|
@ -93,13 +76,12 @@ describe('UiNavLink', () => {
|
|||
icon: 'plugins/kibana/assets/discover.svg',
|
||||
linkToLastSubUrl: false
|
||||
};
|
||||
const link = new UiNavLink(urlBasePath, spec);
|
||||
const link = new UiNavLink(spec);
|
||||
|
||||
expect(link.toJSON()).to.have.property('linkToLastSubUrl', false);
|
||||
});
|
||||
|
||||
it('initializes the linkToLastSubUrl property to true by default', () => {
|
||||
const urlBasePath = undefined;
|
||||
const spec = {
|
||||
id: 'kibana:discover',
|
||||
title: 'Discover',
|
||||
|
@ -108,13 +90,12 @@ describe('UiNavLink', () => {
|
|||
description: 'interactively explore your data',
|
||||
icon: 'plugins/kibana/assets/discover.svg',
|
||||
};
|
||||
const link = new UiNavLink(urlBasePath, spec);
|
||||
const link = new UiNavLink(spec);
|
||||
|
||||
expect(link.toJSON()).to.have.property('linkToLastSubUrl', true);
|
||||
});
|
||||
|
||||
it('initializes the hidden property to false by default', () => {
|
||||
const urlBasePath = undefined;
|
||||
const spec = {
|
||||
id: 'kibana:discover',
|
||||
title: 'Discover',
|
||||
|
@ -123,13 +104,12 @@ describe('UiNavLink', () => {
|
|||
description: 'interactively explore your data',
|
||||
icon: 'plugins/kibana/assets/discover.svg',
|
||||
};
|
||||
const link = new UiNavLink(urlBasePath, spec);
|
||||
const link = new UiNavLink(spec);
|
||||
|
||||
expect(link.toJSON()).to.have.property('hidden', false);
|
||||
});
|
||||
|
||||
it('initializes the disabled property to false by default', () => {
|
||||
const urlBasePath = undefined;
|
||||
const spec = {
|
||||
id: 'kibana:discover',
|
||||
title: 'Discover',
|
||||
|
@ -138,13 +118,12 @@ describe('UiNavLink', () => {
|
|||
description: 'interactively explore your data',
|
||||
icon: 'plugins/kibana/assets/discover.svg',
|
||||
};
|
||||
const link = new UiNavLink(urlBasePath, spec);
|
||||
const link = new UiNavLink(spec);
|
||||
|
||||
expect(link.toJSON()).to.have.property('disabled', false);
|
||||
});
|
||||
|
||||
it('initializes the tooltip property to an empty string by default', () => {
|
||||
const urlBasePath = undefined;
|
||||
const spec = {
|
||||
id: 'kibana:discover',
|
||||
title: 'Discover',
|
||||
|
@ -153,7 +132,7 @@ describe('UiNavLink', () => {
|
|||
description: 'interactively explore your data',
|
||||
icon: 'plugins/kibana/assets/discover.svg',
|
||||
};
|
||||
const link = new UiNavLink(urlBasePath, spec);
|
||||
const link = new UiNavLink(spec);
|
||||
|
||||
expect(link.toJSON()).to.have.property('tooltip', '');
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
export class UiNavLink {
|
||||
constructor(urlBasePath, spec) {
|
||||
constructor(spec) {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
|
@ -36,8 +36,8 @@ export class UiNavLink {
|
|||
this._id = id;
|
||||
this._title = title;
|
||||
this._order = order;
|
||||
this._url = `${urlBasePath || ''}${url}`;
|
||||
this._subUrlBase = `${urlBasePath || ''}${subUrlBase || url}`;
|
||||
this._url = url;
|
||||
this._subUrlBase = subUrlBase || url;
|
||||
this._description = description;
|
||||
this._icon = icon;
|
||||
this._linkToLastSubUrl = linkToLastSubUrl;
|
||||
|
|
|
@ -19,14 +19,13 @@
|
|||
|
||||
import { UiNavLink } from './ui_nav_link';
|
||||
|
||||
export function uiNavLinksMixin(kbnServer, server, config) {
|
||||
export function uiNavLinksMixin(kbnServer, server) {
|
||||
const uiApps = server.getAllUiApps();
|
||||
|
||||
const { navLinkSpecs = [] } = kbnServer.uiExports;
|
||||
const urlBasePath = config.get('server.basePath');
|
||||
|
||||
const fromSpecs = navLinkSpecs
|
||||
.map(navLinkSpec => new UiNavLink(urlBasePath, navLinkSpec));
|
||||
.map(navLinkSpec => new UiNavLink(navLinkSpec));
|
||||
|
||||
const fromApps = uiApps
|
||||
.map(app => app.getNavLink())
|
||||
|
|
|
@ -124,7 +124,7 @@ export function uiRenderMixin(kbnServer, server, config) {
|
|||
branch: config.get('pkg.branch'),
|
||||
buildNum: config.get('pkg.buildNum'),
|
||||
buildSha: config.get('pkg.buildSha'),
|
||||
basePath: config.get('server.basePath'),
|
||||
basePath: request.getBasePath(),
|
||||
serverName: config.get('server.name'),
|
||||
devMode: config.get('env.dev'),
|
||||
uiSettings: await props({
|
||||
|
@ -138,7 +138,7 @@ export function uiRenderMixin(kbnServer, server, config) {
|
|||
try {
|
||||
const request = reply.request;
|
||||
const translations = await server.getUiTranslations();
|
||||
const basePath = config.get('server.basePath');
|
||||
const basePath = request.getBasePath();
|
||||
|
||||
return reply.view('ui_app', {
|
||||
uiPublicUrl: `${basePath}/ui`,
|
||||
|
|
|
@ -201,4 +201,4 @@ describe('createOrUpgradeSavedConfig()', () => {
|
|||
'5.4.0-rc1': true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -135,4 +135,4 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () {
|
|||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -55,4 +55,4 @@ export async function createOrUpgradeSavedConfig(options) {
|
|||
attributes,
|
||||
{ id: version }
|
||||
);
|
||||
}
|
||||
}
|
|
@ -197,4 +197,4 @@ export class UiSettingsService {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,4 +49,4 @@ export function uiSettingsServiceFactory(server, options) {
|
|||
overrides,
|
||||
log: (...args) => server.log(...args),
|
||||
});
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -188,6 +188,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"namespace": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
},
|
||||
|
@ -249,4 +252,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"ui/*": ["src/ui/public/*"]
|
||||
"ui/*": [
|
||||
"src/ui/public/*"
|
||||
]
|
||||
},
|
||||
// Support .tsx files and transform JSX into calls to React.createElement
|
||||
"jsx": "react",
|
||||
|
||||
// Enables all strict type checking options.
|
||||
"strict": true,
|
||||
|
||||
// enables "core language features"
|
||||
"lib": [
|
||||
// ESNext auto includes previous versions all the way back to es5
|
||||
|
@ -17,39 +17,29 @@
|
|||
// includes support for browser APIs
|
||||
"dom"
|
||||
],
|
||||
|
||||
// Node 8 should support everything output by esnext, we override this
|
||||
// in webpack with loader-level compiler options
|
||||
"target": "esnext",
|
||||
|
||||
// Use commonjs for node, overridden in webpack to keep import statements
|
||||
// to maintain support for things like `await import()`
|
||||
"module": "commonjs",
|
||||
|
||||
// Allows default imports from modules with no default export. This does not affect code emit, just type checking.
|
||||
// We have to enable this option explicitly since `esModuleInterop` doesn't enable it automatically when ES2015 or
|
||||
// ESNext module format is used.
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
||||
// Emits __importStar and __importDefault helpers for runtime babel ecosystem compatibility.
|
||||
"esModuleInterop": true,
|
||||
|
||||
// Resolve modules in the same way as Node.js. Aka make `require` works the
|
||||
// same in TypeScript as it does in Node.js.
|
||||
"moduleResolution": "node",
|
||||
|
||||
// Disallow inconsistently-cased references to the same file.
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
// Disable the breaking keyof behaviour introduced in TS 2.9.2 until EUI is updated to support that too
|
||||
"keyofStringsOnly": true,
|
||||
|
||||
// Forbid unused local variables as the rule was deprecated by ts-lint
|
||||
"noUnusedLocals": true,
|
||||
|
||||
// Provide full support for iterables in for..of, spread and destructuring when targeting ES5 or ES3.
|
||||
"downlevelIteration": true,
|
||||
|
||||
// import tslib helpers rather than inlining helpers for iteration or spreading, for instance
|
||||
"importHelpers": true
|
||||
},
|
||||
|
@ -64,4 +54,4 @@
|
|||
// the tsconfig.json file for public files correctly.
|
||||
// "src/**/public/**/*"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ import { licenseManagement } from './plugins/license_management';
|
|||
import { cloud } from './plugins/cloud';
|
||||
import { indexManagement } from './plugins/index_management';
|
||||
import { consoleExtensions } from './plugins/console_extensions';
|
||||
import { spaces } from './plugins/spaces';
|
||||
import { notifications } from './plugins/notifications';
|
||||
import { kueryAutocomplete } from './plugins/kuery_autocomplete';
|
||||
import { canvas } from './plugins/canvas';
|
||||
|
@ -31,6 +32,7 @@ module.exports = function (kibana) {
|
|||
graph(kibana),
|
||||
monitoring(kibana),
|
||||
reporting(kibana),
|
||||
spaces(kibana),
|
||||
security(kibana),
|
||||
searchprofiler(kibana),
|
||||
ml(kibana),
|
||||
|
|
|
@ -25,8 +25,12 @@
|
|||
"@kbn/es": "link:../packages/kbn-es",
|
||||
"@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers",
|
||||
"@kbn/test": "link:../packages/kbn-test",
|
||||
"@types/expect.js": "^0.3.29",
|
||||
"@types/jest": "^23.3.1",
|
||||
"@types/joi": "^10.4.4",
|
||||
"@types/mocha": "^5.2.5",
|
||||
"@types/pngjs": "^3.3.1",
|
||||
"@types/supertest": "^2.0.5",
|
||||
"abab": "^1.0.4",
|
||||
"ansi-colors": "^3.0.5",
|
||||
"ansicolors": "0.3.2",
|
||||
|
@ -212,4 +216,4 @@
|
|||
"engines": {
|
||||
"yarn": "^1.6.0"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,28 @@ import { MemoryRouter } from 'react-router-dom';
|
|||
import Breadcrumbs from '../Breadcrumbs';
|
||||
import { toJson } from '../../../../utils/testHelpers';
|
||||
|
||||
jest.mock(
|
||||
'ui/chrome',
|
||||
() => ({
|
||||
getBasePath: () => `/some/base/path`,
|
||||
getUiSettingsClient: () => {
|
||||
return {
|
||||
get: key => {
|
||||
switch (key) {
|
||||
case 'timepicker:timeDefaults':
|
||||
return { from: 'now-15m', to: 'now', mode: 'quick' };
|
||||
case 'timepicker:refreshIntervalDefaults':
|
||||
return { display: 'Off', pause: false, value: 0 };
|
||||
default:
|
||||
throw new Error(`Unexpected config key: ${key}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}),
|
||||
{ virtual: true }
|
||||
);
|
||||
|
||||
function expectBreadcrumbToMatchSnapshot(route) {
|
||||
const wrapper = mount(
|
||||
<MemoryRouter initialEntries={[`${route}?_g=myG&kuery=myKuery`]}>
|
||||
|
|
|
@ -9,6 +9,34 @@ import { shallow } from 'enzyme';
|
|||
import TransactionOverview from '../view';
|
||||
import { toJson } from '../../../../utils/testHelpers';
|
||||
|
||||
jest.mock(
|
||||
'ui/chrome',
|
||||
() => ({
|
||||
getBasePath: () => `/some/base/path`,
|
||||
getInjected: key => {
|
||||
if (key === 'mlEnabled') {
|
||||
return true;
|
||||
}
|
||||
throw new Error(`inexpected key ${key}`);
|
||||
},
|
||||
getUiSettingsClient: () => {
|
||||
return {
|
||||
get: key => {
|
||||
switch (key) {
|
||||
case 'timepicker:timeDefaults':
|
||||
return { from: 'now-15m', to: 'now', mode: 'quick' };
|
||||
case 'timepicker:refreshIntervalDefaults':
|
||||
return { display: 'Off', pause: false, value: 0 };
|
||||
default:
|
||||
throw new Error(`Unexpected config key: ${key}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}),
|
||||
{ virtual: true }
|
||||
);
|
||||
|
||||
const setup = () => {
|
||||
const props = {
|
||||
license: {
|
||||
|
|
|
@ -21,6 +21,12 @@ jest.mock('../../services/field_format_service', () => ({
|
|||
getFieldFormat: jest.fn()
|
||||
}
|
||||
}));
|
||||
jest.mock('ui/chrome', () => ({
|
||||
getBasePath: (path) => path,
|
||||
getUiSettingsClient: () => ({
|
||||
get: () => null
|
||||
}),
|
||||
}));
|
||||
|
||||
import { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
|
|
@ -21,6 +21,12 @@ jest.mock('../../services/field_format_service', () => ({
|
|||
getFieldFormat: jest.fn()
|
||||
}
|
||||
}));
|
||||
jest.mock('ui/chrome', () => ({
|
||||
getBasePath: (path) => path,
|
||||
getUiSettingsClient: () => ({
|
||||
get: () => null
|
||||
}),
|
||||
}));
|
||||
|
||||
// The mocks for ui/chrome and ui/timefilter are copied from charts_utils.test.js
|
||||
// TODO: Refactor the involved tests to avoid this duplication
|
||||
|
|
|
@ -34,6 +34,13 @@ jest.mock('../../util/string_utils', () => ({
|
|||
mlEscape(d) { return d; }
|
||||
}));
|
||||
|
||||
jest.mock('ui/chrome', () => ({
|
||||
getBasePath: (path) => path,
|
||||
getUiSettingsClient: () => ({
|
||||
get: () => null
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockMlSelectSeverityService = {
|
||||
state: {
|
||||
get() { return { display: 'warning', val: 0 }; }
|
||||
|
|
|
@ -12,6 +12,13 @@ import React from 'react';
|
|||
|
||||
import { ExplorerSwimlane } from './explorer_swimlane';
|
||||
|
||||
jest.mock('ui/chrome', () => ({
|
||||
getBasePath: path => path,
|
||||
getUiSettingsClient: () => ({
|
||||
get: jest.fn()
|
||||
}),
|
||||
}));
|
||||
|
||||
function getExplorerSwimlaneMocks() {
|
||||
const mlExplorerDashboardService = {
|
||||
allowCellRangeSelection: false,
|
||||
|
|
|
@ -40,7 +40,7 @@ export class BulkUploader {
|
|||
throw new Error('interval number of milliseconds is required');
|
||||
}
|
||||
|
||||
this._timer = null;
|
||||
this._timer = null;
|
||||
this._interval = interval;
|
||||
this._log = {
|
||||
debug: message => server.log(['debug', ...LOGGING_TAGS], message),
|
||||
|
|
|
@ -109,13 +109,35 @@ describe('CSV Execute Job', function () {
|
|||
mockServer.config().get.withArgs('xpack.reporting.csv.scroll').returns({});
|
||||
});
|
||||
|
||||
describe('savedObjects', function () {
|
||||
it('calls getScopedSavedObjectsClient with request containing decrypted headers', async function () {
|
||||
describe('calls getScopedSavedObjectsClient with request', function () {
|
||||
it('containing decrypted headers', async function () {
|
||||
const executeJob = executeJobFactory(mockServer);
|
||||
await executeJob({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, cancellationToken);
|
||||
expect(mockServer.savedObjects.getScopedSavedObjectsClient.calledOnce).to.be(true);
|
||||
expect(mockServer.savedObjects.getScopedSavedObjectsClient.firstCall.args[0].headers).to.be.eql(headers);
|
||||
});
|
||||
|
||||
it(`containing getBasePath() returning server's basePath if the job doesn't have one`, async function () {
|
||||
const serverBasePath = '/foo-server/basePath/';
|
||||
mockServer.config().get.withArgs('server.basePath').returns(serverBasePath);
|
||||
const executeJob = executeJobFactory(mockServer);
|
||||
await executeJob({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, cancellationToken);
|
||||
expect(mockServer.savedObjects.getScopedSavedObjectsClient.calledOnce).to.be(true);
|
||||
expect(mockServer.savedObjects.getScopedSavedObjectsClient.firstCall.args[0].getBasePath()).to.be.eql(serverBasePath);
|
||||
});
|
||||
|
||||
it(`containing getBasePath() returning job's basePath if the job has one`, async function () {
|
||||
const serverBasePath = '/foo-server/basePath/';
|
||||
mockServer.config().get.withArgs('server.basePath').returns(serverBasePath);
|
||||
const executeJob = executeJobFactory(mockServer);
|
||||
const jobBasePath = 'foo-job/basePath/';
|
||||
await executeJob(
|
||||
{ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, basePath: jobBasePath },
|
||||
cancellationToken
|
||||
);
|
||||
expect(mockServer.savedObjects.getScopedSavedObjectsClient.calledOnce).to.be(true);
|
||||
expect(mockServer.savedObjects.getScopedSavedObjectsClient.firstCall.args[0].getBasePath()).to.be.eql(jobBasePath);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uiSettings', function () {
|
||||
|
|
|
@ -21,6 +21,7 @@ function createJobFn(server) {
|
|||
return {
|
||||
headers: serializedEncryptedHeaders,
|
||||
indexPatternSavedObject: indexPatternSavedObject,
|
||||
basePath: request.getBasePath(),
|
||||
...jobParams
|
||||
};
|
||||
};
|
||||
|
|
|
@ -16,9 +16,18 @@ function executeJobFn(server) {
|
|||
const config = server.config();
|
||||
const logger = createTaggedLogger(server, ['reporting', 'csv', 'debug']);
|
||||
const generateCsv = createGenerateCsv(logger);
|
||||
const serverBasePath = config.get('server.basePath');
|
||||
|
||||
return async function executeJob(job, cancellationToken) {
|
||||
const { searchRequest, fields, indexPatternSavedObject, metaFields, conflictedTypesFields, headers: serializedEncryptedHeaders } = job;
|
||||
const {
|
||||
searchRequest,
|
||||
fields,
|
||||
indexPatternSavedObject,
|
||||
metaFields,
|
||||
conflictedTypesFields,
|
||||
headers: serializedEncryptedHeaders,
|
||||
basePath
|
||||
} = job;
|
||||
|
||||
let decryptedHeaders;
|
||||
try {
|
||||
|
@ -31,6 +40,10 @@ function executeJobFn(server) {
|
|||
|
||||
const fakeRequest = {
|
||||
headers: decryptedHeaders,
|
||||
// This is used by the spaces SavedObjectClientWrapper to determine the existing space.
|
||||
// We use the basePath from the saved job, which we'll have post spaces being implemented;
|
||||
// or we use the server base path, which uses the default space
|
||||
getBasePath: () => basePath || serverBasePath,
|
||||
};
|
||||
|
||||
const callEndpoint = (endpoint, clientParams = {}, options = {}) => {
|
||||
|
|
|
@ -18,7 +18,7 @@ function createJobFn(server) {
|
|||
relativeUrls,
|
||||
browserTimezone,
|
||||
layout
|
||||
}, headers) {
|
||||
}, headers, request) {
|
||||
const serializedEncryptedHeaders = await crypto.encrypt(headers);
|
||||
|
||||
return {
|
||||
|
@ -28,6 +28,7 @@ function createJobFn(server) {
|
|||
headers: serializedEncryptedHeaders,
|
||||
browserTimezone,
|
||||
layout,
|
||||
basePath: request.getBasePath(),
|
||||
forceNow: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
|
|
|
@ -10,28 +10,28 @@ import { getAbsoluteUrlFactory } from './get_absolute_url';
|
|||
export function compatibilityShimFactory(server) {
|
||||
const getAbsoluteUrl = getAbsoluteUrlFactory(server);
|
||||
|
||||
const getSavedObjectAbsoluteUrl = (savedObj) => {
|
||||
if (savedObj.urlHash) {
|
||||
return getAbsoluteUrl({ hash: savedObj.urlHash });
|
||||
const getSavedObjectAbsoluteUrl = (job, savedObject) => {
|
||||
if (savedObject.urlHash) {
|
||||
return getAbsoluteUrl({ hash: savedObject.urlHash });
|
||||
}
|
||||
|
||||
if (savedObj.relativeUrl) {
|
||||
const { pathname: path, hash, search } = url.parse(savedObj.relativeUrl);
|
||||
return getAbsoluteUrl({ path, hash, search });
|
||||
if (savedObject.relativeUrl) {
|
||||
const { pathname: path, hash, search } = url.parse(savedObject.relativeUrl);
|
||||
return getAbsoluteUrl({ basePath: job.basePath, path, hash, search });
|
||||
}
|
||||
|
||||
if (savedObj.url.startsWith(getAbsoluteUrl())) {
|
||||
return savedObj.url;
|
||||
if (savedObject.url.startsWith(getAbsoluteUrl())) {
|
||||
return savedObject.url;
|
||||
}
|
||||
|
||||
throw new Error(`Unable to generate report for url ${savedObj.url}, it's not a Kibana URL`);
|
||||
throw new Error(`Unable to generate report for url ${savedObject.url}, it's not a Kibana URL`);
|
||||
};
|
||||
|
||||
return function (executeJob) {
|
||||
return async function (job, cancellationToken) {
|
||||
const urls = job.objects.map(getSavedObjectAbsoluteUrl);
|
||||
const urls = job.objects.map(savedObject => getSavedObjectAbsoluteUrl(job, savedObject));
|
||||
|
||||
return await executeJob({ ...job, urls }, cancellationToken);
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ test(`it generates the absolute url if a urlHash is provided`, async () => {
|
|||
expect(mockCreateJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/app/kibana#visualize');
|
||||
});
|
||||
|
||||
test(`it generates the absolute url if a relativeUrl is provided`, async () => {
|
||||
test(`it generates the absolute url using server's basePath if a relativeUrl is provided`, async () => {
|
||||
const mockCreateJob = jest.fn();
|
||||
const compatibilityShim = compatibilityShimFactory(createMockServer());
|
||||
|
||||
|
@ -64,7 +64,17 @@ test(`it generates the absolute url if a relativeUrl is provided`, async () => {
|
|||
expect(mockCreateJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/app/kibana#/visualize?');
|
||||
});
|
||||
|
||||
test(`it generates the absolute url if a relativeUrl with querystring is provided`, async () => {
|
||||
test(`it generates the absolute url using job's basePath if a relativeUrl is provided`, async () => {
|
||||
const mockCreateJob = jest.fn();
|
||||
const compatibilityShim = compatibilityShimFactory(createMockServer());
|
||||
|
||||
const relativeUrl = '/app/kibana#/visualize?';
|
||||
await compatibilityShim(mockCreateJob)({ basePath: '/s/marketing', objects: [ { relativeUrl } ] });
|
||||
expect(mockCreateJob.mock.calls.length).toBe(1);
|
||||
expect(mockCreateJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/s/marketing/app/kibana#/visualize?');
|
||||
});
|
||||
|
||||
test(`it generates the absolute url using server's basePath if a relativeUrl with querystring is provided`, async () => {
|
||||
const mockCreateJob = jest.fn();
|
||||
const compatibilityShim = compatibilityShimFactory(createMockServer());
|
||||
|
||||
|
@ -74,6 +84,16 @@ test(`it generates the absolute url if a relativeUrl with querystring is provide
|
|||
expect(mockCreateJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/app/kibana?_t=123456789#/visualize?_g=()');
|
||||
});
|
||||
|
||||
test(`it generates the absolute url using job's basePath if a relativeUrl with querystring is provided`, async () => {
|
||||
const mockCreateJob = jest.fn();
|
||||
const compatibilityShim = compatibilityShimFactory(createMockServer());
|
||||
|
||||
const relativeUrl = '/app/kibana?_t=123456789#/visualize?_g=()';
|
||||
await compatibilityShim(mockCreateJob)({ basePath: '/s/marketing', objects: [ { relativeUrl } ] });
|
||||
expect(mockCreateJob.mock.calls.length).toBe(1);
|
||||
expect(mockCreateJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/s/marketing/app/kibana?_t=123456789#/visualize?_g=()');
|
||||
});
|
||||
|
||||
test(`it passes the provided browserTimezone through`, async () => {
|
||||
const mockCreateJob = jest.fn();
|
||||
const compatibilityShim = compatibilityShimFactory(createMockServer());
|
||||
|
|
|
@ -11,6 +11,7 @@ function getAbsoluteUrlFn(server) {
|
|||
const config = server.config();
|
||||
|
||||
return function getAbsoluteUrl({
|
||||
basePath = config.get('server.basePath'),
|
||||
hash,
|
||||
path = '/app/kibana',
|
||||
search
|
||||
|
@ -19,7 +20,7 @@ function getAbsoluteUrlFn(server) {
|
|||
protocol: config.get('xpack.reporting.kibanaServer.protocol') || server.info.protocol,
|
||||
hostname: config.get('xpack.reporting.kibanaServer.hostname') || config.get('server.host'),
|
||||
port: config.get('xpack.reporting.kibanaServer.port') || config.get('server.port'),
|
||||
pathname: config.get('server.basePath') + path,
|
||||
pathname: basePath + path,
|
||||
hash: hash,
|
||||
search
|
||||
});
|
||||
|
|
|
@ -92,6 +92,14 @@ test(`uses the provided hash with queryString`, () => {
|
|||
expect(absoluteUrl).toBe(`http://something:8080/tst/app/kibana#${hash}`);
|
||||
});
|
||||
|
||||
test(`uses the provided basePath`, () => {
|
||||
const mockServer = createMockServer();
|
||||
|
||||
const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer);
|
||||
const absoluteUrl = getAbsoluteUrl({ basePath: '/s/marketing' });
|
||||
expect(absoluteUrl).toBe(`http://something:8080/s/marketing/app/kibana`);
|
||||
});
|
||||
|
||||
test(`uses the path`, () => {
|
||||
const mockServer = createMockServer();
|
||||
|
||||
|
@ -109,3 +117,5 @@ test(`uses the search`, () => {
|
|||
const absoluteUrl = getAbsoluteUrl({ search });
|
||||
expect(absoluteUrl).toBe(`http://something:8080/tst/app/kibana?${search}`);
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -31,6 +31,8 @@ function executeJobFn(server) {
|
|||
const crypto = cryptoFactory(server);
|
||||
const compatibilityShim = compatibilityShimFactory(server);
|
||||
|
||||
const serverBasePath = server.config().get('server.basePath');
|
||||
|
||||
const decryptJobHeaders = async (job) => {
|
||||
const decryptedHeaders = await crypto.decrypt(job.headers);
|
||||
return { job, decryptedHeaders };
|
||||
|
@ -44,6 +46,10 @@ function executeJobFn(server) {
|
|||
const getCustomLogo = async ({ job, filteredHeaders }) => {
|
||||
const fakeRequest = {
|
||||
headers: filteredHeaders,
|
||||
// This is used by the spaces SavedObjectClientWrapper to determine the existing space.
|
||||
// We use the basePath from the saved job, which we'll have post spaces being implemented;
|
||||
// or we use the server base path, which uses the default space
|
||||
getBasePath: () => job.basePath || serverBasePath
|
||||
};
|
||||
|
||||
const savedObjects = server.savedObjects;
|
||||
|
|
|
@ -42,7 +42,7 @@ beforeEach(() => {
|
|||
'xpack.reporting.kibanaServer.protocol': 'http',
|
||||
'xpack.reporting.kibanaServer.hostname': 'localhost',
|
||||
'xpack.reporting.kibanaServer.port': 5601,
|
||||
'server.basePath': ''
|
||||
'server.basePath': '/sbp'
|
||||
}[key];
|
||||
});
|
||||
|
||||
|
@ -106,6 +106,37 @@ test(`omits blacklisted headers`, async () => {
|
|||
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, permittedHeaders, undefined, undefined);
|
||||
});
|
||||
|
||||
test('uses basePath from job when creating saved object service', async () => {
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
|
||||
const logo = 'custom-logo';
|
||||
mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo);
|
||||
|
||||
const generatePdfObservable = generatePdfObservableFactory();
|
||||
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
|
||||
|
||||
const executeJob = executeJobFactory(mockServer);
|
||||
const jobBasePath = '/sbp/s/marketing';
|
||||
await executeJob({ objects: [], headers: encryptedHeaders, basePath: jobBasePath }, cancellationToken);
|
||||
|
||||
expect(mockServer.savedObjects.getScopedSavedObjectsClient.mock.calls[0][0].getBasePath()).toBe(jobBasePath);
|
||||
});
|
||||
|
||||
test(`uses basePath from server if job doesn't have a basePath when creating saved object service`, async () => {
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
|
||||
const logo = 'custom-logo';
|
||||
mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo);
|
||||
|
||||
const generatePdfObservable = generatePdfObservableFactory();
|
||||
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
|
||||
|
||||
const executeJob = executeJobFactory(mockServer);
|
||||
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
|
||||
|
||||
expect(mockServer.savedObjects.getScopedSavedObjectsClient.mock.calls[0][0].getBasePath()).toBe('/sbp');
|
||||
});
|
||||
|
||||
test(`gets logo from uiSettings`, async () => {
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
|
||||
|
@ -145,9 +176,9 @@ test(`adds forceNow to hash's query, if it exists`, async () => {
|
|||
const executeJob = executeJobFactory(mockServer);
|
||||
const forceNow = '2000-01-01T00:00:00.000Z';
|
||||
|
||||
await executeJob({ objects: [{ relativeUrl: 'app/kibana#/something' }], forceNow, headers: encryptedHeaders }, cancellationToken);
|
||||
await executeJob({ objects: [{ relativeUrl: '/app/kibana#/something' }], forceNow, headers: encryptedHeaders }, cancellationToken);
|
||||
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/app/kibana#/something?forceNow=2000-01-01T00%3A00%3A00.000Z'], undefined, {}, undefined, undefined);
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/sbp/app/kibana#/something?forceNow=2000-01-01T00%3A00%3A00.000Z'], undefined, {}, undefined, undefined);
|
||||
});
|
||||
|
||||
test(`appends forceNow to hash's query, if it exists`, async () => {
|
||||
|
@ -160,12 +191,12 @@ test(`appends forceNow to hash's query, if it exists`, async () => {
|
|||
const forceNow = '2000-01-01T00:00:00.000Z';
|
||||
|
||||
await executeJob({
|
||||
objects: [{ relativeUrl: 'app/kibana#/something?_g=something' }],
|
||||
objects: [{ relativeUrl: '/app/kibana#/something?_g=something' }],
|
||||
forceNow,
|
||||
headers: encryptedHeaders
|
||||
}, cancellationToken);
|
||||
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/app/kibana#/something?_g=something&forceNow=2000-01-01T00%3A00%3A00.000Z'], undefined, {}, undefined, undefined);
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/sbp/app/kibana#/something?_g=something&forceNow=2000-01-01T00%3A00%3A00.000Z'], undefined, {}, undefined, undefined);
|
||||
});
|
||||
|
||||
test(`doesn't append forceNow query to url, if it doesn't exists`, async () => {
|
||||
|
@ -176,9 +207,9 @@ test(`doesn't append forceNow query to url, if it doesn't exists`, async () => {
|
|||
|
||||
const executeJob = executeJobFactory(mockServer);
|
||||
|
||||
await executeJob({ objects: [{ relativeUrl: 'app/kibana#/something' }], headers: encryptedHeaders }, cancellationToken);
|
||||
await executeJob({ objects: [{ relativeUrl: '/app/kibana#/something' }], headers: encryptedHeaders }, cancellationToken);
|
||||
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/app/kibana#/something'], undefined, {}, undefined, undefined);
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/sbp/app/kibana#/something'], undefined, {}, undefined, undefined);
|
||||
});
|
||||
|
||||
test(`returns content_type of application/pdf`, async () => {
|
||||
|
|
|
@ -4,4 +4,5 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const ALL_RESOURCE = '*';
|
||||
export const GLOBAL_RESOURCE = '*';
|
||||
export const IGNORED_TYPES = ['space'];
|
||||
|
|
|
@ -4,10 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export default function ({ loadTestFile }) {
|
||||
describe('apis RBAC', () => {
|
||||
loadTestFile(require.resolve('./es'));
|
||||
loadTestFile(require.resolve('./privileges'));
|
||||
loadTestFile(require.resolve('./saved_objects'));
|
||||
});
|
||||
export interface IndexPrivilege {
|
||||
names: string[];
|
||||
privileges: string[];
|
||||
field_security?: {
|
||||
grant?: string[];
|
||||
};
|
||||
query?: string;
|
||||
}
|
9
x-pack/plugins/security/common/model/kibana_privilege.ts
Normal file
9
x-pack/plugins/security/common/model/kibana_privilege.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export type KibanaPrivilege = 'none' | 'read' | 'all';
|
||||
|
||||
export const KibanaAppPrivileges: KibanaPrivilege[] = ['read', 'all'];
|
29
x-pack/plugins/security/common/model/role.ts
Normal file
29
x-pack/plugins/security/common/model/role.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { IndexPrivilege } from './index_privilege';
|
||||
import { KibanaPrivilege } from './kibana_privilege';
|
||||
|
||||
export interface Role {
|
||||
name: string;
|
||||
elasticsearch: {
|
||||
cluster: string[];
|
||||
indices: IndexPrivilege[];
|
||||
run_as: string[];
|
||||
};
|
||||
kibana: {
|
||||
global: KibanaPrivilege[];
|
||||
space: {
|
||||
[spaceId: string]: KibanaPrivilege[];
|
||||
};
|
||||
};
|
||||
metadata?: {
|
||||
[anyKey: string]: any;
|
||||
};
|
||||
transient_metadata?: {
|
||||
[anyKey: string]: any;
|
||||
};
|
||||
}
|
|
@ -16,12 +16,12 @@ import { validateConfig } from './server/lib/validate_config';
|
|||
import { authenticateFactory } from './server/lib/auth_redirect';
|
||||
import { checkLicense } from './server/lib/check_license';
|
||||
import { initAuthenticator } from './server/lib/authentication/authenticator';
|
||||
import { initPrivilegesApi } from './server/routes/api/v1/privileges';
|
||||
import { SecurityAuditLogger } from './server/lib/audit_logger';
|
||||
import { AuditLogger } from '../../server/lib/audit_logger';
|
||||
import { SecureSavedObjectsClient } from './server/lib/saved_objects_client/secure_saved_objects_client';
|
||||
import { initAuthorizationService, registerPrivilegesWithCluster } from './server/lib/authorization';
|
||||
import { watchStatusAndLicenseToInitialize } from './server/lib/watch_status_and_license_to_initialize';
|
||||
import { createAuthorizationService, registerPrivilegesWithCluster } from './server/lib/authorization';
|
||||
import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize';
|
||||
import { SecureSavedObjectsClientWrapper } from './server/lib/saved_objects_client/secure_saved_objects_client_wrapper';
|
||||
import { deepFreeze } from './server/lib/deep_freeze';
|
||||
|
||||
export const security = (kibana) => new kibana.Plugin({
|
||||
id: 'security',
|
||||
|
@ -78,6 +78,7 @@ export const security = (kibana) => new kibana.Plugin({
|
|||
return {
|
||||
secureCookies: config.get('xpack.security.secureCookies'),
|
||||
sessionTimeout: config.get('xpack.security.sessionTimeout'),
|
||||
enableSpaceAwarePrivileges: config.get('xpack.spaces.enabled'),
|
||||
};
|
||||
}
|
||||
},
|
||||
|
@ -105,7 +106,8 @@ export const security = (kibana) => new kibana.Plugin({
|
|||
server.auth.strategy('session', 'login', 'required');
|
||||
|
||||
// exposes server.plugins.security.authorization
|
||||
initAuthorizationService(server);
|
||||
const authorization = createAuthorizationService(server, xpackInfoFeature);
|
||||
server.expose('authorization', deepFreeze(authorization));
|
||||
|
||||
watchStatusAndLicenseToInitialize(xpackMainPlugin, plugin, async (license) => {
|
||||
if (license.allowRbac) {
|
||||
|
@ -123,38 +125,46 @@ export const security = (kibana) => new kibana.Plugin({
|
|||
const { callWithRequest, callWithInternalUser } = adminCluster;
|
||||
const callCluster = (...args) => callWithRequest(request, ...args);
|
||||
|
||||
const callWithRequestRepository = savedObjects.getSavedObjectsRepository(callCluster);
|
||||
|
||||
if (!xpackInfoFeature.getLicenseCheckResults().allowRbac) {
|
||||
return new savedObjects.SavedObjectsClient(callWithRequestRepository);
|
||||
if (authorization.mode.useRbacForRequest(request)) {
|
||||
const internalRepository = savedObjects.getSavedObjectsRepository(callWithInternalUser);
|
||||
return new savedObjects.SavedObjectsClient(internalRepository);
|
||||
}
|
||||
|
||||
const { authorization } = server.plugins.security;
|
||||
const checkPrivileges = authorization.checkPrivilegesWithRequest(request);
|
||||
const internalRepository = savedObjects.getSavedObjectsRepository(callWithInternalUser);
|
||||
const callWithRequestRepository = savedObjects.getSavedObjectsRepository(callCluster);
|
||||
return new savedObjects.SavedObjectsClient(callWithRequestRepository);
|
||||
});
|
||||
|
||||
return new SecureSavedObjectsClient({
|
||||
internalRepository,
|
||||
callWithRequestRepository,
|
||||
errors: savedObjects.SavedObjectsClient.errors,
|
||||
checkPrivileges,
|
||||
auditLogger,
|
||||
actions: authorization.actions,
|
||||
});
|
||||
savedObjects.addScopedSavedObjectsClientWrapperFactory(Number.MIN_VALUE, ({ client, request }) => {
|
||||
if (authorization.mode.useRbacForRequest(request)) {
|
||||
const { spaces } = server.plugins;
|
||||
|
||||
return new SecureSavedObjectsClientWrapper({
|
||||
actions: authorization.actions,
|
||||
auditLogger,
|
||||
baseClient: client,
|
||||
checkPrivilegesWithRequest: authorization.checkPrivilegesWithRequest,
|
||||
errors: savedObjects.SavedObjectsClient.errors,
|
||||
request,
|
||||
savedObjectTypes: savedObjects.types,
|
||||
spaces,
|
||||
});
|
||||
}
|
||||
|
||||
return client;
|
||||
});
|
||||
|
||||
getUserProvider(server);
|
||||
|
||||
await initAuthenticator(server);
|
||||
await initAuthenticator(server, authorization.mode);
|
||||
initAuthenticateApi(server);
|
||||
initUsersApi(server);
|
||||
initPublicRolesApi(server);
|
||||
initIndicesApi(server);
|
||||
initPrivilegesApi(server);
|
||||
initLoginView(server, xpackMainPlugin);
|
||||
initLogoutView(server);
|
||||
|
||||
server.injectUiAppVars('login', () => {
|
||||
|
||||
const { showLogin, loginMessage, allowLogin, layout = 'form' } = xpackInfo.feature(plugin.id).getLicenseCheckResults() || {};
|
||||
|
||||
return {
|
||||
|
|
|
@ -7,5 +7,8 @@
|
|||
import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links';
|
||||
|
||||
export const documentationLinks = {
|
||||
dashboardViewMode: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/xpack-view-modes.html`
|
||||
dashboardViewMode: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/xpack-view-modes.html`,
|
||||
esClusterPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/x-pack/${DOC_LINK_VERSION}/security-privileges.html#security-privileges`,
|
||||
esIndicesPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/x-pack/${DOC_LINK_VERSION}/security-privileges.html#privileges-list-indices`,
|
||||
esRunAsPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/x-pack/${DOC_LINK_VERSION}/security-privileges.html#_run_as_privilege`,
|
||||
};
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from 'expect.js';
|
||||
import { isRoleEnabled } from '../role';
|
||||
|
||||
describe('role', () => {
|
||||
describe('isRoleEnabled', () => {
|
||||
it('should return false if role is explicitly not enabled', () => {
|
||||
const testRole = {
|
||||
transient_metadata: {
|
||||
enabled: false
|
||||
}
|
||||
};
|
||||
expect(isRoleEnabled(testRole)).to.be(false);
|
||||
});
|
||||
|
||||
it('should return true if role is explicitly enabled', () => {
|
||||
const testRole = {
|
||||
transient_metadata: {
|
||||
enabled: true
|
||||
}
|
||||
};
|
||||
expect(isRoleEnabled(testRole)).to.be(true);
|
||||
});
|
||||
|
||||
it('should return true if role is NOT explicitly enabled or disabled', () => {
|
||||
const testRole = {};
|
||||
expect(isRoleEnabled(testRole)).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
59
x-pack/plugins/security/public/lib/role.test.ts
Normal file
59
x-pack/plugins/security/public/lib/role.test.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { isReservedRole, isRoleEnabled } from './role';
|
||||
|
||||
describe('role', () => {
|
||||
describe('isRoleEnabled', () => {
|
||||
test('should return false if role is explicitly not enabled', () => {
|
||||
const testRole = {
|
||||
transient_metadata: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
expect(isRoleEnabled(testRole)).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true if role is explicitly enabled', () => {
|
||||
const testRole = {
|
||||
transient_metadata: {
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
expect(isRoleEnabled(testRole)).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true if role is NOT explicitly enabled or disabled', () => {
|
||||
const testRole = {};
|
||||
expect(isRoleEnabled(testRole)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isReservedRole', () => {
|
||||
test('should return false if role is explicitly not reserved', () => {
|
||||
const testRole = {
|
||||
metadata: {
|
||||
_reserved: false,
|
||||
},
|
||||
};
|
||||
expect(isReservedRole(testRole)).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true if role is explicitly reserved', () => {
|
||||
const testRole = {
|
||||
metadata: {
|
||||
_reserved: true,
|
||||
},
|
||||
};
|
||||
expect(isReservedRole(testRole)).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false if role is NOT explicitly reserved or not reserved', () => {
|
||||
const testRole = {};
|
||||
expect(isReservedRole(testRole)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { Role } from '../../common/model/role';
|
||||
|
||||
/**
|
||||
* Returns whether given role is enabled or not
|
||||
|
@ -12,6 +13,15 @@ import { get } from 'lodash';
|
|||
* @param role Object Role JSON, as returned by roles API
|
||||
* @return Boolean true if role is enabled; false otherwise
|
||||
*/
|
||||
export function isRoleEnabled(role) {
|
||||
export function isRoleEnabled(role: Partial<Role>) {
|
||||
return get(role, 'transient_metadata.enabled', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether given role is reserved or not.
|
||||
*
|
||||
* @param {role} the Role as returned by roles API
|
||||
*/
|
||||
export function isReservedRole(role: Partial<Role>) {
|
||||
return get(role, 'metadata._reserved', false);
|
||||
}
|
9
x-pack/plugins/security/public/objects/index.ts
Normal file
9
x-pack/plugins/security/public/objects/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { saveRole, deleteRole } from './lib/roles';
|
||||
|
||||
export { getFields } from './lib/get_fields';
|
15
x-pack/plugins/security/public/objects/lib/get_fields.ts
Normal file
15
x-pack/plugins/security/public/objects/lib/get_fields.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { IHttpResponse } from 'angular';
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
const apiBase = chrome.addBasePath(`/api/security/v1/fields`);
|
||||
|
||||
export async function getFields($http: any, query: string): Promise<string[]> {
|
||||
return await $http
|
||||
.get(`${apiBase}/${query}`)
|
||||
.then((response: IHttpResponse<string[]>) => response.data || []);
|
||||
}
|
19
x-pack/plugins/security/public/objects/lib/roles.ts
Normal file
19
x-pack/plugins/security/public/objects/lib/roles.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { omit } from 'lodash';
|
||||
import chrome from 'ui/chrome';
|
||||
import { Role } from '../../../common/model/role';
|
||||
|
||||
const apiBase = chrome.addBasePath(`/api/security/role`);
|
||||
|
||||
export async function saveRole($http: any, role: Role) {
|
||||
const data = omit(role, 'name', 'transient_metadata', '_unrecognized_applications');
|
||||
return await $http.put(`${apiBase}/${role.name}`, data);
|
||||
}
|
||||
|
||||
export async function deleteRole($http: any, name: string) {
|
||||
return await $http.delete(`${apiBase}/${name}`);
|
||||
}
|
|
@ -10,5 +10,9 @@ import { uiModules } from 'ui/modules';
|
|||
const module = uiModules.get('security', ['ngResource']);
|
||||
module.service('ApplicationPrivileges', ($resource, chrome) => {
|
||||
const baseUrl = chrome.addBasePath('/api/security/v1/privileges');
|
||||
return $resource(baseUrl);
|
||||
return $resource(baseUrl, null, {
|
||||
query: {
|
||||
isArray: false,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
42
x-pack/plugins/security/public/services/role_privileges.js
Normal file
42
x-pack/plugins/security/public/services/role_privileges.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
const clusterPrivileges = [
|
||||
'all',
|
||||
'monitor',
|
||||
'manage',
|
||||
'manage_security',
|
||||
'manage_index_templates',
|
||||
'manage_pipeline',
|
||||
'manage_ingest_pipelines',
|
||||
'transport_client',
|
||||
'manage_ml',
|
||||
'monitor_ml',
|
||||
'manage_watcher',
|
||||
'monitor_watcher',
|
||||
];
|
||||
const indexPrivileges = [
|
||||
'all',
|
||||
'manage',
|
||||
'monitor',
|
||||
'read',
|
||||
'index',
|
||||
'create',
|
||||
'delete',
|
||||
'write',
|
||||
'delete_index',
|
||||
'create_index',
|
||||
'view_index_metadata',
|
||||
'read_cross_cluster',
|
||||
];
|
||||
|
||||
export function getClusterPrivileges() {
|
||||
return [...clusterPrivileges];
|
||||
}
|
||||
|
||||
export function getIndexPrivileges() {
|
||||
return [...indexPrivileges];
|
||||
}
|
|
@ -1,196 +0,0 @@
|
|||
<kbn-management-app section="security" omit-breadcrumb-pages="['edit']">
|
||||
<!-- This content gets injected below the localNav. -->
|
||||
<div class="kuiViewContent kuiViewContent--constrainedWidth kuiViewContentItem">
|
||||
|
||||
<!-- Subheader -->
|
||||
<div class="kuiBar kuiVerticalRhythm">
|
||||
|
||||
<div class="kuiBarSection">
|
||||
<!-- Title -->
|
||||
<h1 class="kuiTitle">
|
||||
<span ng-if="editRole.isNewRole">
|
||||
New Role
|
||||
</span>
|
||||
<span ng-if="!editRole.isNewRole">
|
||||
“{{ role.name }}” Role
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="kuiBarSection">
|
||||
<!-- Delete button -->
|
||||
<button
|
||||
ng-if="!editRole.isNewRole && !role.metadata._reserved"
|
||||
class="kuiButton kuiButton--danger kuiButton--iconText"
|
||||
ng-click="deleteRole(role)"
|
||||
tooltip="Delete Role"
|
||||
>
|
||||
<span class="kuiButton__inner">
|
||||
<span class="kuiButton__icon kuiIcon fa-trash"></span>
|
||||
<span>Delete role</span>
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
ng-if="role.metadata._reserved"
|
||||
class="kuiBadge kuiBadge--default"
|
||||
tooltip="Reserved roles are built-in and cannot be removed or modified."
|
||||
>
|
||||
<span class="kuiIcon fa-lock"></span>
|
||||
Reserved
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuiBar kuiVerticalRhythm" ng-if="otherApplications.length > 0">
|
||||
<div class="kuiInfoPanel kuiInfoPanel--warning">
|
||||
<div class="kuiInfoPanelHeader">
|
||||
<span class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--warning fa-warning"></span>
|
||||
<span class="kuiInfoPanelHeader__title">
|
||||
This role contains application privileges for the {{ otherApplications.join(', ') }} application(s) that can't be edited.
|
||||
If they are for other instances of Kibana, you must manage those privileges on that Kibana instance.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<form name="form" novalidate class="kuiVerticalRhythm">
|
||||
<!-- Name -->
|
||||
<div class="kuiFormSection">
|
||||
<label for="name" class="kuiFormLabel">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="fullWidth"
|
||||
ng-class="::editRole.isNewRole ? 'kuiTextInput' : 'kuiStaticInput'"
|
||||
ng-disabled="!editRole.isNewRole"
|
||||
id="name"
|
||||
name="name"
|
||||
ng-model="role.name"
|
||||
required
|
||||
pattern="[a-zA-Z_][a-zA-Z0-9_@\-\$\.]*"
|
||||
maxlength="1024"
|
||||
data-test-subj="roleFormNameInput"
|
||||
/>
|
||||
|
||||
<!-- Errors -->
|
||||
<div
|
||||
class="kuiInputNote kuiInputNote--danger"
|
||||
ng-show="form.name.$error.pattern"
|
||||
>
|
||||
Name must begin with a letter or underscore and contain only letters, underscores, and numbers.
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="kuiInputNote kuiInputNote--danger"
|
||||
ng-show="form.name.$touched && form.name.$error.required"
|
||||
>
|
||||
Name is required.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuiVerticalRhythm">
|
||||
<!-- Cluster privileges -->
|
||||
<div class="kuiFormSection">
|
||||
<label class="kuiFormLabel">
|
||||
Cluster Privileges
|
||||
</label>
|
||||
<div ng-repeat="privilege in privileges.cluster">
|
||||
<label>
|
||||
<input
|
||||
class="kuiCheckBox"
|
||||
type="checkbox"
|
||||
ng-checked="includes(role.elasticsearch.cluster, privilege)"
|
||||
ng-click="toggle(role.elasticsearch.cluster, privilege)"
|
||||
ng-disabled="role.metadata._reserved || !isRoleEnabled(role)"
|
||||
data-test-subj="clusterPrivileges-{{privilege}}"
|
||||
/>
|
||||
<span class="kuiOptionLabel">{{privilege}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kibana custom privileges -->
|
||||
<div class="kuiFormSection">
|
||||
<label class="kuiFormLabel">
|
||||
Kibana Privileges
|
||||
</label>
|
||||
|
||||
<div ng-repeat="(key, value) in kibanaPrivilegesViewModel">
|
||||
<label>
|
||||
<input
|
||||
class="kuiCheckBox"
|
||||
type="checkbox"
|
||||
ng-model="kibanaPrivilegesViewModel[key]"
|
||||
ng-disabled="role.metadata._reserved || !isRoleEnabled(role)"
|
||||
data-test-subj="kibanaPrivileges-{{key}}"
|
||||
/>
|
||||
<span class="kuiOptionLabel">{{key}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Run-as privileges -->
|
||||
<div class="kuiFormSection">
|
||||
<label class="kuiFormLabel">
|
||||
Run As Privileges
|
||||
</label>
|
||||
<ui-select
|
||||
multiple
|
||||
ng-model="role.elasticsearch.run_as"
|
||||
ng-disabled="role.metadata._reserved || !isRoleEnabled(role)"
|
||||
>
|
||||
<ui-select-match placeholder="Add a user...">
|
||||
{{$item}}
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="user as user in union([$select.search], users) | filter:$select.search">
|
||||
<div ng-bind-html="user"></div>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</div>
|
||||
|
||||
<!-- Index privileges -->
|
||||
<div class="kuiFormSection">
|
||||
<kbn-index-privileges-form
|
||||
is-new-role="editRole.isNewRole"
|
||||
indices="role.elasticsearch.indices"
|
||||
index-patterns="indexPatterns"
|
||||
privileges="privileges"
|
||||
field-options="editRole.fieldOptions"
|
||||
is-reserved="role.metadata._reserved"
|
||||
is-enabled="isRoleEnabled(role)"
|
||||
allow-document-level-security="allowDocumentLevelSecurity"
|
||||
allow-field-level-security="allowFieldLevelSecurity"
|
||||
add-index="addIndex(indices)"
|
||||
remove-index="toggle(indices, index)"
|
||||
></kbn-index-privileges-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuiVerticalRhythm">
|
||||
<!-- Form actions -->
|
||||
<div class="kuiFormSection kuiFormFooter">
|
||||
<button
|
||||
class="kuiButton kuiButton--primary"
|
||||
ng-click="saveRole(role)"
|
||||
ng-if="!role.metadata._reserved && isRoleEnabled(role)"
|
||||
ng-disabled="form.$invalid || !areIndicesValid(role.elasticsearch.indices)"
|
||||
data-test-subj="roleFormSaveButton"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
|
||||
<a
|
||||
class="kuiButton kuiButton--basic"
|
||||
ng-if="!role.metadata._reserved"
|
||||
ng-href="{{rolesHref}}"
|
||||
data-test-subj="roleFormCancelButton"
|
||||
>
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</kbn-management-app>
|
|
@ -1,200 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import routes from 'ui/routes';
|
||||
import { fatalError, toastNotifications } from 'ui/notify';
|
||||
import { toggle } from 'plugins/security/lib/util';
|
||||
import { isRoleEnabled } from 'plugins/security/lib/role';
|
||||
import template from 'plugins/security/views/management/edit_role.html';
|
||||
import 'angular-ui-select';
|
||||
import 'plugins/security/services/application_privilege';
|
||||
import 'plugins/security/services/shield_user';
|
||||
import 'plugins/security/services/shield_role';
|
||||
import 'plugins/security/services/shield_privileges';
|
||||
import 'plugins/security/services/shield_indices';
|
||||
|
||||
import { IndexPatternsProvider } from 'ui/index_patterns/index_patterns';
|
||||
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
|
||||
import { checkLicenseError } from 'plugins/security/lib/check_license_error';
|
||||
import { GateKeeperProvider } from 'plugins/xpack_main/services/gate_keeper';
|
||||
import { EDIT_ROLES_PATH, ROLES_PATH } from './management_urls';
|
||||
|
||||
const getKibanaPrivilegesViewModel = (applicationPrivileges, roleKibanaPrivileges) => {
|
||||
const viewModel = applicationPrivileges.reduce((acc, applicationPrivilege) => {
|
||||
acc[applicationPrivilege.name] = false;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (!roleKibanaPrivileges || roleKibanaPrivileges.length === 0) {
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
const assignedPrivileges = _.uniq(_.flatten(_.pluck(roleKibanaPrivileges, 'privileges')));
|
||||
assignedPrivileges.forEach(assignedPrivilege => {
|
||||
// we don't want to display privileges that aren't in our expected list of privileges
|
||||
if (assignedPrivilege in viewModel) {
|
||||
viewModel[assignedPrivilege] = true;
|
||||
}
|
||||
});
|
||||
|
||||
return viewModel;
|
||||
};
|
||||
|
||||
const getKibanaPrivileges = (kibanaPrivilegesViewModel) => {
|
||||
const selectedPrivileges = Object.keys(kibanaPrivilegesViewModel).filter(key => kibanaPrivilegesViewModel[key]);
|
||||
|
||||
// if we have any selected privileges, add a single application entry
|
||||
if (selectedPrivileges.length > 0) {
|
||||
return [
|
||||
{
|
||||
privileges: selectedPrivileges
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
routes.when(`${EDIT_ROLES_PATH}/:name?`, {
|
||||
template,
|
||||
resolve: {
|
||||
tribeRedirect(Private) {
|
||||
const gateKeeper = Private(GateKeeperProvider);
|
||||
gateKeeper.redirectAndNotifyIfTribe();
|
||||
},
|
||||
|
||||
role($route, ShieldRole, kbnUrl, Promise) {
|
||||
const name = $route.current.params.name;
|
||||
if (name != null) {
|
||||
return ShieldRole.get({ name }).$promise
|
||||
.catch((response) => {
|
||||
|
||||
if (response.status !== 404) {
|
||||
return fatalError(response);
|
||||
}
|
||||
|
||||
toastNotifications.addDanger(`No "${name}" role found.`);
|
||||
kbnUrl.redirect(ROLES_PATH);
|
||||
return Promise.halt();
|
||||
});
|
||||
}
|
||||
return new ShieldRole({
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [],
|
||||
_unrecognized_applications: []
|
||||
});
|
||||
},
|
||||
applicationPrivileges(ApplicationPrivileges, kbnUrl, Promise, Private) {
|
||||
return ApplicationPrivileges.query().$promise
|
||||
.catch(checkLicenseError(kbnUrl, Promise, Private));
|
||||
},
|
||||
users(ShieldUser, kbnUrl, Promise, Private) {
|
||||
// $promise is used here because the result is an ngResource, not a promise itself
|
||||
return ShieldUser.query().$promise
|
||||
.then(users => _.map(users, 'username'))
|
||||
.catch(checkLicenseError(kbnUrl, Promise, Private));
|
||||
},
|
||||
indexPatterns(Private) {
|
||||
const indexPatterns = Private(IndexPatternsProvider);
|
||||
return indexPatterns.getTitles();
|
||||
}
|
||||
},
|
||||
controllerAs: 'editRole',
|
||||
controller($injector, $scope) {
|
||||
const $route = $injector.get('$route');
|
||||
const kbnUrl = $injector.get('kbnUrl');
|
||||
const shieldPrivileges = $injector.get('shieldPrivileges');
|
||||
const Private = $injector.get('Private');
|
||||
const confirmModal = $injector.get('confirmModal');
|
||||
const shieldIndices = $injector.get('shieldIndices');
|
||||
|
||||
$scope.role = $route.current.locals.role;
|
||||
$scope.users = $route.current.locals.users;
|
||||
$scope.indexPatterns = $route.current.locals.indexPatterns;
|
||||
$scope.privileges = shieldPrivileges;
|
||||
|
||||
const applicationPrivileges = $route.current.locals.applicationPrivileges;
|
||||
const role = $route.current.locals.role;
|
||||
$scope.kibanaPrivilegesViewModel = getKibanaPrivilegesViewModel(applicationPrivileges, role.kibana);
|
||||
$scope.otherApplications = role._unrecognized_applications;
|
||||
|
||||
$scope.rolesHref = `#${ROLES_PATH}`;
|
||||
|
||||
this.isNewRole = $route.current.params.name == null;
|
||||
this.fieldOptions = {};
|
||||
|
||||
$scope.deleteRole = (role) => {
|
||||
const doDelete = () => {
|
||||
role.$delete()
|
||||
.then(() => toastNotifications.addSuccess('Deleted role'))
|
||||
.then($scope.goToRoleList)
|
||||
.catch(error => toastNotifications.addDanger(_.get(error, 'data.message')));
|
||||
};
|
||||
const confirmModalOptions = {
|
||||
confirmButtonText: 'Delete role',
|
||||
onConfirm: doDelete
|
||||
};
|
||||
confirmModal('Are you sure you want to delete this role? This action is irreversible!', confirmModalOptions);
|
||||
};
|
||||
|
||||
$scope.saveRole = (role) => {
|
||||
role.elasticsearch.indices = role.elasticsearch.indices.filter((index) => index.names.length);
|
||||
role.elasticsearch.indices.forEach((index) => index.query || delete index.query);
|
||||
|
||||
role.kibana = getKibanaPrivileges($scope.kibanaPrivilegesViewModel);
|
||||
|
||||
return role.$save()
|
||||
.then(() => toastNotifications.addSuccess('Updated role'))
|
||||
.then($scope.goToRoleList)
|
||||
.catch(error => toastNotifications.addDanger(_.get(error, 'data.message')));
|
||||
};
|
||||
|
||||
$scope.goToRoleList = () => {
|
||||
kbnUrl.redirect(ROLES_PATH);
|
||||
};
|
||||
|
||||
$scope.addIndex = indices => {
|
||||
indices.push({ names: [], privileges: [], field_security: { grant: ['*'] } });
|
||||
};
|
||||
|
||||
$scope.areIndicesValid = (indices) => {
|
||||
return indices
|
||||
.filter((index) => index.names.length)
|
||||
.find((index) => index.privileges.length === 0) == null;
|
||||
};
|
||||
|
||||
$scope.fetchFieldOptions = (index) => {
|
||||
const indices = index.names.join(',');
|
||||
const fieldOptions = this.fieldOptions;
|
||||
if (indices && fieldOptions[indices] == null) {
|
||||
shieldIndices.getFields(indices)
|
||||
.then((fields) => fieldOptions[indices] = fields)
|
||||
.catch(() => fieldOptions[indices] = []);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.isRoleEnabled = isRoleEnabled;
|
||||
|
||||
const xpackInfo = Private(XPackInfoProvider);
|
||||
$scope.allowDocumentLevelSecurity = xpackInfo.get('features.security.allowRoleDocumentLevelSecurity');
|
||||
$scope.allowFieldLevelSecurity = xpackInfo.get('features.security.allowRoleFieldLevelSecurity');
|
||||
|
||||
$scope.$watch('role.elasticsearch.indices', (indices) => {
|
||||
if (!indices.length) $scope.addIndex(indices);
|
||||
else indices.forEach($scope.fetchFieldOptions);
|
||||
}, true);
|
||||
|
||||
$scope.toggle = toggle;
|
||||
$scope.includes = _.includes;
|
||||
|
||||
$scope.union = _.flow(_.union, _.compact);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,58 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`it renders without blowing up 1`] = `
|
||||
<EuiPanel
|
||||
grow={true}
|
||||
hasShadow={false}
|
||||
paddingSize="m"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="baseline"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
justifyContent="flexStart"
|
||||
responsive={false}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiTitle
|
||||
size="m"
|
||||
>
|
||||
<h2>
|
||||
<EuiIcon
|
||||
className="collapsiblePanel__logo"
|
||||
size="xl"
|
||||
type="logoElasticsearch"
|
||||
/>
|
||||
|
||||
Elasticsearch
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
hide
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<React.Fragment>
|
||||
<EuiSpacer
|
||||
size="l"
|
||||
/>
|
||||
<p>
|
||||
child
|
||||
</p>
|
||||
</React.Fragment>
|
||||
</EuiPanel>
|
||||
`;
|
|
@ -0,0 +1,4 @@
|
|||
.collapsiblePanel__logo {
|
||||
margin-right: 8px;
|
||||
vertical-align: text-bottom;
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { CollapsiblePanel } from './collapsible_panel';
|
||||
|
||||
test('it renders without blowing up', () => {
|
||||
const wrapper = shallow(
|
||||
<CollapsiblePanel iconType="logoElasticsearch" title="Elasticsearch">
|
||||
<p>child</p>
|
||||
</CollapsiblePanel>
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it renders children by default', () => {
|
||||
const wrapper = mount(
|
||||
<CollapsiblePanel iconType="logoElasticsearch" title="Elasticsearch">
|
||||
<p className="child">child 1</p>
|
||||
<p className="child">child 2</p>
|
||||
</CollapsiblePanel>
|
||||
);
|
||||
|
||||
expect(wrapper.find(CollapsiblePanel)).toHaveLength(1);
|
||||
expect(wrapper.find('.child')).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('it hides children when the "hide" link is clicked', () => {
|
||||
const wrapper = mount(
|
||||
<CollapsiblePanel iconType="logoElasticsearch" title="Elasticsearch">
|
||||
<p className="child">child 1</p>
|
||||
<p className="child">child 2</p>
|
||||
</CollapsiblePanel>
|
||||
);
|
||||
|
||||
expect(wrapper.find(CollapsiblePanel)).toHaveLength(1);
|
||||
expect(wrapper.find('.child')).toHaveLength(2);
|
||||
|
||||
wrapper.find(EuiLink).simulate('click');
|
||||
|
||||
expect(wrapper.find('.child')).toHaveLength(0);
|
||||
});
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import './collapsible_panel.less';
|
||||
|
||||
interface Props {
|
||||
iconType: string | any;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
collapsed: boolean;
|
||||
}
|
||||
|
||||
export class CollapsiblePanel extends Component<Props, State> {
|
||||
public state = {
|
||||
collapsed: false,
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<EuiPanel>
|
||||
{this.getTitle()}
|
||||
{this.getForm()}
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
public getTitle = () => {
|
||||
return (
|
||||
// @ts-ignore
|
||||
<EuiFlexGroup alignItems={'baseline'} gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
<EuiIcon
|
||||
type={this.props.iconType}
|
||||
size={'xl'}
|
||||
className={'collapsiblePanel__logo'}
|
||||
/>{' '}
|
||||
{this.props.title}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink onClick={this.toggleCollapsed}>{this.state.collapsed ? 'show' : 'hide'}</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
public getForm = () => {
|
||||
if (this.state.collapsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer />
|
||||
{this.props.children}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
public toggleCollapsed = () => {
|
||||
this.setState({
|
||||
collapsed: !this.state.collapsed,
|
||||
});
|
||||
};
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue