diff --git a/docs/getting-started/tutorial-dashboard.asciidoc b/docs/getting-started/tutorial-dashboard.asciidoc index aab93eb51ca232..87a1e38efcfe0c 100644 --- a/docs/getting-started/tutorial-dashboard.asciidoc +++ b/docs/getting-started/tutorial-dashboard.asciidoc @@ -2,11 +2,12 @@ === Add visualizations to a dashboard A dashboard is a collection of visualizations that you can arrange and share. -You'll build a dashboard that contains the visualizations you saved during +You'll build a dashboard that contains the visualizations and map that you saved during this tutorial. . Open *Dashboard*. . On the Dashboard overview page, click *Create new dashboard*. +. Set the time filter to May 18, 2015 to May 20, 2015. . Click *Add* in the menu bar. . Add *Bar Example*, *Map Example*, *Markdown Example*, and *Pie Example*. + @@ -26,12 +27,12 @@ is on the lower right. ==== Inspect the data -Seeing visualizations of your data is great, +Seeing visualizations of your data is great, but sometimes you need to look at the actual data to understand what's really going on. You can inspect the data behind any visualization and view the {es} query used to retrieve it. -. In the dashboard, hover the pointer over the pie chart, and then click the icon in the upper right. +. In the dashboard, hover the pointer over the pie chart, and then click the icon in the upper right. . From the *Options* menu, select *Inspect*. + [role="screenshot"] diff --git a/docs/getting-started/tutorial-visualizing.asciidoc b/docs/getting-started/tutorial-visualizing.asciidoc index 5e61475cf2839d..a16343aa4850a2 100644 --- a/docs/getting-started/tutorial-visualizing.asciidoc +++ b/docs/getting-started/tutorial-visualizing.asciidoc @@ -3,11 +3,11 @@ In the Visualize application, you can shape your data using a variety of charts, tables, and maps, and more. In this tutorial, you'll create four -visualizations: +visualizations: * <> * <> -* <> +* <> * <> [float] @@ -25,7 +25,7 @@ types in Kibana. image::images/tutorial-visualize-wizard-step-1.png[] . Click *Pie*. -. In *Choose a source*, select the `ba*` index pattern. +. In *Choose a source*, select the `ba*` index pattern. + Initially, the pie contains a single "slice." That's because the default search matched all documents. @@ -76,7 +76,7 @@ in a ring around the balance ranges. [role="screenshot"] image::images/tutorial-visualize-pie-3.png[] -. To save this chart so you can use it later, click *Save* in +. To save this chart so you can use it later, click *Save* in the top menu bar and enter `Pie Example`. [float] @@ -123,14 +123,36 @@ you did at the beginning of the tutorial, when you marked the `play_name` field as `not analyzed`. [float] -[[tutorial-visualize-map]] -=== Coordinate map +[[tutorial-visualize-markdown]] +=== Markdown -Using a coordinate map, you can visualize geographic information in the log file sample data. +Create a Markdown widget to add formatted text to your dashboard. + +. Create a *Markdown* visualization. +. Copy the following text into the text box. ++ +[source,markdown] +# This is a tutorial dashboard! +The Markdown widget uses **markdown** syntax. +> Blockquotes in Markdown use the > character. -. Create a *Coordinate map* and set the search source to `logstash*`. +. Click *Apply changes* image:images/apply-changes-button.png[]. + -You haven't defined any buckets yet, so the visualization is a map of the world. +The Markdown renders in the preview pane. ++ +[role="screenshot"] +image::images/tutorial-visualize-md-2.png[] + +. *Save* this visualization with the name `Markdown Example`. + +[float] +[[tutorial-visualize-map]] +=== Map + +Using <>, you can visualize geographic information in the log file sample data. + +. Click *Maps* in the New Visualization +menu to create a Map. . Set the time. .. In the time filter, click *Show dates*. @@ -138,14 +160,19 @@ You haven't defined any buckets yet, so the visualization is a map of the world. .. Set the *Start date* to May 18, 2015. .. In the time filter, click *now*, then *Absolute*. .. Set the *End date* to May 20, 2015. +.. Click *Update* . Map the geo coordinates from the log files. -.. In the *Buckets* pane, click *Add > Geo coordinates*. -.. Set *Aggregation* to *Geohash*. -.. Set *Field* to *geo.coordinates*. +.. Click *Add layer*. +.. Click the *Grid aggregation* data source. +.. Set *Index pattern* to *logstash*. +.. Click the *Add layer* button. -. Click *Apply changes* image:images/apply-changes-button.png[]. +. Set the layer style. +.. For *Fill color*, select the yellow to red color ramp. +.. For *Border color*, select white. +.. Click *Save & close*. + The map now looks like this: + @@ -155,26 +182,3 @@ image::images/tutorial-visualize-map-2.png[] . Navigate the map by clicking and dragging. Use the controls on the left to zoom the map and set filters. . *Save* this map with the name `Map Example`. - -[float] -[[tutorial-visualize-markdown]] -=== Markdown - -The final visualization is a Markdown widget that renders formatted text. - -. Create a *Markdown* visualization. -. Copy the following text into the text box. -+ -[source,markdown] -# This is a tutorial dashboard! -The Markdown widget uses **markdown** syntax. -> Blockquotes in Markdown use the > character. - -. Click *Apply changes* image:images/apply-changes-button.png[]. -+ -The Markdown renders in the preview pane. -+ -[role="screenshot"] -image::images/tutorial-visualize-md-2.png[] - -. *Save* this visualization with the name `Markdown Example`. diff --git a/docs/images/tutorial-visualize-map-2.png b/docs/images/tutorial-visualize-map-2.png index db9f0d56bc963d..f4d1d0e47fe6ad 100644 Binary files a/docs/images/tutorial-visualize-map-2.png and b/docs/images/tutorial-visualize-map-2.png differ diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc index 88ad6a26d36970..3ff44b97de6356 100644 --- a/docs/maps/maps-getting-started.asciidoc +++ b/docs/maps/maps-getting-started.asciidoc @@ -84,7 +84,7 @@ the {es} index `kibana_sample_data_logs` on the shared key iso2 = geo.src. . Set *Right source* to *kibana_sample_data_logs*. . Set *Right field* to *geo.src*. -===== Set the vector style +===== Set the layer style All of the world countries are still a single color because the layer is using <>. To shade the world countries based on which country is sending the most requests, you'll need to use <>. @@ -161,9 +161,9 @@ image::maps/images/grid_metrics_both.png[] . Select *Sum* in the aggregation select. . Select *bytes* in the field select. -===== Set the vector style +===== Set the layer style -. In *Vector style*, change *Symbol size*: +. In *Layer style*, change *Symbol size*: .. Set *Min size* to 1. .. Set *Max size* to 25. .. In the field select, select *sum of bytes*. diff --git a/package.json b/package.json index a8e60e9749f724..fa4aa5d40c9d40 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "dependencies": { "@babel/core": "^7.5.5", "@babel/register": "^7.5.5", - "@elastic/charts": "^13.5.12", + "@elastic/charts": "^14.0.0", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "1.0.5", "@elastic/eui": "14.8.0", diff --git a/packages/kbn-es/src/custom_snapshots.js b/packages/kbn-es/src/custom_snapshots.js index 0af89b8e9b68c1..be6bbeca538ffa 100644 --- a/packages/kbn-es/src/custom_snapshots.js +++ b/packages/kbn-es/src/custom_snapshots.js @@ -26,7 +26,8 @@ function isVersionFlag(a) { function getCustomSnapshotUrl() { // force use of manually created snapshots until live ones are available if (!process.env.KBN_ES_SNAPSHOT_URL && !process.argv.some(isVersionFlag)) { - return 'https://storage.googleapis.com/kibana-ci-tmp-artifacts/{name}-{version}-{os}-x86_64.{ext}'; + // return 'https://storage.googleapis.com/kibana-ci-tmp-artifacts/{name}-{version}-{os}-x86_64.{ext}'; + return; } if (process.env.KBN_ES_SNAPSHOT_URL && process.env.KBN_ES_SNAPSHOT_URL !== 'false') { diff --git a/packages/kbn-spec-to-console/README.md b/packages/kbn-spec-to-console/README.md index fc068b7ba65d6f..6729f03b3d4db8 100644 --- a/packages/kbn-spec-to-console/README.md +++ b/packages/kbn-spec-to-console/README.md @@ -23,14 +23,10 @@ At the root of the Kibana repository, run the following commands: ```sh # OSS -yarn spec_to_console \ - -g "/rest-api-spec/src/main/resources/rest-api-spec/api/*" \ - -d "src/legacy/core_plugins/console/api_server/spec/generated" +yarn spec_to_console -g "/rest-api-spec/src/main/resources/rest-api-spec/api/*" -d "src/legacy/core_plugins/console/server/api_server/spec/generated" # X-pack -yarn spec_to_console \ - -g "/x-pack/plugin/src/test/resources/rest-api-spec/api/*" \ - -d "x-pack/plugins/console_extensions/spec/generated" +yarn spec_to_console -g "/x-pack/plugin/src/test/resources/rest-api-spec/api/*" -d "x-pack/legacy/plugins/console_extensions/spec/generated" ``` ### Information used in Console that is not available in the REST spec diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 87389d2c10f033..a5532faec19ede 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -129,6 +129,7 @@ export class ChromeService {
void; + isCloudEnabled: boolean; } interface State { @@ -296,6 +297,7 @@ class HeaderUI extends Component { kibanaVersion, onIsLockedUpdate, legacyMode, + isCloudEnabled, } = this.props; const { appTitle, @@ -394,7 +396,9 @@ class HeaderUI extends Component { - + diff --git a/src/core/public/chrome/ui/header/header_help_menu.tsx b/src/core/public/chrome/ui/header/header_help_menu.tsx index 688d76c9d75d9c..c04fbaa07ba712 100644 --- a/src/core/public/chrome/ui/header/header_help_menu.tsx +++ b/src/core/public/chrome/ui/header/header_help_menu.tsx @@ -17,30 +17,29 @@ * under the License. */ +import * as Rx from 'rxjs'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import * as Rx from 'rxjs'; - +import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { - // TODO: add type annotations - // @ts-ignore - EuiButton, - // @ts-ignore + EuiButtonEmpty, EuiFlexGroup, - // @ts-ignore EuiFlexItem, - // @ts-ignore EuiHeaderSectionItemButton, EuiIcon, EuiPopover, EuiPopoverTitle, EuiSpacer, - EuiText, } from '@elastic/eui'; -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { HeaderExtension } from './header_extension'; import { ChromeHelpExtension } from '../../chrome_service'; +import { + ELASTIC_SUPPORT_LINK, + GITHUB_CREATE_ISSUE_LINK, + KIBANA_ASK_ELASTIC_LINK, + KIBANA_FEEDBACK_LINK, +} from '../../constants'; interface Props { helpExtension$: Rx.Observable; @@ -48,6 +47,7 @@ interface Props { kibanaVersion: string; useDefaultContent?: boolean; kibanaDocLink: string; + isCloudEnabled: boolean; } interface State { @@ -90,23 +90,50 @@ class HeaderHelpMenuUI extends Component { const defaultContent = useDefaultContent ? ( - -

- -

-
- - - - + + + + + + + + + + + + + + + + + + + - +
) : null; diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index fcc232345a802d..0ac2f59525c326 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -39,6 +39,7 @@ interface RequestFixtureOptions { path?: string; method?: RouteMethod; socket?: Socket; + routeTags?: string[]; } function createKibanaRequestMock({ @@ -49,6 +50,7 @@ function createKibanaRequestMock({ query = {}, method = 'get', socket = new Socket(), + routeTags, }: RequestFixtureOptions = {}) { const queryString = querystring.stringify(query); return KibanaRequest.from( @@ -61,10 +63,11 @@ function createKibanaRequestMock({ method, url: { path, + pathname: path, query: queryString, search: queryString ? `?${queryString}` : queryString, }, - route: { settings: {} }, + route: { settings: { tags: routeTags } }, raw: { req: { socket }, }, diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index 15f46711fc94b5..cf0769fced460e 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -34,6 +34,7 @@ export interface SavedObjectsLegacyService { addScopedSavedObjectsClientWrapperFactory: SavedObjectsClientProvider< Request >['addClientWrapperFactory']; + setScopedSavedObjectsClientFactory: SavedObjectsClientProvider['setClientFactory']; getScopedSavedObjectsClient: SavedObjectsClientProvider['getClient']; SavedObjectsClient: typeof SavedObjectsClient; types: string[]; diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 6525590ee96c5b..79a3e573ab98c9 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -49,7 +49,6 @@ describe('SavedObjectsRepository', () => { hits: [ { _index: '.kibana', - _type: '_doc', _id: 'index-pattern:logstash-*', _score: 1, ...mockVersionProps, @@ -65,7 +64,6 @@ describe('SavedObjectsRepository', () => { }, { _index: '.kibana', - _type: '_doc', _id: 'config:6.0.0-alpha1', _score: 1, ...mockVersionProps, @@ -80,7 +78,6 @@ describe('SavedObjectsRepository', () => { }, { _index: '.kibana', - _type: '_doc', _id: 'index-pattern:stocks-*', _score: 1, ...mockVersionProps, @@ -96,7 +93,6 @@ describe('SavedObjectsRepository', () => { }, { _index: '.kibana', - _type: '_doc', _id: 'globaltype:something', _score: 1, ...mockVersionProps, @@ -118,7 +114,6 @@ describe('SavedObjectsRepository', () => { hits: [ { _index: '.kibana', - _type: '_doc', _id: 'foo-namespace:index-pattern:logstash-*', _score: 1, ...mockVersionProps, @@ -135,7 +130,6 @@ describe('SavedObjectsRepository', () => { }, { _index: '.kibana', - _type: '_doc', _id: 'foo-namespace:config:6.0.0-alpha1', _score: 1, ...mockVersionProps, @@ -151,7 +145,6 @@ describe('SavedObjectsRepository', () => { }, { _index: '.kibana', - _type: '_doc', _id: 'foo-namespace:index-pattern:stocks-*', _score: 1, ...mockVersionProps, @@ -168,7 +161,6 @@ describe('SavedObjectsRepository', () => { }, { _index: '.kibana', - _type: '_doc', _id: 'globaltype:something', _score: 1, ...mockVersionProps, @@ -290,7 +282,6 @@ describe('SavedObjectsRepository', () => { describe('#create', () => { beforeEach(() => { callAdminCluster.mockImplementation((method, params) => ({ - _type: '_doc', _id: params.id, ...mockVersionProps, })); @@ -863,7 +854,6 @@ describe('SavedObjectsRepository', () => { items: [ { create: { - _type: '_doc', _id: 'config:one', error: { reason: 'type[config] missing', @@ -872,7 +862,6 @@ describe('SavedObjectsRepository', () => { }, { create: { - _type: '_doc', _id: 'index-pattern:two', ...mockVersionProps, }, @@ -910,14 +899,12 @@ describe('SavedObjectsRepository', () => { items: [ { create: { - _type: '_doc', _id: 'config:one', ...mockVersionProps, }, }, { create: { - _type: '_doc', _id: 'index-pattern:two', ...mockVersionProps, }, @@ -962,7 +949,6 @@ describe('SavedObjectsRepository', () => { items: [ { create: { - _type: '_doc', _id: 'foo-namespace:config:one', _index: '.kibana-test', _primary_term: 1, @@ -971,7 +957,6 @@ describe('SavedObjectsRepository', () => { }, { create: { - _type: '_doc', _id: 'foo-namespace:index-pattern:two', _primary_term: 1, _seq_no: 2, @@ -1021,14 +1006,12 @@ describe('SavedObjectsRepository', () => { items: [ { create: { - _type: '_doc', _id: 'config:one', ...mockVersionProps, }, }, { create: { - _type: '_doc', _id: 'index-pattern:two', ...mockVersionProps, }, @@ -1092,7 +1075,7 @@ describe('SavedObjectsRepository', () => { expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); - it('should return objects in the same order regardless of type', () => {}); + it('should return objects in the same order regardless of type', () => { }); }); describe('#delete', () => { @@ -1496,7 +1479,6 @@ describe('SavedObjectsRepository', () => { describe('#get', () => { const noNamespaceResult = { _id: 'index-pattern:logstash-*', - _type: '_doc', ...mockVersionProps, _source: { type: 'index-pattern', @@ -1509,7 +1491,6 @@ describe('SavedObjectsRepository', () => { }; const namespacedResult = { _id: 'foo-namespace:index-pattern:logstash-*', - _type: '_doc', ...mockVersionProps, _source: { namespace: 'foo-namespace', @@ -1699,14 +1680,12 @@ describe('SavedObjectsRepository', () => { callAdminCluster.mockResolvedValue({ docs: [ { - _type: '_doc', _id: 'config:good', found: true, ...mockVersionProps, _source: { ...mockTimestampFields, config: { title: 'Test' } }, }, { - _type: '_doc', _id: 'config:bad', found: false, }, @@ -1728,14 +1707,12 @@ describe('SavedObjectsRepository', () => { callAdminCluster.mockResolvedValue({ docs: [ { - _type: '_doc', _id: 'config:good', found: true, ...mockVersionProps, _source: { ...mockTimestampFields, config: { title: 'Test' } }, }, { - _type: '_doc', _id: 'config:bad', found: false, }, @@ -1770,21 +1747,18 @@ describe('SavedObjectsRepository', () => { callAdminCluster.mockResolvedValue({ docs: [ { - _type: '_doc', _id: 'one', found: true, ...mockVersionProps, _source: { ...mockTimestampFields, config: { title: 'Test1' } }, }, { - _type: '_doc', _id: 'three', found: true, ...mockVersionProps, _source: { ...mockTimestampFields, config: { title: 'Test3' } }, }, { - _type: '_doc', _id: 'five', found: true, ...mockVersionProps, @@ -1859,7 +1833,6 @@ describe('SavedObjectsRepository', () => { beforeEach(() => { callAdminCluster.mockResolvedValue({ _id: `${type}:${id}`, - _type: '_doc', ...mockVersionProps, result: 'updated', }); @@ -2177,7 +2150,6 @@ describe('SavedObjectsRepository', () => { items: objects.map(items => ({ update: { _id: `${items.type}:${items.id}`, - _type: '_doc', ...mockVersionProps, result: 'updated', } @@ -2244,15 +2216,14 @@ describe('SavedObjectsRepository', () => { callAdminCluster.mockReturnValue({ items: objects - // remove invalid from mocks + // remove invalid from mocks .filter(item => item.id !== 'invalid') .map(items => { - switch(items.id) { + switch (items.id) { case 'version_clash': return ({ update: { _id: `${items.type}:${items.id}`, - _type: '_doc', error: { type: 'version_conflict_engine_exception' } @@ -2262,7 +2233,6 @@ describe('SavedObjectsRepository', () => { return ({ update: { _id: `${items.type}:${items.id}`, - _type: '_doc', ...mockVersionProps, result: 'updated', } @@ -2353,7 +2323,7 @@ describe('SavedObjectsRepository', () => { expect(callAdminCluster).toHaveBeenCalledTimes(1); - const [, { body: [{ update: firstUpdate },, { update: secondUpdate }] }] = callAdminCluster.mock.calls[0]; + const [, { body: [{ update: firstUpdate }, , { update: secondUpdate }] }] = callAdminCluster.mock.calls[0]; expect(firstUpdate).toMatchObject({ if_seq_no: 100, @@ -2407,7 +2377,7 @@ describe('SavedObjectsRepository', () => { expect(callAdminCluster).toHaveBeenCalledTimes(1); - const [, { body: [, { doc }] } ] = callAdminCluster.mock.calls[0]; + const [, { body: [, { doc }] }] = callAdminCluster.mock.calls[0]; expect(doc).toMatchObject({ references: [{ @@ -2434,7 +2404,7 @@ describe('SavedObjectsRepository', () => { expect(callAdminCluster).toHaveBeenCalledTimes(1); - const [, { body: [, { doc }] } ] = callAdminCluster.mock.calls[0]; + const [, { body: [, { doc }] }] = callAdminCluster.mock.calls[0]; expect(doc).toMatchObject({ references: [], @@ -2637,7 +2607,6 @@ describe('SavedObjectsRepository', () => { describe('#incrementCounter', () => { beforeEach(() => { callAdminCluster.mockImplementation((method, params) => ({ - _type: '_doc', _id: params.id, ...mockVersionProps, _index: '.kibana', @@ -2657,7 +2626,6 @@ describe('SavedObjectsRepository', () => { it('formats Elasticsearch response', async () => { callAdminCluster.mockImplementation((method, params) => ({ - _type: '_doc', _id: params.id, ...mockVersionProps, _index: '.kibana', @@ -2776,7 +2744,6 @@ describe('SavedObjectsRepository', () => { it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { callAdminCluster.mockImplementation((method, params) => ({ - _type: '_doc', _id: params.id, ...mockVersionProps, _index: '.kibana', diff --git a/src/core/server/saved_objects/service/lib/scoped_client_provider.ts b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts index ad1ceb60cdb86a..87607acd94fc48 100644 --- a/src/core/server/saved_objects/service/lib/scoped_client_provider.ts +++ b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts @@ -100,7 +100,7 @@ export class SavedObjectsClientProvider { this._wrapperFactories.add(priority, { id, factory }); } - setClientFactory(customClientFactory: SavedObjectsClientFactory) { + setClientFactory(customClientFactory: SavedObjectsClientFactory) { if (this._clientFactory !== this._originalClientFactory) { throw new Error(`custom client factory is already set, unable to replace the current one`); } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 14943fc96f2681..73626775381d7d 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1492,6 +1492,8 @@ export interface SavedObjectsLegacyService { // (undocumented) schema: SavedObjectsSchema; // (undocumented) + setScopedSavedObjectsClientFactory: SavedObjectsClientProvider['setClientFactory']; + // (undocumented) types: string[]; } diff --git a/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js b/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js index e48af1f60509bf..4a5d048637fccf 100644 --- a/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js +++ b/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js @@ -35,11 +35,11 @@ import { const recordsToBulkBody = records => { return records.reduce((acc, record) => { - const { index, type, id, source } = record.value; + const { index, id, source } = record.value; return [ ...acc, - { index: { _index: index, _type: type, _id: id } }, + { index: { _index: index, _id: id } }, source ]; }, []); diff --git a/src/es_archiver/lib/docs/index_doc_records_stream.js b/src/es_archiver/lib/docs/index_doc_records_stream.js index 943b1e6e2f329e..3e88f10387f9e5 100644 --- a/src/es_archiver/lib/docs/index_doc_records_stream.js +++ b/src/es_archiver/lib/docs/index_doc_records_stream.js @@ -30,7 +30,6 @@ export function createIndexDocRecordsStream(client, stats, progress) { { index: { _index: doc.index, - _type: doc.type, _id: doc.id, } }, diff --git a/src/es_archiver/lib/indices/kibana_index.js b/src/es_archiver/lib/indices/kibana_index.js index dc916e11d698c0..6f491783829a8c 100644 --- a/src/es_archiver/lib/indices/kibana_index.js +++ b/src/es_archiver/lib/indices/kibana_index.js @@ -176,7 +176,6 @@ export async function cleanKibanaIndices({ client, stats, log, kibanaPluginIds } export async function createDefaultSpace({ index, client }) { await client.create({ index, - type: '_doc', id: 'space:default', ignore: 409, body: { diff --git a/src/fixtures/fake_row.js b/src/fixtures/fake_row.js index 5bc752de299b27..747c6e06be4278 100644 --- a/src/fixtures/fake_row.js +++ b/src/fixtures/fake_row.js @@ -31,7 +31,6 @@ export function getFakeRow(id, mapping) { _id: id, _index: 'test', _source: getFakeRowVals('original', id, mapping), - _type: 'doc', sort: [id], }; } diff --git a/src/fixtures/hits.js b/src/fixtures/hits.js index 413501a3599ed1..e8da3e8ee285bc 100644 --- a/src/fixtures/hits.js +++ b/src/fixtures/hits.js @@ -37,7 +37,6 @@ export default function fitsFixture() { return { _score: 1, _id: 1000 + i, - _type: 'test', _index: 'test-index', _source: { '@timestamp': row[0], diff --git a/src/legacy/core_plugins/console/np_ready/public/application/constants/help_example.txt b/src/legacy/core_plugins/console/np_ready/public/application/constants/help_example.txt index 9bc22ecd2d6301..fd37c413670337 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/constants/help_example.txt +++ b/src/legacy/core_plugins/console/np_ready/public/application/constants/help_example.txt @@ -1,8 +1,8 @@ # index a doc -PUT index/type/1 +PUT index/1 { "body": "here" } # and get it ... -GET index/type/1 +GET index/1 diff --git a/src/legacy/core_plugins/console/public/quarantined/tests/src/input_tokenization.test.js b/src/legacy/core_plugins/console/public/quarantined/tests/src/input_tokenization.test.js index cfb0f1b8c24dbb..01f30f826ab269 100644 --- a/src/legacy/core_plugins/console/public/quarantined/tests/src/input_tokenization.test.js +++ b/src/legacy/core_plugins/console/public/quarantined/tests/src/input_tokenization.test.js @@ -251,10 +251,10 @@ describe('Input Tokenization', () => { 'paren.lparen', '{', 'paren.rparen', '}', 'paren.rparen', '}' ], 'POST _search\n' + - '{\n' + - ' "q": {}\n' + - ' \n' + - '}' + '{\n' + + ' "q": {}\n' + + ' \n' + + '}' ); tokenTest( @@ -263,10 +263,10 @@ describe('Input Tokenization', () => { 'paren.rparen', '}', 'paren.rparen', '}' ], 'POST _search\n' + - '{\n' + - ' "q": { "s": {}}\n' + - ' \n' + - '}' + '{\n' + + ' "q": { "s": {}}\n' + + ' \n' + + '}' ); function statesAsList() { @@ -305,44 +305,44 @@ describe('Input Tokenization', () => { statesTest( ['start', 'json', 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "query": { "match_all": {} }\n' + - '}' + '{\n' + + ' "query": { "match_all": {} }\n' + + '}' ); statesTest( ['start', 'json', ['json', 'json'], ['json', 'json'], 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "query": { \n' + - ' "match_all": {} \n' + - ' }\n' + - '}' + '{\n' + + ' "query": { \n' + + ' "match_all": {} \n' + + ' }\n' + + '}' ); statesTest( ['start', 'json', 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "script": { "source": "" }\n' + - '}' + '{\n' + + ' "script": { "source": "" }\n' + + '}' ); statesTest( ['start', 'json', 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "script": ""\n' + - '}' + '{\n' + + ' "script": ""\n' + + '}' ); statesTest( ['start', 'json', ['json', 'json'], 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "script": {\n' + - ' }\n' + - '}' + '{\n' + + ' "script": {\n' + + ' }\n' + + '}' ); @@ -350,41 +350,41 @@ describe('Input Tokenization', () => { ['start', 'json', ['script-start', 'json', 'json', 'json'], ['script-start', 'json', 'json', 'json'], ['json', 'json'], 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "test": { "script": """\n' + - ' test script\n' + - ' """\n' + - ' }\n' + - '}' + '{\n' + + ' "test": { "script": """\n' + + ' test script\n' + + ' """\n' + + ' }\n' + + '}' ); statesTest( ['start', 'json', ['script-start', 'json'], ['script-start', 'json'], 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "script": """\n' + - ' test script\n' + - ' """,\n' + - '}' + '{\n' + + ' "script": """\n' + + ' test script\n' + + ' """,\n' + + '}' ); statesTest( ['start', 'json', 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "script": """test script""",\n' + - '}' + '{\n' + + ' "script": """test script""",\n' + + '}' ); statesTest( ['start', 'json', ['string_literal', 'json'], ['string_literal', 'json'], 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "something": """\n' + - ' test script\n' + - ' """,\n' + - '}' + '{\n' + + ' "something": """\n' + + ' test script\n' + + ' """,\n' + + '}' ); statesTest( @@ -392,21 +392,21 @@ describe('Input Tokenization', () => { ['json', 'json'], ['json', 'json'], 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "something": { "f" : """\n' + - ' test script\n' + - ' """,\n' + - ' "g": 1\n' + - ' }\n' + - '}' + '{\n' + + ' "something": { "f" : """\n' + + ' test script\n' + + ' """,\n' + + ' "g": 1\n' + + ' }\n' + + '}' ); statesTest( ['start', 'json', 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "something": """test script""",\n' + - '}' + '{\n' + + ' "something": """test script""",\n' + + '}' ); }); diff --git a/src/legacy/core_plugins/console/server/__tests__/proxy_route/params.js b/src/legacy/core_plugins/console/server/__tests__/proxy_route/params.js index 7c6e221d9ce818..aa7b764f84fc78 100644 --- a/src/legacy/core_plugins/console/server/__tests__/proxy_route/params.js +++ b/src/legacy/core_plugins/console/server/__tests__/proxy_route/params.js @@ -61,7 +61,7 @@ describe('Console Proxy Route', () => { const { statusCode } = await server.inject({ method: 'POST', - url: '/api/console/proxy?method=GET&path=/baz/type/id', + url: '/api/console/proxy?method=GET&path=/baz/id', }); expect(statusCode).to.be(403); @@ -79,7 +79,7 @@ describe('Console Proxy Route', () => { const { statusCode } = await server.inject({ method: 'POST', - url: '/api/console/proxy?method=GET&path=/foo/type/id', + url: '/api/console/proxy?method=GET&path=/foo/id', }); expect(statusCode).to.be(200); @@ -98,7 +98,7 @@ describe('Console Proxy Route', () => { const { statusCode } = await server.inject({ method: 'POST', - url: '/api/console/proxy?method=GET&path=/foo/type/id', + url: '/api/console/proxy?method=GET&path=/foo/id', }); expect(statusCode).to.be(200); @@ -116,7 +116,7 @@ describe('Console Proxy Route', () => { server.route(createProxyRoute({ baseUrl: 'http://localhost:9200', getConfigForReq })); await server.inject({ method: 'POST', - url: '/api/console/proxy?method=HEAD&path=/index/type/id', + url: '/api/console/proxy?method=HEAD&path=/index/id', }); sinon.assert.calledOnce(getConfigForReq); @@ -125,8 +125,8 @@ describe('Console Proxy Route', () => { expect(args[0]).to.have.property('method', 'post'); expect(args[0]) .to.have.property('query') - .eql({ method: 'HEAD', path: '/index/type/id' }); - expect(args[1]).to.be('http://localhost:9200/index/type/id?pretty=true'); + .eql({ method: 'HEAD', path: '/index/id' }); + expect(args[1]).to.be('http://localhost:9200/index/id?pretty=true'); }); it('sends the returned timeout, agent, and base headers to request', async () => { @@ -154,7 +154,7 @@ describe('Console Proxy Route', () => { await server.inject({ method: 'POST', - url: '/api/console/proxy?method=HEAD&path=/index/type/id', + url: '/api/console/proxy?method=HEAD&path=/index/id', }); sinon.assert.calledOnce(requestModule.sendRequest); diff --git a/src/legacy/core_plugins/console/server/__tests__/proxy_route/query_string.js b/src/legacy/core_plugins/console/server/__tests__/proxy_route/query_string.js index 7cabe891771646..f20adb897be658 100644 --- a/src/legacy/core_plugins/console/server/__tests__/proxy_route/query_string.js +++ b/src/legacy/core_plugins/console/server/__tests__/proxy_route/query_string.js @@ -85,19 +85,19 @@ describe('Console Proxy Route', () => { }); describe('starts with a slash', () => { it('combines well with the base url', async () => { - await request('GET', '/index/type/id'); + await request('GET', '/index/id'); sinon.assert.calledOnce(requestModule.sendRequest); expect(requestModule.sendRequest.getCall(0).args[0].uri.href).to.be( - 'http://localhost:9200/index/type/id?pretty=true' + 'http://localhost:9200/index/id?pretty=true' ); }); }); describe(`doesn't start with a slash`, () => { it('combines well with the base url', async () => { - await request('GET', 'index/type/id'); + await request('GET', 'index/id'); sinon.assert.calledOnce(requestModule.sendRequest); expect(requestModule.sendRequest.getCall(0).args[0].uri.href).to.be( - 'http://localhost:9200/index/type/id?pretty=true' + 'http://localhost:9200/index/id?pretty=true' ); }); }); diff --git a/src/legacy/core_plugins/console/server/__tests__/wildcard_matcher.js b/src/legacy/core_plugins/console/server/__tests__/wildcard_matcher.js index 4eb0de4e5ebec4..9999c98701ffc7 100644 --- a/src/legacy/core_plugins/console/server/__tests__/wildcard_matcher.js +++ b/src/legacy/core_plugins/console/server/__tests__/wildcard_matcher.js @@ -40,7 +40,7 @@ describe('WildcardMatcher', function () { it('matches nothing', () => should('', '*')); it('does not match /', () => shouldNot('/', '*')); it('matches localhost', () => should('localhost', '*')); - it('matches a path', () => should('/index/type/_search', '*')); + it('matches a path', () => should('/index/_search', '*')); describe('defaultValue = /', function () { it('matches /', () => should('/', '*', '/')); @@ -52,7 +52,7 @@ describe('WildcardMatcher', function () { it('does not match https', () => shouldNot('https', 'http')); it('does not match nothing', () => shouldNot('', 'http')); it('does not match localhost', () => shouldNot('localhost', 'http')); - it('does not match a path', () => shouldNot('/index/type/_search', 'http')); + it('does not match a path', () => shouldNot('/index/_search', 'http')); }); describe('pattern = 560{1..9}', function () { diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/count.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/count.json index a657440f1fe4a2..bd69fd0c77ec84 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/count.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/count.json @@ -30,8 +30,7 @@ ], "patterns": [ "_count", - "{indices}/_count", - "{indices}/{type}/_count" + "{indices}/_count" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/search-count.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/delete_by_query.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/delete_by_query.json index 12735f2d4b6933..3867efd814238c 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/delete_by_query.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/delete_by_query.json @@ -1,6 +1,7 @@ { "delete_by_query": { "url_params": { + "analyzer": "", "analyze_wildcard": "__flag__", "default_operator": [ "AND", @@ -30,7 +31,6 @@ "dfs_query_then_fetch" ], "search_timeout": "", - "size": "", "max_docs": "all documents", "sort": [], "_source": [], @@ -52,8 +52,7 @@ "POST" ], "patterns": [ - "{indices}/_delete_by_query", - "{indices}/{type}/_delete_by_query" + "{indices}/_delete_by_query" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-delete-by-query.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/exists.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/exists.json index 4b7b18b9fe1b38..a6799b9e361cde 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/exists.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/exists.json @@ -21,8 +21,7 @@ "HEAD" ], "patterns": [ - "{indices}/_doc/{id}", - "{indices}/{type}/{id}" + "{indices}/_doc/{id}" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-get.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/explain.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/explain.json index be01e462878db1..e4654a99a76eac 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/explain.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/explain.json @@ -22,8 +22,7 @@ "POST" ], "patterns": [ - "{indices}/_explain/{id}", - "{indices}/{type}/{id}/_explain" + "{indices}/_explain/{id}" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/search-explain.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/get.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/get.json index a0b70545baff99..5a72761c7c32e1 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/get.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/get.json @@ -21,8 +21,7 @@ "GET" ], "patterns": [ - "{indices}/_doc/{id}", - "{indices}/{type}/{id}" + "{indices}/_doc/{id}" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-get.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/get_script_context.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/get_script_context.json new file mode 100644 index 00000000000000..528c261df77078 --- /dev/null +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/get_script_context.json @@ -0,0 +1,10 @@ +{ + "get_script_context": { + "methods": [ + "GET" + ], + "patterns": [ + "_script_context" + ] + } +} diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/get_source.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/get_source.json index 420e03a1bdcf1d..8201960363a784 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/get_source.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/get_source.json @@ -20,8 +20,7 @@ "GET" ], "patterns": [ - "{indices}/_source/{id}", - "{indices}/{type}/{id}/_source" + "{indices}/_source/{id}" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-get.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/index.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/index.json index 7b5551727d6451..25977806776a7a 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/index.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/index.json @@ -29,9 +29,7 @@ ], "patterns": [ "{indices}/_doc/{id}", - "{indices}/_doc", - "{indices}/{type}", - "{indices}/{type}/{id}" + "{indices}/_doc" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-index_.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_field_mapping.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_field_mapping.json index ae82696434cedb..362b266ecb1831 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_field_mapping.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_field_mapping.json @@ -18,9 +18,7 @@ ], "patterns": [ "_mapping/field/{fields}", - "{indices}/_mapping/field/{fields}", - "_mapping/{type}/field/{fields}", - "{indices}/_mapping/{type}/field/{fields}" + "{indices}/_mapping/field/{fields}" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-get-field-mapping.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_mapping.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_mapping.json index 03f5de56ea3514..73f4e42262bf26 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_mapping.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_mapping.json @@ -1,7 +1,6 @@ { "indices.get_mapping": { "url_params": { - "include_type_name": "__flag__", "ignore_unavailable": "__flag__", "allow_no_indices": "__flag__", "expand_wildcards": [ @@ -18,9 +17,7 @@ ], "patterns": [ "_mapping", - "{indices}/_mapping", - "_mapping/{type}", - "{indices}/_mapping/{type}" + "{indices}/_mapping" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-get-mapping.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.put_mapping.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.put_mapping.json index 3b833117be499c..07a62a64b64e15 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.put_mapping.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.put_mapping.json @@ -1,7 +1,6 @@ { "indices.put_mapping": { "url_params": { - "include_type_name": "__flag__", "timeout": "", "master_timeout": "", "ignore_unavailable": "__flag__", @@ -18,14 +17,7 @@ "POST" ], "patterns": [ - "{indices}/_mapping", - "{indices}/{type}/_mapping", - "{indices}/_mapping/{type}", - "{indices}/{type}/_mappings", - "{indices}/_mappings/{type}", - "_mappings/{type}", - "{indices}/_mappings", - "_mapping/{type}" + "{indices}/_mapping" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-put-mapping.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.shrink.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.shrink.json index 6fbdea0f1244bf..31acc86a2fa560 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.shrink.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.shrink.json @@ -1,7 +1,6 @@ { "indices.shrink": { "url_params": { - "copy_settings": "__flag__", "timeout": "", "master_timeout": "", "wait_for_active_shards": "" diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.split.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.split.json index 68f2e338cd2013..1bfbaa078b7967 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.split.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.split.json @@ -1,7 +1,6 @@ { "indices.split": { "url_params": { - "copy_settings": "__flag__", "timeout": "", "master_timeout": "", "wait_for_active_shards": "" diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/mget.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/mget.json index f84b46a379cf4d..612bef571fe3f9 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/mget.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/mget.json @@ -16,8 +16,7 @@ ], "patterns": [ "_mget", - "{indices}/_mget", - "{indices}/{type}/_mget" + "{indices}/_mget" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-multi-get.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch.json index 502d3e25686dfa..ecb71e9ba23c0c 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch.json @@ -20,8 +20,7 @@ ], "patterns": [ "_msearch", - "{indices}/_msearch", - "{indices}/{type}/_msearch" + "{indices}/_msearch" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/search-multi-search.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch_template.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch_template.json index 02077dd91439b2..0b0ca087b1819f 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch_template.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch_template.json @@ -9,7 +9,8 @@ ], "typed_keys": "__flag__", "max_concurrent_searches": "", - "rest_total_hits_as_int": "__flag__" + "rest_total_hits_as_int": "__flag__", + "ccs_minimize_roundtrips": "__flag__" }, "methods": [ "GET", @@ -17,8 +18,7 @@ ], "patterns": [ "_msearch/template", - "{indices}/_msearch/template", - "{indices}/{type}/_msearch/template" + "{indices}/_msearch/template" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/search-multi-search.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/mtermvectors.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/mtermvectors.json index f5c8cbe76bbc44..72a134eca4d2e9 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/mtermvectors.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/mtermvectors.json @@ -25,8 +25,7 @@ ], "patterns": [ "_mtermvectors", - "{indices}/_mtermvectors", - "{indices}/{type}/_mtermvectors" + "{indices}/_mtermvectors" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-multi-termvectors.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/search.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/search.json index 24bda08dd5dbf3..eb21b43644d777 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/search.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/search.json @@ -65,8 +65,7 @@ ], "patterns": [ "_search", - "{indices}/_search", - "{indices}/{type}/_search" + "{indices}/_search" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/search-search.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/search_template.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/search_template.json index 6b73e939843a46..582ecab1dd614b 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/search_template.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/search_template.json @@ -22,7 +22,8 @@ "explain": "__flag__", "profile": "__flag__", "typed_keys": "__flag__", - "rest_total_hits_as_int": "__flag__" + "rest_total_hits_as_int": "__flag__", + "ccs_minimize_roundtrips": "__flag__" }, "methods": [ "GET", @@ -30,8 +31,7 @@ ], "patterns": [ "_search/template", - "{indices}/_search/template", - "{indices}/{type}/_search/template" + "{indices}/_search/template" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/search-template.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/termvectors.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/termvectors.json index 80373d903aad84..d94cffc38b7afc 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/termvectors.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/termvectors.json @@ -24,9 +24,7 @@ ], "patterns": [ "{indices}/_termvectors/{id}", - "{indices}/_termvectors", - "{indices}/{type}/{id}/_termvectors", - "{indices}/{type}/_termvectors" + "{indices}/_termvectors" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-termvectors.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/update_by_query.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/update_by_query.json index e5857f219af460..739ea16888146a 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/update_by_query.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/update_by_query.json @@ -32,7 +32,6 @@ "dfs_query_then_fetch" ], "search_timeout": "", - "size": "", "max_docs": "all documents", "sort": [], "_source": [], @@ -55,8 +54,7 @@ "POST" ], "patterns": [ - "{indices}/_update_by_query", - "{indices}/{type}/_update_by_query" + "{indices}/_update_by_query" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-update-by-query.html" } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js index 39a9f7cd98a574..5956b6c306b0e5 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js @@ -106,10 +106,10 @@ const allSavedObjects = [ }, ]; -const $http = () => {}; +const $http = () => { }; $http.post = jest.fn().mockImplementation(() => []); const defaultProps = { - goInspectObject: () => {}, + goInspectObject: () => { }, confirmModalPromise: jest.fn(), savedObjectsClient: { find: jest.fn(), @@ -256,7 +256,6 @@ describe('ObjectsTable', () => { const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({ _id: obj.id, - _type: obj._type, _source: {}, })); @@ -297,7 +296,6 @@ describe('ObjectsTable', () => { const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({ _id: obj.id, - _type: obj._type, _source: {}, })); diff --git a/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/fetch.test.js b/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/fetch.test.js index e8e95fd92dd110..a5db4602872ee4 100644 --- a/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/fetch.test.js +++ b/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/fetch.test.js @@ -32,7 +32,6 @@ function setupMockCallCluster(optCount, language) { if (optCount === null) { return Promise.resolve({ _index: '.kibana_1', - _type: 'doc', _id: 'kql-telemetry:kql-telemetry', found: false, }); diff --git a/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/scroll.js b/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/scroll.js index a2cc63b4b8679b..6926fa72b0ae22 100644 --- a/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/scroll.js +++ b/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/scroll.js @@ -53,10 +53,8 @@ export function registerScrollForExportRoute(server) { }); return objects.map(hit => { - const type = hit.type; return { _id: hit.id, - _type: type, _source: hit.attributes, _meta: { savedObjectVersion: 2 diff --git a/src/legacy/core_plugins/telemetry/index.ts b/src/legacy/core_plugins/telemetry/index.ts index 4b6566415f3e18..50a25423b5eb82 100644 --- a/src/legacy/core_plugins/telemetry/index.ts +++ b/src/legacy/core_plugins/telemetry/index.ts @@ -17,6 +17,7 @@ * under the License. */ +import * as Rx from 'rxjs'; import { resolve } from 'path'; import JoiNamespace from 'joi'; import { Server } from 'hapi'; @@ -45,6 +46,14 @@ const telemetry = (kibana: any) => { config(Joi: typeof JoiNamespace) { return Joi.object({ enabled: Joi.boolean().default(true), + optIn: Joi.when('allowChangingOptInStatus', { + is: false, + then: Joi.valid(true), + otherwise: Joi.boolean() + .allow(null) + .default(null), + }), + allowChangingOptInStatus: Joi.boolean().default(true), // `config` is used internally and not intended to be set config: Joi.string().default(Joi.ref('$defaultConfigPath')), banner: Joi.boolean().default(true), @@ -80,8 +89,25 @@ const telemetry = (kibana: any) => { }, }, async replaceInjectedVars(originalInjectedVars: any, request: any) { + const config = request.server.config(); + const optIn = config.get('telemetry.optIn'); + const allowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus'); const currentKibanaVersion = getCurrentKibanaVersion(request.server); - const telemetryOptedIn = await getTelemetryOptIn({ request, currentKibanaVersion }); + let telemetryOptedIn: boolean | null; + + if (typeof optIn === 'boolean' && !allowChangingOptInStatus) { + // When not allowed to change optIn status and an optIn value is set, we'll overwrite with that + telemetryOptedIn = optIn; + } else { + telemetryOptedIn = await getTelemetryOptIn({ + request, + currentKibanaVersion, + }); + if (telemetryOptedIn === null) { + // In the senario there's no value set in telemetryOptedIn, we'll return optIn value + telemetryOptedIn = optIn; + } + } return { ...originalInjectedVars, @@ -93,20 +119,36 @@ const telemetry = (kibana: any) => { return { telemetryEnabled: getXpackConfigWithDeprecated(config, 'telemetry.enabled'), telemetryUrl: getXpackConfigWithDeprecated(config, 'telemetry.url'), - telemetryBanner: getXpackConfigWithDeprecated(config, 'telemetry.banner'), - telemetryOptedIn: null, + telemetryBanner: + config.get('telemetry.allowChangingOptInStatus') !== false && + getXpackConfigWithDeprecated(config, 'telemetry.banner'), + telemetryOptedIn: config.get('telemetry.optIn'), + allowChangingOptInStatus: config.get('telemetry.allowChangingOptInStatus'), }; }, hacks: ['plugins/telemetry/hacks/telemetry_init', 'plugins/telemetry/hacks/telemetry_opt_in'], mappings, }, - init(server: Server) { + async init(server: Server) { const initializerContext = { env: { packageInfo: { version: getCurrentKibanaVersion(server), }, }, + config: { + create() { + const config = server.config(); + return Rx.of({ + enabled: config.get('telemetry.enabled'), + optIn: config.get('telemetry.optIn'), + config: config.get('telemetry.config'), + banner: config.get('telemetry.banner'), + url: config.get('telemetry.url'), + allowChangingOptInStatus: config.get('telemetry.allowChangingOptInStatus'), + }); + }, + }, } as PluginInitializerContext; const coreSetup = ({ @@ -114,7 +156,7 @@ const telemetry = (kibana: any) => { log: server.log, } as any) as CoreSetup; - telemetryPlugin(initializerContext).setup(coreSetup); + await telemetryPlugin(initializerContext).setup(coreSetup); // register collectors server.usage.collectorSet.register(createLocalizationUsageCollector(server)); diff --git a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap b/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap index c1ad6276aee250..e1aead3798de7a 100644 --- a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap +++ b/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap @@ -1,6 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`TelemetryForm renders as expected 1`] = ` +exports[`TelemetryForm doesn't render form when not allowed to change optIn status 1`] = `""`; + +exports[`TelemetryForm renders as expected when allows to change optIn status 1`] = ` { switch (key) { case '$http': return mockHttp; + case 'allowChangingOptInStatus': + return true; default: return null; } @@ -47,7 +49,23 @@ const buildTelemetryOptInProvider = () => { }; describe('TelemetryForm', () => { - it('renders as expected', () => { + it('renders as expected when allows to change optIn status', () => { + mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true }); + + expect(shallowWithIntl( + ) + ).toMatchSnapshot(); + }); + + it(`doesn't render form when not allowed to change optIn status`, () => { + mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: false }); + expect(shallowWithIntl( { const optIn = true; const bannerId = 'bruce-banner'; - mockInjectedMetadata({ telemetryOptedIn: optIn }); + mockInjectedMetadata({ telemetryOptedIn: optIn, allowChangingOptInStatus: true }); const telemetryOptInProvider = getTelemetryOptInProvider(); telemetryOptInProvider.setBannerId(bannerId); @@ -92,7 +92,7 @@ describe('click_banner', () => { remove: sinon.spy() }; const optIn = true; - mockInjectedMetadata({ telemetryOptedIn: null }); + mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true }); const telemetryOptInProvider = getTelemetryOptInProvider({ simulateFailure: true }); await clickBanner(telemetryOptInProvider, optIn, { _banners: banners, _toastNotifications: toastNotifications }); @@ -110,7 +110,7 @@ describe('click_banner', () => { remove: sinon.spy() }; const optIn = false; - mockInjectedMetadata({ telemetryOptedIn: null }); + mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true }); const telemetryOptInProvider = getTelemetryOptInProvider({ simulateError: true }); await clickBanner(telemetryOptInProvider, optIn, { _banners: banners, _toastNotifications: toastNotifications }); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js index fd21a5122b5946..f26ca0ca0e3c51 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js +++ b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js @@ -38,7 +38,7 @@ const getTelemetryOptInProvider = (enabled, { simulateFailure = false } = {}) => const chrome = { addBasePath: url => url }; - mockInjectedMetadata({ telemetryOptedIn: enabled }); + mockInjectedMetadata({ telemetryOptedIn: enabled, allowChangingOptInStatus: true }); const $injector = { get: (key) => { diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js index 19e7ccbe618661..240c991a75b646 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js +++ b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js @@ -38,7 +38,7 @@ const getMockInjector = () => { }; const getTelemetryOptInProvider = ({ telemetryOptedIn = null } = {}) => { - mockInjectedMetadata({ telemetryOptedIn }); + mockInjectedMetadata({ telemetryOptedIn, allowChangingOptInStatus: true }); const injector = getMockInjector(); const chrome = { addBasePath: (url) => url diff --git a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js index 0034fa4438238a..26f14fc87d937c 100644 --- a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js +++ b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js @@ -34,7 +34,7 @@ describe('TelemetryOptInProvider', () => { addBasePath: (url) => url }; - mockInjectedMetadata({ telemetryOptedIn: optedIn }); + mockInjectedMetadata({ telemetryOptedIn: optedIn, allowChangingOptInStatus: true }); const mockInjector = { get: (key) => { diff --git a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js index f98f5e16e00c3b..012f8de6400420 100644 --- a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js +++ b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js @@ -24,10 +24,11 @@ import { } from '../../../../../core/public/mocks'; const injectedMetadataMock = injectedMetadataServiceMock.createStartContract(); -export function mockInjectedMetadata({ telemetryOptedIn }) { +export function mockInjectedMetadata({ telemetryOptedIn, allowChangingOptInStatus }) { const mockGetInjectedVar = jest.fn().mockImplementation((key) => { switch (key) { case 'telemetryOptedIn': return telemetryOptedIn; + case 'allowChangingOptInStatus': return allowChangingOptInStatus; default: throw new Error(`unexpected injectedVar ${key}`); } }); diff --git a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts index f4462ffea7a337..4d27bad352cd4d 100644 --- a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts +++ b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts @@ -28,11 +28,15 @@ let currentOptInStatus = false; export function TelemetryOptInProvider($injector: any, chrome: any) { currentOptInStatus = npStart.core.injectedMetadata.getInjectedVar('telemetryOptedIn') as boolean; + const allowChangingOptInStatus = npStart.core.injectedMetadata.getInjectedVar( + 'allowChangingOptInStatus' + ) as boolean; setCanTrackUiMetrics(currentOptInStatus); const provider = { getBannerId: () => bannerId, getOptIn: () => currentOptInStatus, + canChangeOptInStatus: () => allowChangingOptInStatus, setBannerId(id: string) { bannerId = id; }, diff --git a/src/legacy/core_plugins/telemetry/server/plugin.ts b/src/legacy/core_plugins/telemetry/server/plugin.ts index a5f0f1234799a4..813aa0df09e8c8 100644 --- a/src/legacy/core_plugins/telemetry/server/plugin.ts +++ b/src/legacy/core_plugins/telemetry/server/plugin.ts @@ -29,7 +29,7 @@ export class TelemetryPlugin { this.currentKibanaVersion = initializerContext.env.packageInfo.version; } - public setup(core: CoreSetup) { + public async setup(core: CoreSetup) { const currentKibanaVersion = this.currentKibanaVersion; telemetryCollectionManager.setStatsGetter(getStats, 'local'); registerRoutes({ core, currentKibanaVersion }); diff --git a/src/legacy/core_plugins/telemetry/server/routes/index.ts b/src/legacy/core_plugins/telemetry/server/routes/index.ts index 2eb6bf95b4f45e..549b3ef6068ec4 100644 --- a/src/legacy/core_plugins/telemetry/server/routes/index.ts +++ b/src/legacy/core_plugins/telemetry/server/routes/index.ts @@ -27,6 +27,6 @@ interface RegisterRoutesParams { } export function registerRoutes({ core, currentKibanaVersion }: RegisterRoutesParams) { - registerOptInRoutes({ core, currentKibanaVersion }); registerTelemetryDataRoutes(core); + registerOptInRoutes({ core, currentKibanaVersion }); } diff --git a/src/plugins/expressions/public/expression_types/number.ts b/src/plugins/expressions/public/expression_types/number.ts index 8434536f8f6b83..52b2bb1ff3194a 100644 --- a/src/plugins/expressions/public/expression_types/number.ts +++ b/src/plugins/expressions/public/expression_types/number.ts @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { ExpressionType } from '../types'; import { Datatable } from './datatable'; import { Render } from './render'; @@ -28,7 +29,20 @@ export const number = (): ExpressionType => ({ from: { null: () => 0, boolean: b => Number(b), - string: n => Number(n), + string: n => { + const value = Number(n); + if (Number.isNaN(value)) { + throw new Error( + i18n.translate('expressions_np.types.number.fromStringConversionErrorMessage', { + defaultMessage: 'Can\'t typecast "{string}" string to number', + values: { + string: n, + }, + }) + ); + } + return value; + }, }, to: { render: (value: number): Render<{ text: string }> => { diff --git a/src/plugins/expressions/public/expression_types/tests/number.test.ts b/src/plugins/expressions/public/expression_types/tests/number.test.ts new file mode 100644 index 00000000000000..3336a1384ea797 --- /dev/null +++ b/src/plugins/expressions/public/expression_types/tests/number.test.ts @@ -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 { number } from '../number'; + +describe('number', () => { + it('should fail when typecasting not numeric string to number', () => { + expect(() => number().from!.string('123test', {})).toThrowErrorMatchingInlineSnapshot( + `"Can't typecast \\"123test\\" string to number"` + ); + }); +}); diff --git a/test/api_integration/apis/index_patterns/es_errors/lib/get_es_errors.js b/test/api_integration/apis/index_patterns/es_errors/lib/get_es_errors.js index f303bca0da5743..df221a09a661c9 100644 --- a/test/api_integration/apis/index_patterns/es_errors/lib/get_es_errors.js +++ b/test/api_integration/apis/index_patterns/es_errors/lib/get_es_errors.js @@ -36,7 +36,6 @@ export async function getDocNotFoundError(es) { try { await es.get({ index: 'basic_index', - type: 'type', id: '1234' }); } catch (err) { diff --git a/test/functional/apps/management/_handle_version_conflict.js b/test/functional/apps/management/_handle_version_conflict.js index ce5f968a37115e..217e6d4c1a8d38 100644 --- a/test/functional/apps/management/_handle_version_conflict.js +++ b/test/functional/apps/management/_handle_version_conflict.js @@ -56,7 +56,6 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.setScriptedFieldScript(`doc['bytes'].value`); const response = await es.update({ index: '.kibana', - type: '_doc', id: 'index-pattern:logstash-*', body: { 'doc': { 'index-pattern': { 'fieldFormatMap': '{"geo.src":{"id":"number"}}' } } @@ -83,7 +82,6 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.setFieldFormat('url'); const response = await es.update({ index: '.kibana', - type: '_doc', id: 'index-pattern:logstash-*', body: { 'doc': { 'index-pattern': { 'fieldFormatMap': '{"geo.dest":{"id":"number"}}' } } diff --git a/x-pack/legacy/plugins/actions/server/shim.ts b/x-pack/legacy/plugins/actions/server/shim.ts index 0da6b84f2cc691..1af62d276f10bb 100644 --- a/x-pack/legacy/plugins/actions/server/shim.ts +++ b/x-pack/legacy/plugins/actions/server/shim.ts @@ -42,7 +42,7 @@ export interface KibanaConfig { */ export type TaskManagerStartContract = Pick; export type XPackMainPluginSetupContract = Pick; -export type SecurityPluginSetupContract = Pick; +export type SecurityPluginSetupContract = Pick; export type SecurityPluginStartContract = Pick; export type TaskManagerSetupContract = Pick< TaskManager, diff --git a/x-pack/legacy/plugins/alerting/server/shim.ts b/x-pack/legacy/plugins/alerting/server/shim.ts index d86eab2038095d..0ee1ef843d7d05 100644 --- a/x-pack/legacy/plugins/alerting/server/shim.ts +++ b/x-pack/legacy/plugins/alerting/server/shim.ts @@ -41,7 +41,7 @@ export interface Server extends Legacy.Server { * Shim what we're thinking setup and start contracts will look like */ export type TaskManagerStartContract = Pick; -export type SecurityPluginSetupContract = Pick; +export type SecurityPluginSetupContract = Pick; export type SecurityPluginStartContract = Pick; export type XPackMainPluginSetupContract = Pick; export type TaskManagerSetupContract = Pick< diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts index f9c160f4031df3..1a15be1f65dec6 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts @@ -33,7 +33,6 @@ export const esResponse = { hits: [ { _index: 'apm-7.0.0-alpha1-error-2018.04.25', - _type: 'doc', _id: 'qH7C_WIBcmGuKeCHJvvT', _score: null, _source: { @@ -61,7 +60,6 @@ export const esResponse = { hits: [ { _index: 'apm-7.0.0-alpha1-error-2018.04.25', - _type: 'doc', _id: '_3_D_WIBcmGuKeCHFwOW', _score: null, _source: { @@ -93,7 +91,6 @@ export const esResponse = { hits: [ { _index: 'apm-7.0.0-alpha1-error-2018.04.25', - _type: 'doc', _id: 'dn_D_WIBcmGuKeCHQgXJ', _score: null, _source: { @@ -125,7 +122,6 @@ export const esResponse = { hits: [ { _index: 'apm-7.0.0-alpha1-error-2018.04.25', - _type: 'doc', _id: 'dX_D_WIBcmGuKeCHQgXJ', _score: null, _source: { diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.mocks.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.mocks.ts index 982077e2e66655..1ed7f56e0b10d3 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.mocks.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.mocks.ts @@ -22,7 +22,6 @@ export const searchMocks = { hits: [ { _index: '.apm-agent-configuration', - _type: '_doc', _id: '-aQHsm0BxZLczArvNQYW', _score: 0.9808292, _source: { @@ -39,7 +38,6 @@ export const searchMocks = { }, { _index: '.apm-agent-configuration', - _type: '_doc', _id: '-KQHsm0BxZLczArvNAb0', _score: 0.18232156, _source: { @@ -56,7 +54,6 @@ export const searchMocks = { }, { _index: '.apm-agent-configuration', - _type: '_doc', _id: '96QHsm0BxZLczArvNAbD', _score: 0.0, _source: { diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock-responses/mlBucketSpanResponse.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock-responses/mlBucketSpanResponse.ts index a4e54d240f2047..4c329fa9d1035f 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock-responses/mlBucketSpanResponse.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock-responses/mlBucketSpanResponse.ts @@ -19,7 +19,6 @@ export const mlBucketSpanResponse = { hits: [ { _index: '.ml-anomalies-shared', - _type: 'doc', _id: 'opbeans-go-request-high_mean_response_time_model_plot_1542636000000_900_0_29791_0', _score: 1.0, diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.tsx index 0bcb0c89ba1ccb..ab34f332dc1269 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.tsx @@ -54,7 +54,7 @@ export const CustomInterval = ({ gutterSize, buttonSize, onSubmit, defaultValue - +
markdown mock
markdown mock
markdown mock
My Canvas Workpad
" @@ -18,7 +18,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with height specified "
markdown mock
markdown mock
markdown mock
My Canvas Workpad
" @@ -30,7 +30,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with page specified 2` "
markdown mock
markdown mock
markdown mock
My Canvas Workpad
" @@ -42,7 +42,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with width and height "
markdown mock
markdown mock
markdown mock
My Canvas Workpad
" @@ -54,7 +54,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with width specified 2 "
markdown mock
markdown mock
markdown mock
My Canvas Workpad
" diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.examples.storyshot index 1b351e9ac0f6ab..c3352b52c591d2 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.examples.storyshot @@ -88,9 +88,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -126,9 +126,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -165,9 +165,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -205,9 +205,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -249,9 +249,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -290,9 +290,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -332,9 +332,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -368,9 +368,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -418,9 +418,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -471,9 +471,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -509,9 +509,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -545,9 +545,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -582,9 +582,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -620,9 +620,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -658,9 +658,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -698,9 +698,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -735,9 +735,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -772,9 +772,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -811,9 +811,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -862,9 +862,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -905,9 +905,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -996,9 +996,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1065,9 +1065,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1105,9 +1105,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1146,9 +1146,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1188,9 +1188,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1228,9 +1228,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1268,9 +1268,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1312,6 +1312,11 @@ exports[`Storyshots shareables/Canvas component 1`] = ` >
@@ -1620,9 +1625,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1659,9 +1664,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1699,9 +1704,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1743,9 +1748,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1784,9 +1789,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1826,9 +1831,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1862,9 +1867,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1912,9 +1917,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1965,9 +1970,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2003,9 +2008,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2039,9 +2044,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2076,9 +2081,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2114,9 +2119,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2152,9 +2157,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2192,9 +2197,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2229,9 +2234,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2266,9 +2271,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2305,9 +2310,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2356,9 +2361,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2399,9 +2404,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2490,9 +2495,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2559,9 +2564,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2599,9 +2604,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2640,9 +2645,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2682,9 +2687,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2722,9 +2727,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2762,9 +2767,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2806,6 +2811,11 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` >
@@ -3116,6 +3126,11 @@ exports[`Storyshots shareables/Canvas contextual: hello 1`] = ` >
App renders properly 1`] = ` "
markdown mock
" diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/canvas.module.scss b/x-pack/legacy/plugins/canvas/shareable_runtime/components/canvas.module.scss index 88619c150c1f5c..f7e47d8ddeb2b7 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/canvas.module.scss +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/canvas.module.scss @@ -18,5 +18,7 @@ :global .kbnCanvas :local .page { position: absolute; - transform-origin: center center; + transform-origin: left top; + top: 0; + left: 0; } diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.examples.storyshot index bad95ca4fb5b50..6570016336d9e9 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.examples.storyshot @@ -41,9 +41,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -79,9 +79,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -118,9 +118,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -158,9 +158,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -202,9 +202,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -243,9 +243,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -285,9 +285,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -321,9 +321,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -371,9 +371,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -424,9 +424,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -462,9 +462,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -498,9 +498,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -535,9 +535,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -573,9 +573,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -611,9 +611,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -651,9 +651,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -688,9 +688,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -725,9 +725,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -764,9 +764,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -815,9 +815,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -858,9 +858,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -949,9 +949,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1018,9 +1018,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1058,9 +1058,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1099,9 +1099,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1141,9 +1141,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1181,9 +1181,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1221,9 +1221,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1265,6 +1265,11 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` >
@@ -1529,6 +1534,11 @@ exports[`Storyshots shareables/Footer contextual: hello 1`] = ` >
@@ -92,9 +92,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -130,9 +130,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -169,9 +169,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -209,9 +209,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -253,9 +253,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -294,9 +294,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -336,9 +336,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -372,9 +372,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -422,9 +422,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -475,9 +475,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -513,9 +513,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -549,9 +549,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -586,9 +586,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -624,9 +624,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -662,9 +662,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -702,9 +702,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -739,9 +739,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -776,9 +776,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -815,9 +815,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -866,9 +866,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -909,9 +909,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1000,9 +1000,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1069,9 +1069,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1109,9 +1109,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1150,9 +1150,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1192,9 +1192,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1232,9 +1232,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1272,9 +1272,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1333,9 +1333,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: hello 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 150, + "width": 1080, } } > diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.examples.storyshot index 6bb60b8e574a23..8d0ced56d14748 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.examples.storyshot @@ -19,6 +19,11 @@ exports[`Storyshots shareables/Footer/Title component 1`] = ` >
= ({ workpadWidth, }) => { const scale = height / workpadHeight; - const style = { - height: workpadHeight * scale, - width: workpadWidth * scale, - }; const transform = { - ...style, + height: workpadHeight, + width: workpadWidth, transform: `scale3d(${scale}, ${scale}, 1)`, }; @@ -73,7 +70,10 @@ export const PagePreviewComponent: FC = ({ className={css.root} onClick={() => onClick(index)} onKeyPress={() => onClick(index)} - style={style} + style={{ + height: workpadHeight * scale, + width: workpadWidth * scale, + }} >
diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.examples.storyshot index 7c4395820776ee..b159e6499ed9fd 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.examples.storyshot @@ -128,18 +128,8 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings component: off, className="euiFlexItem euiFlexItem--flexGrowZero" >
-
- -
@@ -303,18 +293,8 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings component: on, 5 className="euiFlexItem euiFlexItem--flexGrowZero" >
-
- -
@@ -478,18 +458,8 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings contextual 1`] = className="euiFlexItem euiFlexItem--flexGrowZero" >
-
- -
diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap index 072cf01255a0d2..8c14377374295a 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap @@ -396,18 +396,8 @@ exports[` can navigate Autoplay Settings 2`] = ` class="euiFlexItem euiFlexItem--flexGrowZero" >
-
- -
diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/title.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/title.tsx index 955a8ee530db59..6e3eba7be08d63 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/title.tsx +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/title.tsx @@ -20,7 +20,7 @@ interface Props { */ export const TitleComponent: FC = ({ title }) => ( - + diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/template.html b/x-pack/legacy/plugins/canvas/shareable_runtime/template.html index 94fe2085826677..cb43ceb9af131d 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/template.html +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/template.html @@ -1,3 +1,4 @@ + diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/legacy/plugins/canvas/shareable_runtime/webpack.config.js index f0f98f0f448488..c711f9510a10b4 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/webpack.config.js @@ -41,6 +41,7 @@ module.exports = { KIBANA_ROOT, 'src/legacy/core_plugins/interpreter/public/types' ), + tinymath: path.resolve(KIBANA_ROOT, 'node_modules/tinymath/lib/tinymath.es5.js'), }, extensions: ['.js', '.json', '.ts', '.tsx', '.scss'], }, diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/ccr.forget_follower.json b/x-pack/legacy/plugins/console_extensions/spec/generated/ccr.forget_follower.json index f2aabe9ef4257e..aa9a42c54dff43 100644 --- a/x-pack/legacy/plugins/console_extensions/spec/generated/ccr.forget_follower.json +++ b/x-pack/legacy/plugins/console_extensions/spec/generated/ccr.forget_follower.json @@ -6,6 +6,6 @@ "patterns": [ "{indices}/_ccr/forget_follower" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-post-forget-follower.html" + "documentation": "http://www.elastic.co/guide/en/elasticsearch/reference/current" } } diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/graph.explore.json b/x-pack/legacy/plugins/console_extensions/spec/generated/graph.explore.json index d886f663fd42c2..d4a81c03255f92 100644 --- a/x-pack/legacy/plugins/console_extensions/spec/generated/graph.explore.json +++ b/x-pack/legacy/plugins/console_extensions/spec/generated/graph.explore.json @@ -9,8 +9,7 @@ "POST" ], "patterns": [ - "{indices}/_graph/explore", - "{indices}/{type}/_graph/explore" + "{indices}/_graph/explore" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/graph-explore-api.html" } diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/security.delete_privileges.json b/x-pack/legacy/plugins/console_extensions/spec/generated/security.delete_privileges.json index a7b56aa904bb22..b44798013fe592 100644 --- a/x-pack/legacy/plugins/console_extensions/spec/generated/security.delete_privileges.json +++ b/x-pack/legacy/plugins/console_extensions/spec/generated/security.delete_privileges.json @@ -13,6 +13,6 @@ "patterns": [ "_security/privilege/{application}/{name}" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delete-privilege.html" + "documentation": "TODO" } } diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/security.put_privileges.json b/x-pack/legacy/plugins/console_extensions/spec/generated/security.put_privileges.json index 4dbe88c526f0e6..7ecffc780c0662 100644 --- a/x-pack/legacy/plugins/console_extensions/spec/generated/security.put_privileges.json +++ b/x-pack/legacy/plugins/console_extensions/spec/generated/security.put_privileges.json @@ -14,6 +14,6 @@ "patterns": [ "_security/privilege/" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-privileges.html" + "documentation": "TODO" } } diff --git a/x-pack/legacy/plugins/console_extensions/spec/overrides/security.delete_privileges.json b/x-pack/legacy/plugins/console_extensions/spec/overrides/security.delete_privileges.json new file mode 100644 index 00000000000000..35fb78c532c12b --- /dev/null +++ b/x-pack/legacy/plugins/console_extensions/spec/overrides/security.delete_privileges.json @@ -0,0 +1,3 @@ +{ + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delete-privilege.html" +} diff --git a/x-pack/legacy/plugins/console_extensions/spec/overrides/security.put_privileges.json b/x-pack/legacy/plugins/console_extensions/spec/overrides/security.put_privileges.json new file mode 100644 index 00000000000000..ae37d9a889543d --- /dev/null +++ b/x-pack/legacy/plugins/console_extensions/spec/overrides/security.put_privileges.json @@ -0,0 +1,3 @@ +{ + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-privileges.html" +} diff --git a/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/grokdebugger_request.js b/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/grokdebugger_request.js index 5a86ae56da37b6..35c7825dddfc24 100644 --- a/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/grokdebugger_request.js +++ b/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/grokdebugger_request.js @@ -35,7 +35,6 @@ export class GrokdebuggerRequest { docs: [ { _index: 'grokdebugger', - _type: 'grokdebugger', _id: 'grokdebugger', _source: { rawEvent: this.rawEvent.toString() diff --git a/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_response/__tests__/grokdebugger_response.js b/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_response/__tests__/grokdebugger_response.js index b4e7eb0814bf18..c131ea7e1fd549 100644 --- a/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_response/__tests__/grokdebugger_response.js +++ b/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_response/__tests__/grokdebugger_response.js @@ -19,7 +19,6 @@ describe('grokdebugger_response', () => { { doc: { _index: 'grokdebugger', - _type: 'grokdebugger', _id: 'grokdebugger', _source: { 'request': '/index.html', diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/telemetry_opt_in.test.js.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/telemetry_opt_in.test.js.snap index 642b8399ff6d18..f82e8b03527c04 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/telemetry_opt_in.test.js.snap +++ b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/telemetry_opt_in.test.js.snap @@ -104,7 +104,261 @@ exports[`TelemetryOptIn should display when telemetry not opted in 1`] = ` "timeZone": null, } } -/> +> + +
+ + +

+ + Help Elastic support provide better service + +

+
+ +
+ + + + + + } + className="eui-AlignBaseline" + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="readMorePopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > + +

+ + + , + "telemetryPrivacyStatementLink": + + , + } + } + /> +

+
+ , + } + } + /> + + } + onChange={[Function]} + > +
+ +
+ +
+ + `; exports[`TelemetryOptIn should not display when telemetry is opted in 1`] = ` @@ -213,3 +467,110 @@ exports[`TelemetryOptIn should not display when telemetry is opted in 1`] = ` } /> `; + +exports[`TelemetryOptIn shouldn't display when telemetry optIn status can't change 1`] = ` + +`; diff --git a/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js b/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js index 4e94657e03deeb..a92ca384e8a371 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js +++ b/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; +import { setTelemetryEnabled, setTelemetryOptInService } from '../public/lib/telemetry'; import { TelemetryOptIn } from '../public/components/telemetry_opt_in'; import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; @@ -11,16 +12,30 @@ jest.mock('ui/capabilities', () => ({ get: jest.fn(), })); +setTelemetryEnabled(true); + describe('TelemetryOptIn', () => { test('should display when telemetry not opted in', () => { - const telemetry = require('../public/lib/telemetry'); - telemetry.showTelemetryOptIn = () => { return true; }; + setTelemetryOptInService({ + getOptIn: () => false, + canChangeOptInStatus: () => true, + }); const rendered = mountWithIntl(); expect(rendered).toMatchSnapshot(); }); test('should not display when telemetry is opted in', () => { - const telemetry = require('../public/lib/telemetry'); - telemetry.showTelemetryOptIn = () => { return false; }; + setTelemetryOptInService({ + getOptIn: () => true, + canChangeOptInStatus: () => true, + }); + const rendered = mountWithIntl(); + expect(rendered).toMatchSnapshot(); + }); + test(`shouldn't display when telemetry optIn status can't change`, () => { + setTelemetryOptInService({ + getOptIn: () => false, + canChangeOptInStatus: () => false, + }); const rendered = mountWithIntl(); expect(rendered).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/license_management/public/lib/telemetry.js b/x-pack/legacy/plugins/license_management/public/lib/telemetry.js index bf8bed05aabed3..61d0322227d8ef 100644 --- a/x-pack/legacy/plugins/license_management/public/lib/telemetry.js +++ b/x-pack/legacy/plugins/license_management/public/lib/telemetry.js @@ -25,7 +25,7 @@ export const optInToTelemetry = async (enableTelemetry) => { await telemetryOptInService.setOptIn(enableTelemetry); }; export const shouldShowTelemetryOptIn = () => { - return telemetryEnabled && !telemetryOptInService.getOptIn(); + return telemetryEnabled && !telemetryOptInService.getOptIn() && telemetryOptInService.canChangeOptInStatus(); }; export const getTelemetryFetcher = () => { return fetchTelemetry(httpClient, { unencrypted: true }); diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/bottom_bar.js b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/bottom_bar.js deleted file mode 100644 index 75736d44219cad..00000000000000 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/bottom_bar.js +++ /dev/null @@ -1,86 +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 { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; - -import { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiBottomBar, -} from '@elastic/eui'; - -import { MODE as DATAVISUALIZER_MODE } from '../file_datavisualizer_view'; - -export function BottomBar({ showBar, mode, changeMode, onCancel, disableImport }) { - if (showBar) { - if (mode === DATAVISUALIZER_MODE.READ) { - return ( - - - - changeMode(DATAVISUALIZER_MODE.IMPORT)} - > - - - - - onCancel()} - > - - - - - - - ); - } else { - return ( - - - - changeMode(DATAVISUALIZER_MODE.READ)} - > - - - - - onCancel()} - > - - - - - - ); - } - } - return null; -} diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx new file mode 100644 index 00000000000000..72e4f88062789f --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx @@ -0,0 +1,94 @@ +/* + * 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 React, { FC } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiBottomBar, + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, +} from '@elastic/eui'; + +import { MODE as DATAVISUALIZER_MODE } from '../file_datavisualizer_view/constants'; + +interface BottomBarProps { + mode: DATAVISUALIZER_MODE; + onChangeMode: (mode: DATAVISUALIZER_MODE) => void; + onCancel: () => void; + disableImport?: boolean; +} + +/** + * Bottom bar component for Data Visualizer page. + */ +export const BottomBar: FC = ({ mode, onChangeMode, onCancel, disableImport }) => { + if (mode === DATAVISUALIZER_MODE.READ) { + return ( + + + + + ) : null + } + > + onChangeMode(DATAVISUALIZER_MODE.IMPORT)} + > + + + + + + onCancel()}> + + + + + + ); + } else { + return ( + + + + onChangeMode(DATAVISUALIZER_MODE.READ)}> + + + + + onCancel()}> + + + + + + ); + } +}; diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/index.js b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/index.ts similarity index 99% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/index.js rename to x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/index.ts index 236a5aa577f28d..24202d7c746b8b 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/index.js +++ b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ - export { BottomBar } from './bottom_bar'; diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/constants.ts b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/constants.ts new file mode 100644 index 00000000000000..7c463d58c607c1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/constants.ts @@ -0,0 +1,13 @@ +/* + * 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. + */ + +/** + * File data visualizer modes. + */ +export enum MODE { + READ, + IMPORT, +} diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js index fee0c423f023d7..94e9c2c2ac5846 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js +++ b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js @@ -31,11 +31,7 @@ import { reduceData, hasImportPermission, } from '../utils'; - -export const MODE = { - READ: 0, - IMPORT: 1, -}; +import { MODE } from './constants'; const UPLOAD_SIZE_MB = 5; @@ -306,13 +302,12 @@ export class FileDataVisualizerView extends Component { fields={fields} /> - + />} @@ -329,12 +324,11 @@ export class FileDataVisualizerView extends Component { hideBottomBar={this.hideBottomBar} /> - + />} diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/index.js b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/index.js index 5721b35045434c..cc9714d2b4c458 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/index.js +++ b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/index.js @@ -5,4 +5,4 @@ */ -export { FileDataVisualizerView, MODE } from './file_datavisualizer_view'; +export { FileDataVisualizerView } from './file_datavisualizer_view'; diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/utils/utils.js b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/utils/utils.js index f9148da5ed8262..2707e3f5510dbb 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/utils/utils.js +++ b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/utils/utils.js @@ -116,8 +116,12 @@ export function processResults(results) { }; } -// a check for the minimum privileges needed to create and ingest data into an index. -// if called with no indexName, the check will just look for the minimum cluster privileges. +/** + * A check for the minimum privileges needed to create and ingest data into an index. + * If called with no indexName, the check will just look for the minimum cluster privileges. + * @param {string} indexName + * @returns {Promise} + */ export async function hasImportPermission(indexName) { const priv = { cluster: [ diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/node_available_warning/index.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/node_available_warning/index.ts similarity index 99% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/node_available_warning/index.js rename to x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/node_available_warning/index.ts index 3e3ca96a4d4c31..3ed859580762e5 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/node_available_warning/index.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/node_available_warning/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ - export { NodeAvailableWarning } from './node_available_warning'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/node_available_warning/node_available_warning.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/node_available_warning/node_available_warning.js deleted file mode 100644 index b8bf465eea5769..00000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/node_available_warning/node_available_warning.js +++ /dev/null @@ -1,67 +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 { mlNodesAvailable, permissionToViewMlNodeCount } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; - -import React from 'react'; - -import { - EuiCallOut, - EuiLink, - EuiSpacer, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export function NodeAvailableWarning() { - const isCloud = false; // placeholder for future specific cloud functionality - if ((mlNodesAvailable() === true) || (permissionToViewMlNodeCount() === false)) { - return (); - } else { - return ( - - )} - color="warning" - iconType="alert" - > -

-
- - - - ) - }} - /> - : '', - }} - /> -

-
- -
- ); - } -} diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/node_available_warning/node_available_warning.tsx b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/node_available_warning/node_available_warning.tsx new file mode 100644 index 00000000000000..17562aba8e45a0 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/node_available_warning/node_available_warning.tsx @@ -0,0 +1,66 @@ +/* + * 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 React, { Fragment, FC } from 'react'; + +import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { mlNodesAvailable, permissionToViewMlNodeCount } from '../../../../ml_nodes_check'; +import { cloudDeploymentId, isCloud } from '../../../../jobs/new_job_new/utils/new_job_defaults'; + +export const NodeAvailableWarning: FC = () => { + if (mlNodesAvailable() === true || permissionToViewMlNodeCount() === false) { + return null; + } else { + const id = cloudDeploymentId(); + return ( + + + } + color="warning" + iconType="alert" + > +

+ +
+ + {isCloud && id !== null && ( + +
+ + + + ), + }} + /> +
+ )} +

+
+ +
+ ); + } +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/utils/new_job_defaults.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/utils/new_job_defaults.ts index c86a5a7861b642..e3c4bc8a4a28ce 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/utils/new_job_defaults.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/utils/new_job_defaults.ts @@ -19,20 +19,32 @@ export interface MlServerLimits { max_model_memory_limit?: string; } +export interface CloudInfo { + cloudId: string | null; + isCloud: boolean; +} + let defaults: MlServerDefaults = { anomaly_detectors: {}, datafeeds: {}, }; let limits: MlServerLimits = {}; +const cloudInfo: CloudInfo = { + cloudId: null, + isCloud: false, +}; + export async function loadNewJobDefaults() { try { const resp = await ml.mlInfo(); defaults = resp.defaults; limits = resp.limits; - return { defaults, limits }; + cloudInfo.cloudId = resp.cloudId || null; + cloudInfo.isCloud = resp.cloudId !== undefined; + return { defaults, limits, cloudId: cloudInfo }; } catch (error) { - return { defaults, limits }; + return { defaults, limits, cloudId: cloudInfo }; } } @@ -43,3 +55,24 @@ export function newJobDefaults(): MlServerDefaults { export function newJobLimits(): MlServerLimits { return limits; } + +export function cloudId(): string | null { + return cloudInfo.cloudId; +} + +export function isCloud(): boolean { + return cloudInfo.isCloud; +} + +export function cloudDeploymentId(): string | null { + if (cloudInfo.cloudId === null) { + return null; + } + const tempCloudId = cloudInfo.cloudId.replace(/^.+:/, ''); + try { + const matches = atob(tempCloudId).match(/^.+\$(.+)(?=\$)/); + return matches !== null && matches.length === 2 ? matches[1] : null; + } catch (error) { + return null; + } +} diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts index 2e7da6fb9cc69b..22062331bb380f 100644 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts +++ b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts @@ -54,6 +54,7 @@ export interface MlInfoResponse { version: string; }; upgrade_mode: boolean; + cloudId?: string; } declare interface Ml { diff --git a/x-pack/legacy/plugins/ml/server/routes/system.js b/x-pack/legacy/plugins/ml/server/routes/system.js index 8ca285eb236089..a686971672c580 100644 --- a/x-pack/legacy/plugins/ml/server/routes/system.js +++ b/x-pack/legacy/plugins/ml/server/routes/system.js @@ -21,6 +21,7 @@ import { isSecurityDisabled } from '../lib/security_utils'; export function systemRoutes({ commonRouteConfig, elasticsearchPlugin, + config, route, xpackMainPlugin, spacesPlugin @@ -168,10 +169,17 @@ export function systemRoutes({ route({ method: 'GET', path: '/api/ml/info', - handler(request) { + async handler(request) { const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - return callWithRequest('ml.info') - .catch(resp => wrapError(resp)); + + try { + const info = await callWithRequest('ml.info'); + const cloudIdKey = 'xpack.cloud.id'; + const cloudId = config.has(cloudIdKey) && config.get(cloudIdKey); + return { ...info, cloudId }; + } catch (error) { + return wrapError(error); + } }, config: { ...commonRouteConfig diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.ts index 749ab3c50f1c06..9b64e896dad18d 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.ts @@ -25,6 +25,16 @@ export const getCustomLogo = async ({ // 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, + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, }; const savedObjects = server.savedObjects; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.js b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.js index 2e826f51e7218a..ff49daced4a65f 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.js +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.js @@ -53,6 +53,16 @@ function executeJobFn(server) { // 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, + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, }; const callEndpoint = (endpoint, clientParams = {}, options = {}) => { diff --git a/x-pack/legacy/plugins/reporting/public/components/report_listing.test.tsx b/x-pack/legacy/plugins/reporting/public/components/report_listing.test.tsx index 87360efe04e8bf..d78eb5c409c1f6 100644 --- a/x-pack/legacy/plugins/reporting/public/components/report_listing.test.tsx +++ b/x-pack/legacy/plugins/reporting/public/components/report_listing.test.tsx @@ -39,15 +39,15 @@ jest.mock('ui/kfetch', () => ({ if (pathname === '/api/reporting/jobs/list') { return Promise.resolve([ { _index: '.reporting-2019.08.18', _id: 'jzoik8dh1q2i89fb5f19znm6', _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.869Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _type: '_doc', _id: 'jzoik7tn1q2i89fb5f60e5ve', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.155Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _type: '_doc', _id: 'jzoik5tb1q2i89fb5fckchny', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:21.551Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _type: '_doc', _id: 'jzoik5a11q2i89fb5f130t2m', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:20.857Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _type: '_doc', _id: 'jzoik3ka1q2i89fb5fdx93g7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:18.634Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _type: '_doc', _id: 'jzoik2vt1q2i89fb5ffw723n', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:17.753Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _type: '_doc', _id: 'jzoik1851q2i89fb5fdge6e7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1080, height: 720 } }, type: 'canvas workpad', title: 'My Canvas Workpad - Dark', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:15.605Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _type: '_doc', _id: 'jzoijyre1q2i89fb5fa7xzvi', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:12.410Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _type: '_doc', _id: 'jzoijv5h1q2i89fb5ffklnhx', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:07.733Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _type: '_doc', _id: 'jznhgk7r1bx789fb5f6hxok7', _score: null, _source: { kibana_name: 'spicy.local', browser_type: 'chromium', created_at: '2019-08-23T02:15:47.799Z', jobtype: 'printable_pdf', created_by: 'elastic', kibana_id: 'ca75e26c-2b7d-464f-aef0-babb67c735a0', output: { content_type: 'application/pdf', size: 877114 }, completed_at: '2019-08-23T02:15:57.707Z', payload: { type: 'dashboard (legacy)', title: 'tests-panels', }, max_attempts: 3, started_at: '2019-08-23T02:15:48.794Z', attempts: 1, status: 'completed', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik7tn1q2i89fb5f60e5ve', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.155Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik5tb1q2i89fb5fckchny', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:21.551Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik5a11q2i89fb5f130t2m', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:20.857Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik3ka1q2i89fb5fdx93g7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:18.634Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik2vt1q2i89fb5ffw723n', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:17.753Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoik1851q2i89fb5fdge6e7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1080, height: 720 } }, type: 'canvas workpad', title: 'My Canvas Workpad - Dark', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:15.605Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoijyre1q2i89fb5fa7xzvi', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:12.410Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jzoijv5h1q2i89fb5ffklnhx', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:07.733Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore + { _index: '.reporting-2019.08.18', _id: 'jznhgk7r1bx789fb5f6hxok7', _score: null, _source: { kibana_name: 'spicy.local', browser_type: 'chromium', created_at: '2019-08-23T02:15:47.799Z', jobtype: 'printable_pdf', created_by: 'elastic', kibana_id: 'ca75e26c-2b7d-464f-aef0-babb67c735a0', output: { content_type: 'application/pdf', size: 877114 }, completed_at: '2019-08-23T02:15:57.707Z', payload: { type: 'dashboard (legacy)', title: 'tests-panels', }, max_attempts: 3, started_at: '2019-08-23T02:15:48.794Z', attempts: 1, status: 'completed', }, }, // prettier-ignore ]); } diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js index 9f638670a6b599..84549d0680ff36 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js @@ -1072,7 +1072,6 @@ describe('Get Doc Path from ES Response', () => { it('returns a formatted string after response of an update', function () { const responseMock = { _index: 'foo', - _type: '_doc', _id: 'booId', }; expect(getUpdatedDocPath(responseMock)).equal('/foo/_doc/booId'); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js index 61af0437575e9c..fe2e578f83cc5e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js @@ -22,8 +22,8 @@ export function formatJobObject(job) { } export function getUpdatedDocPath(response) { - const { _index: ind, _type: type = '_doc', _id: id } = response; - return `/${ind}/${type}/${id}`; + const { _index: ind, _id: id } = response; + return `/${ind}/${id}`; } const MAX_PARTIAL_ERROR_LENGTH = 1000; // 1000 of beginning, 1000 of end diff --git a/x-pack/legacy/plugins/reporting/server/lib/get_user.js b/x-pack/legacy/plugins/reporting/server/lib/get_user.js index 70af19239df87b..2c4f3bcb2dd36d 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/get_user.js +++ b/x-pack/legacy/plugins/reporting/server/lib/get_user.js @@ -5,23 +5,19 @@ */ import { oncePerServer } from './once_per_server'; -import { getClient as getShieldClient } from '../../../../server/lib/get_client_shield'; function getUserFn(server) { - const callShieldWithRequest = getShieldClient(server).callWithRequest; - - return async function getUser(request) { - const xpackInfo = server.plugins.xpack_main.info; - if (xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security').isEnabled()) { - try { - return await callShieldWithRequest(request, 'shield.authenticate'); - } catch (err) { - server.log(['reporting', 'getUser', 'debug'], err); - return null; - } + return async request => { + if (!server.plugins.security) { + return null; } - return null; + try { + return await server.plugins.security.getUser(request); + } catch (err) { + server.log(['reporting', 'getUser', 'debug'], err); + return null; + } }; } diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/__tests__/authorized_user_pre_routing.test.js b/x-pack/legacy/plugins/reporting/server/routes/lib/__tests__/authorized_user_pre_routing.test.js index 42b2019507fe9a..c9c93727fd45f6 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/__tests__/authorized_user_pre_routing.test.js +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/__tests__/authorized_user_pre_routing.test.js @@ -15,7 +15,7 @@ describe('authorized_user_pre_routing', function () { // so createMockServer reuses the same 'instance' of the server and overwrites // the properties to contain different values const createMockServer = (function () { - const callWithRequestStub = sinon.stub(); + const getUserStub = sinon.stub(); let mockConfig; const mockServer = { @@ -30,13 +30,7 @@ describe('authorized_user_pre_routing', function () { log: function () {}, plugins: { xpack_main: {}, - elasticsearch: { - createCluster: function () { - return { - callWithRequest: callWithRequestStub - }; - } - } + security: { getUser: getUserStub }, } }; @@ -57,8 +51,8 @@ describe('authorized_user_pre_routing', function () { } }; - callWithRequestStub.resetHistory(); - callWithRequestStub.returns(Promise.resolve(user)); + getUserStub.resetHistory(); + getUserStub.resolves(user); return mockServer; }; }()); diff --git a/x-pack/legacy/plugins/security/common/constants.ts b/x-pack/legacy/plugins/security/common/constants.ts index 2ec429b4d9c4cb..08e49ad995550b 100644 --- a/x-pack/legacy/plugins/security/common/constants.ts +++ b/x-pack/legacy/plugins/security/common/constants.ts @@ -4,8 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const GLOBAL_RESOURCE = '*'; -export const IGNORED_TYPES = ['space']; -export const APPLICATION_PREFIX = 'kibana-'; -export const RESERVED_PRIVILEGES_APPLICATION_WILDCARD = 'kibana-*'; export const INTERNAL_API_BASE_PATH = '/internal/security'; diff --git a/x-pack/legacy/plugins/security/common/login_state.ts b/x-pack/legacy/plugins/security/common/login_state.ts index b41fb85214c66b..b1eb3d61fe5f30 100644 --- a/x-pack/legacy/plugins/security/common/login_state.ts +++ b/x-pack/legacy/plugins/security/common/login_state.ts @@ -9,5 +9,4 @@ export type LoginLayout = 'form' | 'error-es-unavailable' | 'error-xpack-unavail export interface LoginState { layout: LoginLayout; allowLogin: boolean; - loginMessage: string; } diff --git a/x-pack/legacy/plugins/security/common/model/index.ts b/x-pack/legacy/plugins/security/common/model/index.ts index 19243c25fef7e6..6c2976815559ba 100644 --- a/x-pack/legacy/plugins/security/common/model/index.ts +++ b/x-pack/legacy/plugins/security/common/model/index.ts @@ -4,14 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -export { Role, RoleIndexPrivilege, RoleKibanaPrivilege } from './role'; -export { FeaturesPrivileges } from './features_privileges'; -export { RawKibanaPrivileges, RawKibanaFeaturePrivileges } from './raw_kibana_privileges'; -export { KibanaPrivileges } from './kibana_privileges'; export { ApiKey } from './api_key'; -export { User, EditUser, getUserDisplayName } from '../../../../../plugins/security/common/model'; export { AuthenticatedUser, + BuiltinESPrivileges, + EditUser, + FeaturesPrivileges, + KibanaPrivileges, + RawKibanaFeaturePrivileges, + RawKibanaPrivileges, + Role, + RoleIndexPrivilege, + RoleKibanaPrivilege, + User, canUserChangePassword, + getUserDisplayName, } from '../../../../../plugins/security/common/model'; -export { BuiltinESPrivileges } from './builtin_es_privileges'; diff --git a/x-pack/legacy/plugins/security/index.d.ts b/x-pack/legacy/plugins/security/index.d.ts index a0d18dd3cbb99c..18284c8be689a1 100644 --- a/x-pack/legacy/plugins/security/index.d.ts +++ b/x-pack/legacy/plugins/security/index.d.ts @@ -6,12 +6,10 @@ import { Legacy } from 'kibana'; import { AuthenticatedUser } from './common/model'; -import { AuthorizationService } from './server/lib/authorization/service'; /** * Public interface of the security plugin. */ export interface SecurityPlugin { - authorization: Readonly; getUser: (request: Legacy.Request) => Promise; } diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index f9e82f575ce2e2..c098e3e67a6d91 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -8,29 +8,13 @@ import { resolve } from 'path'; import { initAuthenticateApi } from './server/routes/api/v1/authenticate'; import { initUsersApi } from './server/routes/api/v1/users'; import { initApiKeysApi } from './server/routes/api/v1/api_keys'; -import { initExternalRolesApi } from './server/routes/api/external/roles'; -import { initPrivilegesApi } from './server/routes/api/external/privileges'; import { initIndicesApi } from './server/routes/api/v1/indices'; -import { initGetBuiltinPrivilegesApi } from './server/routes/api/v1/builtin_privileges'; import { initOverwrittenSessionView } from './server/routes/views/overwritten_session'; import { initLoginView } from './server/routes/views/login'; import { initLogoutView } from './server/routes/views/logout'; import { initLoggedOutView } from './server/routes/views/logged_out'; -import { checkLicense } from './server/lib/check_license'; -import { SecurityAuditLogger } from './server/lib/audit_logger'; import { AuditLogger } from '../../server/lib/audit_logger'; -import { - createAuthorizationService, - disableUICapabilitesFactory, - initAPIAuthorization, - initAppAuthorization, - registerPrivilegesWithCluster, - validateFeaturePrivileges -} 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'; -import { createOptionalPlugin } from '../../server/lib/optional_plugin'; import { KibanaRequest } from '../../../../src/core/server'; import { createCSPRuleString } from '../../../../src/legacy/server/csp'; @@ -103,23 +87,22 @@ export const security = (kibana) => new kibana.Plugin({ } return { - secureCookies: securityPlugin.config.secureCookies, - sessionTimeout: securityPlugin.config.sessionTimeout, + secureCookies: securityPlugin.__legacyCompat.config.secureCookies, + sessionTimeout: securityPlugin.__legacyCompat.config.sessionTimeout, enableSpaceAwarePrivileges: server.config().get('xpack.spaces.enabled'), }; }, }, async postInit(server) { - const plugin = this; - - const xpackMainPlugin = server.plugins.xpack_main; + const securityPlugin = server.newPlatform.setup.plugins.security; + if (!securityPlugin) { + throw new Error('New Platform XPack Security plugin is not available.'); + } - watchStatusAndLicenseToInitialize(xpackMainPlugin, plugin, async (license) => { - if (license.allowRbac) { - const { security } = server.plugins; - await validateFeaturePrivileges(security.authorization.actions, xpackMainPlugin.getFeatures()); - await registerPrivilegesWithCluster(server); + watchStatusAndLicenseToInitialize(server.plugins.xpack_main, this, async () => { + if (securityPlugin.__legacyCompat.license.getFeatures().allowRbac) { + await securityPlugin.__legacyCompat.registerPrivilegesWithCluster(); } }); }, @@ -131,110 +114,46 @@ export const security = (kibana) => new kibana.Plugin({ } const config = server.config(); - const xpackMainPlugin = server.plugins.xpack_main; - const xpackInfo = xpackMainPlugin.info; - securityPlugin.registerLegacyAPI({ - xpackInfo, + const xpackInfo = server.plugins.xpack_main.info; + securityPlugin.__legacyCompat.registerLegacyAPI({ + savedObjects: server.savedObjects, + auditLogger: new AuditLogger(server, 'security', config, xpackInfo), isSystemAPIRequest: server.plugins.kibana.systemApi.isSystemApiRequest.bind( server.plugins.kibana.systemApi ), + capabilities: { registerCapabilitiesModifier: server.registerCapabilitiesModifier }, cspRules: createCSPRuleString(config.get('csp.rules')), + kibanaIndexName: config.get('kibana.index'), }); - const plugin = this; - const xpackInfoFeature = xpackInfo.feature(plugin.id); - - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackInfoFeature.registerLicenseCheckResultsGenerator(checkLicense); + // Legacy xPack Info endpoint returns whatever we return in a callback for `registerLicenseCheckResultsGenerator` + // and the result is consumed by the legacy plugins all over the place, so we should keep it here for now. We assume + // that when legacy callback is called license has been already propagated to the new platform security plugin and + // features are up to date. + xpackInfo.feature(this.id).registerLicenseCheckResultsGenerator( + () => securityPlugin.__legacyCompat.license.getFeatures() + ); server.expose({ getUser: request => securityPlugin.authc.getCurrentUser(KibanaRequest.from(request)) }); - const { savedObjects } = server; - - const spaces = createOptionalPlugin(config, 'xpack.spaces', server.plugins, 'spaces'); - - // exposes server.plugins.security.authorization - const authorization = createAuthorizationService(server, xpackInfoFeature, xpackMainPlugin, spaces); - server.expose('authorization', deepFreeze(authorization)); - - const auditLogger = new SecurityAuditLogger(new AuditLogger(server, 'security', server.config(), xpackInfo)); - - savedObjects.setScopedSavedObjectsClientFactory(({ - request, - }) => { - const adminCluster = server.plugins.elasticsearch.getCluster('admin'); - const { callWithRequest, callWithInternalUser } = adminCluster; - const callCluster = (...args) => callWithRequest(request, ...args); - - if (authorization.mode.useRbacForRequest(request)) { - const internalRepository = savedObjects.getSavedObjectsRepository(callWithInternalUser); - return new savedObjects.SavedObjectsClient(internalRepository); - } - - const callWithRequestRepository = savedObjects.getSavedObjectsRepository(callCluster); - return new savedObjects.SavedObjectsClient(callWithRequestRepository); - }); - - savedObjects.addScopedSavedObjectsClientWrapperFactory(Number.MAX_SAFE_INTEGER - 1, 'security', ({ client, request }) => { - if (authorization.mode.useRbacForRequest(request)) { - return new SecureSavedObjectsClientWrapper({ - actions: authorization.actions, - auditLogger, - baseClient: client, - checkSavedObjectsPrivilegesWithRequest: authorization.checkSavedObjectsPrivilegesWithRequest, - errors: savedObjects.SavedObjectsClient.errors, - request, - savedObjectTypes: savedObjects.types, - }); - } - - return client; - }); - initAuthenticateApi(securityPlugin, server); - initAPIAuthorization(server, authorization); - initAppAuthorization(server, xpackMainPlugin, authorization); initUsersApi(securityPlugin, server); initApiKeysApi(server); - initExternalRolesApi(server); initIndicesApi(server); - initPrivilegesApi(server); - initGetBuiltinPrivilegesApi(server); - initLoginView(securityPlugin, server, xpackMainPlugin); + initLoginView(securityPlugin, server); initLogoutView(server); initLoggedOutView(securityPlugin, server); initOverwrittenSessionView(server); server.injectUiAppVars('login', () => { - - const { showLogin, loginMessage, allowLogin, layout = 'form' } = xpackInfo.feature(plugin.id).getLicenseCheckResults() || {}; - + const { showLogin, allowLogin, layout = 'form' } = securityPlugin.__legacyCompat.license.getFeatures(); return { loginState: { showLogin, allowLogin, - loginMessage, layout, } }; }); - - server.registerCapabilitiesModifier((request, uiCapabilities) => { - // if we have a license which doesn't enable security, or we're a legacy user - // we shouldn't disable any ui capabilities - const { authorization } = server.plugins.security; - if (!authorization.mode.useRbacForRequest(request)) { - return uiCapabilities; - } - - const disableUICapabilites = disableUICapabilitesFactory(server, request); - // if we're an anonymous route, we disable all ui capabilities - if (request.route.settings.auth === false) { - return disableUICapabilites.all(uiCapabilities); - } - - return disableUICapabilites.usingPrivileges(uiCapabilities); - }); } }); diff --git a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.ts b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.ts index f2e2c4bc1be990..aa7096d141f434 100644 --- a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.ts +++ b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.ts @@ -6,7 +6,10 @@ import _ from 'lodash'; import { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../common/model'; -import { areActionsFullyCovered, compareActions } from '../../../common/privilege_calculator_utils'; +import { + areActionsFullyCovered, + compareActions, +} from '../../../../../../plugins/security/common/privilege_calculator_utils'; import { NO_PRIVILEGE_VALUE } from '../../views/management/edit_role/lib/constants'; import { isGlobalPrivilegeDefinition } from '../privilege_utils'; import { diff --git a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_base_privilege_calculator.ts b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_base_privilege_calculator.ts index 37ed5b6c02e9b4..dd4e91aa4037a8 100644 --- a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_base_privilege_calculator.ts +++ b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_base_privilege_calculator.ts @@ -6,7 +6,7 @@ import _ from 'lodash'; import { KibanaPrivileges, RoleKibanaPrivilege } from '../../../common/model'; -import { compareActions } from '../../../common/privilege_calculator_utils'; +import { compareActions } from '../../../../../../plugins/security/common/privilege_calculator_utils'; import { NO_PRIVILEGE_VALUE } from '../../views/management/edit_role/lib/constants'; import { isGlobalPrivilegeDefinition } from '../privilege_utils'; import { PRIVILEGE_SOURCE, PrivilegeExplanation } from './kibana_privilege_calculator_types'; diff --git a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.ts b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.ts index 597a05a5372b1c..ed18b5d1e89a78 100644 --- a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.ts +++ b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.ts @@ -6,7 +6,7 @@ import _ from 'lodash'; import { FeaturesPrivileges, KibanaPrivileges, RoleKibanaPrivilege } from '../../../common/model'; -import { areActionsFullyCovered } from '../../../common/privilege_calculator_utils'; +import { areActionsFullyCovered } from '../../../../../../plugins/security/common/privilege_calculator_utils'; import { NO_PRIVILEGE_VALUE } from '../../views/management/edit_role/lib/constants'; import { isGlobalPrivilegeDefinition } from '../privilege_utils'; import { diff --git a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_privileges_calculator_factory.ts b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_privileges_calculator_factory.ts index 3d8a0698465ab2..aee6943214c577 100644 --- a/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_privileges_calculator_factory.ts +++ b/x-pack/legacy/plugins/security/public/lib/kibana_privilege_calculator/kibana_privileges_calculator_factory.ts @@ -5,7 +5,7 @@ */ import _ from 'lodash'; import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../common/model'; -import { compareActions } from '../../../common/privilege_calculator_utils'; +import { compareActions } from '../../../../../../plugins/security/common/privilege_calculator_utils'; import { copyRole } from '../../lib/role_utils'; import { KibanaPrivilegeCalculator } from './kibana_privilege_calculator'; diff --git a/x-pack/legacy/plugins/security/public/lib/roles_api.ts b/x-pack/legacy/plugins/security/public/lib/roles_api.ts index b83e9369a37ea9..20c1491ccaac69 100644 --- a/x-pack/legacy/plugins/security/public/lib/roles_api.ts +++ b/x-pack/legacy/plugins/security/public/lib/roles_api.ts @@ -5,7 +5,7 @@ */ import { kfetch } from 'ui/kfetch'; -import { Role } from '../../common/model/role'; +import { Role } from '../../common/model'; export class RolesApi { public static async getRoles(): Promise { diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.test.tsx b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.test.tsx index 21c1dacb06d422..664c9f2a046c09 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.test.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.test.tsx @@ -33,7 +33,6 @@ const createLoginState = (options?: Partial) => { return { allowLogin: true, layout: 'form', - loginMessage: '', ...options, } as LoginState; }; diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap b/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap index 852cbb26a1dcfb..fc33c6e0a82cc2 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap @@ -389,7 +389,6 @@ exports[`LoginPage enabled form state renders as expected 1`] = ` Object { "allowLogin": true, "layout": "form", - "loginMessage": "", } } next="" diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.test.tsx b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.test.tsx index 8d7bd0e10352a4..af91d12624c644 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.test.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.test.tsx @@ -32,7 +32,6 @@ const createLoginState = (options?: Partial) => { return { allowLogin: true, layout: 'form', - loginMessage: '', ...options, } as LoginState; }; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx index 75f9520cef64b7..cb60b773f92e0e 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx @@ -10,9 +10,9 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { UICapabilities } from 'ui/capabilities'; import { Space } from '../../../../../../spaces/common/model/space'; import { Feature } from '../../../../../../../../plugins/features/server'; +import { Actions } from '../../../../../../../../plugins/security/server/authorization/actions'; +import { privilegesFactory } from '../../../../../../../../plugins/security/server/authorization/privileges'; import { RawKibanaPrivileges, Role } from '../../../../../common/model'; -import { actionsFactory } from '../../../../../server/lib/authorization/actions'; -import { privilegesFactory } from '../../../../../server/lib/authorization/privileges'; import { EditRolePage } from './edit_role_page'; import { SimplePrivilegeSection } from './privileges/kibana/simple_privilege_section'; import { SpaceAwarePrivilegeSection } from './privileges/kibana/space_aware_privilege_section'; @@ -56,13 +56,9 @@ const buildFeatures = () => { }; const buildRawKibanaPrivileges = () => { - const xpackMainPlugin = { + return privilegesFactory(new Actions('unit_test_version'), { getFeatures: () => buildFeatures(), - }; - - const actions = actionsFactory({ get: jest.fn(() => 'unit_test_version') }); - - return privilegesFactory(actions, xpackMainPlugin as any).get(); + }).get(); }; const buildBuiltinESPrivileges = () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js b/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js index b1cf7e9f467561..24e304b0010d06 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js @@ -88,7 +88,7 @@ const routeDefinition = (action) => ({ return kfetch({ method: 'get', pathname: '/api/security/privileges', query: { includeActions: true } }); }, builtinESPrivileges() { - return kfetch({ method: 'get', pathname: '/api/security/v1/esPrivileges/builtin' }); + return kfetch({ method: 'get', pathname: '/internal/security/esPrivileges/builtin' }); }, features() { return kfetch({ method: 'get', pathname: '/api/features' }).catch(e => { diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.tsx b/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.tsx index 9a1d029273c190..d0645f85946dba 100644 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.tsx @@ -21,7 +21,7 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { Component } from 'react'; import { toastNotifications } from 'ui/notify'; -import { Role } from '../../../../../common/model/role'; +import { Role } from '../../../../../common/model'; import { isRoleEnabled, isReadOnlyRole, isReservedRole } from '../../../../lib/role_utils'; import { RolesApi } from '../../../../lib/roles_api'; import { ConfirmDelete } from './confirm_delete'; diff --git a/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/h.js b/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/h.js deleted file mode 100644 index 31355b87957549..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/h.js +++ /dev/null @@ -1,21 +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 { stub } from 'sinon'; - -export function hFixture() { - const h = {}; - - Object.assign(h, { - authenticated: stub().returns(h), - continue: 'continue value', - redirect: stub().returns(h), - unstate: stub().returns(h), - takeover: stub().returns(h) - }); - - return h; -} diff --git a/x-pack/legacy/plugins/security/server/lib/__tests__/check_license.js b/x-pack/legacy/plugins/security/server/lib/__tests__/check_license.js deleted file mode 100644 index ad5c59f36eb443..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/__tests__/check_license.js +++ /dev/null @@ -1,133 +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 '@kbn/expect'; -import sinon from 'sinon'; -import { checkLicense } from '../check_license'; - -describe('check_license', function () { - - let mockXPackInfo; - - beforeEach(function () { - mockXPackInfo = { - isAvailable: sinon.stub(), - isXpackUnavailable: sinon.stub(), - feature: sinon.stub(), - license: sinon.stub({ - isOneOf() { }, - }) - }; - - mockXPackInfo.isAvailable.returns(true); - }); - - it('should display error when ES is unavailable', () => { - mockXPackInfo.isAvailable.returns(false); - mockXPackInfo.isXpackUnavailable.returns(false); - - const licenseCheckResults = checkLicense(mockXPackInfo); - expect(licenseCheckResults).to.be.eql({ - showLogin: true, - allowLogin: false, - showLinks: false, - allowRoleDocumentLevelSecurity: false, - allowRoleFieldLevelSecurity: false, - layout: 'error-es-unavailable', - allowRbac: false, - }); - }); - - it('should display error when X-Pack is unavailable', () => { - mockXPackInfo.isAvailable.returns(false); - mockXPackInfo.isXpackUnavailable.returns(true); - - const licenseCheckResults = checkLicense(mockXPackInfo); - expect(licenseCheckResults).to.be.eql({ - showLogin: true, - allowLogin: false, - showLinks: false, - allowRoleDocumentLevelSecurity: false, - allowRoleFieldLevelSecurity: false, - layout: 'error-xpack-unavailable', - allowRbac: false, - }); - }); - - - it('should show login page and other security elements if license is basic and security is enabled.', () => { - mockXPackInfo.license.isOneOf.withArgs(['basic']).returns(true); - mockXPackInfo.license.isOneOf.withArgs(['platinum', 'trial']).returns(false); - mockXPackInfo.feature.withArgs('security').returns({ - isEnabled: () => { return true; } - }); - - const licenseCheckResults = checkLicense(mockXPackInfo); - expect(licenseCheckResults).to.be.eql({ - showLogin: true, - allowLogin: true, - showLinks: true, - allowRoleDocumentLevelSecurity: false, - allowRoleFieldLevelSecurity: false, - allowRbac: true - }); - }); - - it('should not show login page or other security elements if security is disabled in Elasticsearch.', () => { - mockXPackInfo.license.isOneOf.withArgs(['basic']).returns(false); - mockXPackInfo.feature.withArgs('security').returns({ - isEnabled: () => { return false; } - }); - - const licenseCheckResults = checkLicense(mockXPackInfo); - expect(licenseCheckResults).to.be.eql({ - showLogin: false, - allowLogin: false, - showLinks: false, - allowRoleDocumentLevelSecurity: false, - allowRoleFieldLevelSecurity: false, - allowRbac: false, - linksMessage: 'Access is denied because Security is disabled in Elasticsearch.' - }); - }); - - it('should allow to login and allow RBAC but forbid document level security if license is not platinum or trial.', () => { - mockXPackInfo.license.isOneOf - .returns(false) - .withArgs(['platinum', 'trial']).returns(false); - mockXPackInfo.feature.withArgs('security').returns({ - isEnabled: () => { return true; } - }); - - expect(checkLicense(mockXPackInfo)).to.be.eql({ - showLogin: true, - allowLogin: true, - showLinks: true, - allowRoleDocumentLevelSecurity: false, - allowRoleFieldLevelSecurity: false, - allowRbac: true, - }); - }); - - it('should allow to login, allow RBAC and document level security if license is platinum or trial.', () => { - mockXPackInfo.license.isOneOf - .returns(false) - .withArgs(['platinum', 'trial']).returns(true); - mockXPackInfo.feature.withArgs('security').returns({ - isEnabled: () => { return true; } - }); - - expect(checkLicense(mockXPackInfo)).to.be.eql({ - showLogin: true, - allowLogin: true, - showLinks: true, - allowRoleDocumentLevelSecurity: true, - allowRoleFieldLevelSecurity: true, - allowRbac: true, - }); - }); - -}); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/actions/actions.test.ts b/x-pack/legacy/plugins/security/server/lib/authorization/actions/actions.test.ts deleted file mode 100644 index 11194d237e10c5..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/authorization/actions/actions.test.ts +++ /dev/null @@ -1,57 +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 { actionsFactory } from '.'; - -const createMockConfig = (settings: Record = {}) => { - const mockConfig = { - get: jest.fn(), - }; - - mockConfig.get.mockImplementation(key => settings[key]); - - return mockConfig; -}; - -describe('#constructor', () => { - test('requires version to be a string', () => { - const mockConfig = createMockConfig(); - - expect(() => actionsFactory(mockConfig)).toThrowErrorMatchingInlineSnapshot( - `"version should be a string"` - ); - }); - - test(`doesn't allow an empty string`, () => { - const mockConfig = createMockConfig({ 'pkg.version': '' }); - - expect(() => actionsFactory(mockConfig)).toThrowErrorMatchingInlineSnapshot( - `"version can't be an empty string"` - ); - }); -}); - -describe('#login', () => { - test('returns login:', () => { - const version = 'mock-version'; - const mockConfig = createMockConfig({ 'pkg.version': version }); - - const actions = actionsFactory(mockConfig); - - expect(actions.login).toBe('login:'); - }); -}); - -describe('#version', () => { - test("returns `version:${config.get('pkg.version')}`", () => { - const version = 'mock-version'; - const mockConfig = createMockConfig({ 'pkg.version': version }); - - const actions = actionsFactory(mockConfig); - - expect(actions.version).toBe(`version:${version}`); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/api_authorization.test.ts b/x-pack/legacy/plugins/security/server/lib/authorization/api_authorization.test.ts deleted file mode 100644 index 00d920c2f15b2d..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/authorization/api_authorization.test.ts +++ /dev/null @@ -1,193 +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 { Server } from 'hapi'; -import { AuthorizationService } from './service'; - -import { actionsFactory } from './actions'; -import { initAPIAuthorization } from './api_authorization'; - -const actions = actionsFactory({ - get(key: string) { - if (key === 'pkg.version') { - return `1.0.0-zeta1`; - } - - throw new Error(`Unexpected config key: ${key}`); - }, -}); - -describe('initAPIAuthorization', () => { - test(`route that doesn't start with "/api/" continues`, async () => { - const server = new Server(); - initAPIAuthorization(server, {} as AuthorizationService); - server.route([ - { - method: 'GET', - path: '/app/foo', - handler: () => { - return 'foo app response'; - }, - }, - ]); - const { result, statusCode } = await server.inject({ - method: 'GET', - url: '/app/foo', - }); - expect(result).toBe('foo app response'); - expect(statusCode).toBe(200); - }); - - test(`protected route that starts with "/api/", but "mode.useRbacForRequest()" returns false continues`, async () => { - const server = new Server(); - const mockAuthorizationService: AuthorizationService = { - mode: { - useRbacForRequest: jest.fn().mockReturnValue(false), - }, - } as any; - initAPIAuthorization(server, mockAuthorizationService); - server.route([ - { - method: 'GET', - path: '/api/foo', - options: { - tags: ['access:foo'], - }, - handler: () => { - return 'foo api response'; - }, - }, - ]); - const { request, result, statusCode } = await server.inject({ - method: 'GET', - url: '/api/foo', - }); - expect(result).toBe('foo api response'); - expect(statusCode).toBe(200); - expect(mockAuthorizationService.mode.useRbacForRequest).toHaveBeenCalledWith(request); - }); - - test(`unprotected route that starts with "/api/", but "mode.useRbacForRequest()" returns true continues`, async () => { - const server = new Server(); - const mockAuthorizationService: AuthorizationService = { - mode: { - useRbacForRequest: jest.fn().mockReturnValue(true), - }, - } as any; - initAPIAuthorization(server, mockAuthorizationService); - server.route([ - { - method: 'GET', - path: '/api/foo', - options: { - tags: ['not-access:foo'], - }, - handler: () => { - return 'foo api response'; - }, - }, - ]); - const { request, result, statusCode } = await server.inject({ - method: 'GET', - url: '/api/foo', - }); - expect(result).toBe('foo api response'); - expect(statusCode).toBe(200); - expect(mockAuthorizationService.mode.useRbacForRequest).toHaveBeenCalledWith(request); - }); - - test(`protected route that starts with "/api/", "mode.useRbacForRequest()" returns true and user is authorized continues`, async () => { - const headers = { - authorization: 'foo', - }; - const server = new Server(); - const mockCheckPrivileges = jest.fn().mockReturnValue({ hasAllRequested: true }); - const mockAuthorizationService: AuthorizationService = { - actions, - checkPrivilegesDynamicallyWithRequest: (req: any) => { - // hapi conceals the actual "request" from us, so we make sure that the headers are passed to - // "checkPrivilegesDynamicallyWithRequest" because this is what we're really concerned with - expect(req.headers).toMatchObject(headers); - - return mockCheckPrivileges; - }, - mode: { - useRbacForRequest: jest.fn().mockReturnValue(true), - }, - } as any; - initAPIAuthorization(server, mockAuthorizationService); - server.route([ - { - method: 'GET', - path: '/api/foo', - options: { - tags: ['access:foo'], - }, - handler: () => { - return 'foo api response'; - }, - }, - ]); - const { request, result, statusCode } = await server.inject({ - method: 'GET', - url: '/api/foo', - headers, - }); - expect(result).toBe('foo api response'); - expect(statusCode).toBe(200); - expect(mockCheckPrivileges).toHaveBeenCalledWith([actions.api.get('foo')]); - expect(mockAuthorizationService.mode.useRbacForRequest).toHaveBeenCalledWith(request); - }); - - test(`protected route that starts with "/api/", "mode.useRbacForRequest()" returns true and user isn't authorized responds with a 404`, async () => { - const headers = { - authorization: 'foo', - }; - const server = new Server(); - const mockCheckPrivileges = jest.fn().mockReturnValue({ hasAllRequested: false }); - const mockAuthorizationService: AuthorizationService = { - actions, - checkPrivilegesDynamicallyWithRequest: (req: any) => { - // hapi conceals the actual "request" from us, so we make sure that the headers are passed to - // "checkPrivilegesDynamicallyWithRequest" because this is what we're really concerned with - expect(req.headers).toMatchObject(headers); - - return mockCheckPrivileges; - }, - mode: { - useRbacForRequest: jest.fn().mockReturnValue(true), - }, - } as any; - initAPIAuthorization(server, mockAuthorizationService); - server.route([ - { - method: 'GET', - path: '/api/foo', - options: { - tags: ['access:foo'], - }, - handler: () => { - return 'foo api response'; - }, - }, - ]); - const { request, result, statusCode } = await server.inject({ - method: 'GET', - url: '/api/foo', - headers, - }); - expect(result).toMatchInlineSnapshot(` -Object { - "error": "Not Found", - "message": "Not Found", - "statusCode": 404, -} -`); - expect(statusCode).toBe(404); - expect(mockCheckPrivileges).toHaveBeenCalledWith([actions.api.get('foo')]); - expect(mockAuthorizationService.mode.useRbacForRequest).toHaveBeenCalledWith(request); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/app_authorization.test.ts b/x-pack/legacy/plugins/security/server/lib/authorization/app_authorization.test.ts deleted file mode 100644 index 52bc6de63146a2..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/authorization/app_authorization.test.ts +++ /dev/null @@ -1,198 +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 { Server } from 'hapi'; -import { AuthorizationService } from './service'; - -import { Feature } from '../../../../../../plugins/features/server'; -import { XPackMainPlugin } from '../../../../xpack_main/xpack_main'; -import { actionsFactory } from './actions'; -import { initAppAuthorization } from './app_authorization'; - -const actions = actionsFactory({ - get(key: string) { - if (key === 'pkg.version') { - return `1.0.0-zeta1`; - } - - throw new Error(`Unexpected config key: ${key}`); - }, -}); - -const createMockXPackMainPlugin = (): XPackMainPlugin => { - const features: Feature[] = [ - { - id: 'foo', - name: 'Foo', - app: ['foo'], - privileges: {}, - }, - ]; - return { - getFeatures: () => features, - } as XPackMainPlugin; -}; - -describe('initAppAuthorization', () => { - test(`route that doesn't start with "/app/" continues`, async () => { - const server = new Server(); - initAppAuthorization(server, createMockXPackMainPlugin(), {} as AuthorizationService); - server.route([ - { - method: 'GET', - path: '/api/foo', - handler: () => { - return 'foo app response'; - }, - }, - ]); - const { result, statusCode } = await server.inject({ - method: 'GET', - url: '/api/foo', - }); - expect(result).toBe('foo app response'); - expect(statusCode).toBe(200); - }); - - test(`protected route that starts with "/app/", but "mode.useRbacForRequest()" returns false continues`, async () => { - const server = new Server(); - const mockAuthorizationService: AuthorizationService = { - mode: { - useRbacForRequest: jest.fn().mockReturnValue(false), - }, - } as any; - initAppAuthorization(server, createMockXPackMainPlugin(), mockAuthorizationService); - server.route([ - { - method: 'GET', - path: '/app/foo', - handler: () => { - return 'foo app response'; - }, - }, - ]); - const { request, result, statusCode } = await server.inject({ - method: 'GET', - url: '/app/foo', - }); - expect(result).toBe('foo app response'); - expect(statusCode).toBe(200); - expect(mockAuthorizationService.mode.useRbacForRequest).toHaveBeenCalledWith(request); - }); - - test(`unprotected route that starts with "/app/", and "mode.useRbacForRequest()" returns true continues`, async () => { - const server = new Server(); - const mockAuthorizationService: AuthorizationService = { - actions, - mode: { - useRbacForRequest: jest.fn().mockReturnValue(true), - }, - } as any; - initAppAuthorization(server, createMockXPackMainPlugin(), mockAuthorizationService); - server.route([ - { - method: 'GET', - path: '/app/bar', - handler: () => { - return 'bar app response'; - }, - }, - ]); - const { request, result, statusCode } = await server.inject({ - method: 'GET', - url: '/app/bar', - }); - expect(result).toBe('bar app response'); - expect(statusCode).toBe(200); - expect(mockAuthorizationService.mode.useRbacForRequest).toHaveBeenCalledWith(request); - }); - - test(`protected route that starts with "/app/", "mode.useRbacForRequest()" returns true and user is authorized continues`, async () => { - const headers = { - authorization: 'foo', - }; - const server = new Server(); - const mockCheckPrivileges = jest.fn().mockReturnValue({ hasAllRequested: true }); - const mockAuthorizationService: AuthorizationService = { - actions, - checkPrivilegesDynamicallyWithRequest: (req: any) => { - // hapi conceals the actual "request" from us, so we make sure that the headers are passed to - // "checkPrivilegesDynamicallyWithRequest" because this is what we're really concerned with - expect(req.headers).toMatchObject(headers); - - return mockCheckPrivileges; - }, - mode: { - useRbacForRequest: jest.fn().mockReturnValue(true), - }, - } as any; - initAppAuthorization(server, createMockXPackMainPlugin(), mockAuthorizationService); - server.route([ - { - method: 'GET', - path: '/app/foo', - handler: () => { - return 'foo app response'; - }, - }, - ]); - const { request, result, statusCode } = await server.inject({ - method: 'GET', - url: '/app/foo', - headers, - }); - expect(result).toBe('foo app response'); - expect(statusCode).toBe(200); - expect(mockCheckPrivileges).toHaveBeenCalledWith(actions.app.get('foo')); - expect(mockAuthorizationService.mode.useRbacForRequest).toHaveBeenCalledWith(request); - }); - - test(`protected route that starts with "/app/", "mode.useRbacForRequest()" returns true and user isn't authorized responds with a 404`, async () => { - const headers = { - authorization: 'foo', - }; - const server = new Server(); - const mockCheckPrivileges = jest.fn().mockReturnValue({ hasAllRequested: false }); - const mockAuthorizationService: AuthorizationService = { - actions, - checkPrivilegesDynamicallyWithRequest: (req: any) => { - // hapi conceals the actual "request" from us, so we make sure that the headers are passed to - // "checkPrivilegesDynamicallyWithRequest" because this is what we're really concerned with - expect(req.headers).toMatchObject(headers); - - return mockCheckPrivileges; - }, - mode: { - useRbacForRequest: jest.fn().mockReturnValue(true), - }, - } as any; - initAppAuthorization(server, createMockXPackMainPlugin(), mockAuthorizationService); - server.route([ - { - method: 'GET', - path: '/app/foo', - handler: () => { - return 'foo app response'; - }, - }, - ]); - const { request, result, statusCode } = await server.inject({ - method: 'GET', - url: '/app/foo', - headers, - }); - expect(result).toMatchInlineSnapshot(` -Object { - "error": "Not Found", - "message": "Not Found", - "statusCode": 404, -} -`); - expect(statusCode).toBe(404); - expect(mockCheckPrivileges).toHaveBeenCalledWith(actions.app.get('foo')); - expect(mockAuthorizationService.mode.useRbacForRequest).toHaveBeenCalledWith(request); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/index.ts b/x-pack/legacy/plugins/security/server/lib/authorization/index.ts deleted file mode 100644 index 32c05dc8a5ebcf..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/authorization/index.ts +++ /dev/null @@ -1,16 +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. - */ - -export { Actions } from './actions'; -export { createAuthorizationService } from './service'; -export { disableUICapabilitesFactory } from './disable_ui_capabilities'; -export { initAPIAuthorization } from './api_authorization'; -export { initAppAuthorization } from './app_authorization'; -export { PrivilegeSerializer } from './privilege_serializer'; -// @ts-ignore -export { registerPrivilegesWithCluster } from './register_privileges_with_cluster'; -export { ResourceSerializer } from './resource_serializer'; -export { validateFeaturePrivileges } from './validate_feature_privileges'; diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/mode.test.ts b/x-pack/legacy/plugins/security/server/lib/authorization/mode.test.ts deleted file mode 100644 index 26a10295cc1273..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/authorization/mode.test.ts +++ /dev/null @@ -1,74 +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 { requestFixture } from '../__tests__/__fixtures__/request'; -import { authorizationModeFactory } from './mode'; - -class MockXPackInfoFeature { - public getLicenseCheckResults = jest.fn(); - - constructor(allowRbac: boolean) { - this.getLicenseCheckResults.mockReturnValue({ allowRbac }); - } -} - -describe(`#useRbacForRequest`, () => { - test(`throws an Error if request isn't specified`, async () => { - const mockXpackInfoFeature = new MockXPackInfoFeature(false); - const mode = authorizationModeFactory(mockXpackInfoFeature as any); - - expect(() => mode.useRbacForRequest(undefined as any)).toThrowErrorMatchingInlineSnapshot( - `"Invalid value used as weak map key"` - ); - }); - - test(`throws an Error if request is "null"`, async () => { - const mockXpackInfoFeature = new MockXPackInfoFeature(false); - const mode = authorizationModeFactory(mockXpackInfoFeature as any); - - expect(() => mode.useRbacForRequest(null as any)).toThrowErrorMatchingInlineSnapshot( - `"Invalid value used as weak map key"` - ); - }); - - test(`returns false if xpackInfoFeature.getLicenseCheckResults().allowRbac is false`, async () => { - const mockXpackInfoFeature = new MockXPackInfoFeature(false); - const mode = authorizationModeFactory(mockXpackInfoFeature as any); - const request = requestFixture(); - - const result = mode.useRbacForRequest(request); - expect(result).toBe(false); - }); - - test(`returns false if xpackInfoFeature.getLicenseCheckResults().allowRbac is initially false, and changes to true`, async () => { - const mockXpackInfoFeature = new MockXPackInfoFeature(false); - const mode = authorizationModeFactory(mockXpackInfoFeature as any); - const request = requestFixture(); - - expect(mode.useRbacForRequest(request)).toBe(false); - mockXpackInfoFeature.getLicenseCheckResults.mockReturnValue({ allowRbac: true }); - expect(mode.useRbacForRequest(request)).toBe(false); - }); - - test(`returns true if xpackInfoFeature.getLicenseCheckResults().allowRbac is true`, async () => { - const mockXpackInfoFeature = new MockXPackInfoFeature(true); - const mode = authorizationModeFactory(mockXpackInfoFeature as any); - const request = requestFixture(); - - const result = mode.useRbacForRequest(request); - expect(result).toBe(true); - }); - - test(`returns true if xpackInfoFeature.getLicenseCheckResults().allowRbac is initially true, and changes to false`, async () => { - const mockXpackInfoFeature = new MockXPackInfoFeature(true); - const mode = authorizationModeFactory(mockXpackInfoFeature as any); - const request = requestFixture(); - - expect(mode.useRbacForRequest(request)).toBe(true); - mockXpackInfoFeature.getLicenseCheckResults.mockReturnValue({ allowRbac: false }); - expect(mode.useRbacForRequest(request)).toBe(true); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/mode.ts b/x-pack/legacy/plugins/security/server/lib/authorization/mode.ts deleted file mode 100644 index ea4d8114171302..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/authorization/mode.ts +++ /dev/null @@ -1,34 +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 { Request } from 'hapi'; -import { XPackFeature } from '../../../../xpack_main/xpack_main'; - -export interface AuthorizationMode { - useRbacForRequest(request: Request): boolean; -} - -/* - * 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 function authorizationModeFactory(securityXPackFeature: XPackFeature) { - const useRbacForRequestCache = new WeakMap(); - - return { - useRbacForRequest(request: Request) { - if (!useRbacForRequestCache.has(request)) { - useRbacForRequestCache.set( - request, - Boolean(securityXPackFeature.getLicenseCheckResults().allowRbac) - ); - } - - return useRbacForRequestCache.get(request); - }, - }; -} diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/register_privileges_with_cluster.js b/x-pack/legacy/plugins/security/server/lib/authorization/register_privileges_with_cluster.js deleted file mode 100644 index 0150913d1b62b4..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/authorization/register_privileges_with_cluster.js +++ /dev/null @@ -1,73 +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 { difference, isEmpty, isEqual } from 'lodash'; -import { getClient } from '../../../../../server/lib/get_client_shield'; -import { serializePrivileges } from './privileges_serializer'; - -export async function registerPrivilegesWithCluster(server) { - - const { application, privileges } = server.plugins.security.authorization; - - const arePrivilegesEqual = (existingPrivileges, expectedPrivileges) => { - // when comparing privileges, the order of the actions doesn't matter, lodash's isEqual - // doesn't know how to compare Sets - return isEqual(existingPrivileges, expectedPrivileges, (value, other, key) => { - if (key === 'actions' && Array.isArray(value) && Array.isArray(other)) { - // Array.sort() is in-place, and we don't want to be modifying the actual order - // of the arrays permanently, and there's potential they're frozen, so we're copying - // before comparing. - return isEqual([...value].sort(), [...other].sort()); - } - }); - }; - - const getPrivilegesToDelete = (existingPrivileges, expectedPrivileges) => { - if (isEmpty(existingPrivileges)) { - return []; - } - - return difference(Object.keys(existingPrivileges[application]), Object.keys(expectedPrivileges[application])); - }; - - const expectedPrivileges = serializePrivileges(application, privileges.get()); - - server.log(['security', 'debug'], `Registering Kibana Privileges with Elasticsearch for ${application}`); - - const callCluster = getClient(server).callWithInternalUser; - - try { - // we only want to post the privileges when they're going to change as Elasticsearch has - // to clear the role cache to get these changes reflected in the _has_privileges API - const existingPrivileges = await callCluster(`shield.getPrivilege`, { privilege: application }); - if (arePrivilegesEqual(existingPrivileges, expectedPrivileges)) { - server.log(['security', 'debug'], `Kibana Privileges already registered with Elasticearch for ${application}`); - return; - } - - const privilegesToDelete = getPrivilegesToDelete(existingPrivileges, expectedPrivileges); - for (const privilegeToDelete of privilegesToDelete) { - server.log(['security', 'debug'], `Deleting Kibana Privilege ${privilegeToDelete} from Elasticearch for ${application}`); - try { - await callCluster('shield.deletePrivilege', { - application, - privilege: privilegeToDelete - }); - } catch (err) { - server.log(['security', 'error'], `Error deleting Kibana Privilege ${privilegeToDelete}`); - throw err; - } - } - - await callCluster('shield.postPrivileges', { - body: expectedPrivileges - }); - server.log(['security', 'debug'], `Updated Kibana Privileges with Elasticearch for ${application}`); - } catch (err) { - server.log(['security', 'error'], `Error registering Kibana Privileges with Elasticsearch for ${application}: ${err.message}`); - throw err; - } -} diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/service.test.ts b/x-pack/legacy/plugins/security/server/lib/authorization/service.test.ts deleted file mode 100644 index a4c733a7e97170..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/authorization/service.test.ts +++ /dev/null @@ -1,112 +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 { - mockActionsFactory, - mockAuthorizationModeFactory, - mockCheckPrivilegesDynamicallyWithRequestFactory, - mockCheckPrivilegesWithRequestFactory, - mockCheckSavedObjectsPrivilegesWithRequestFactory, - mockGetClient, - mockPrivilegesFactory, -} from './service.test.mocks'; - -import { getClient } from '../../../../../server/lib/get_client_shield'; -import { actionsFactory } from './actions'; -import { checkPrivilegesWithRequestFactory } from './check_privileges'; -import { checkPrivilegesDynamicallyWithRequestFactory } from './check_privileges_dynamically'; -import { checkSavedObjectsPrivilegesWithRequestFactory } from './check_saved_objects_privileges'; -import { authorizationModeFactory } from './mode'; -import { privilegesFactory } from './privileges'; -import { createAuthorizationService } from './service'; - -const createMockConfig = (settings: Record = {}) => { - const mockConfig = { - get: jest.fn(), - }; - - mockConfig.get.mockImplementation((key: string) => settings[key]); - - return mockConfig; -}; - -test(`returns exposed services`, () => { - const kibanaIndex = '.a-kibana-index'; - const mockConfig = createMockConfig({ - 'kibana.index': kibanaIndex, - }); - const mockServer = { - expose: jest.fn(), - config: jest.fn().mockReturnValue(mockConfig), - plugins: Symbol(), - savedObjects: Symbol(), - log: Symbol(), - }; - const mockShieldClient = Symbol(); - mockGetClient.mockReturnValue(mockShieldClient); - - const mockCheckPrivilegesWithRequest = Symbol(); - mockCheckPrivilegesWithRequestFactory.mockReturnValue(mockCheckPrivilegesWithRequest); - - const mockCheckPrivilegesDynamicallyWithRequest = Symbol(); - mockCheckPrivilegesDynamicallyWithRequestFactory.mockReturnValue( - mockCheckPrivilegesDynamicallyWithRequest - ); - - const mockCheckSavedObjectsPrivilegesWithRequest = Symbol(); - mockCheckSavedObjectsPrivilegesWithRequestFactory.mockReturnValue( - mockCheckSavedObjectsPrivilegesWithRequest - ); - - const mockActions = Symbol(); - mockActionsFactory.mockReturnValue(mockActions); - const mockXpackInfoFeature = Symbol(); - const mockFeatures = Symbol(); - const mockXpackMainPlugin = { - getFeatures: () => mockFeatures, - }; - const mockPrivilegesService = Symbol(); - mockPrivilegesFactory.mockReturnValue(mockPrivilegesService); - const mockAuthorizationMode = Symbol(); - mockAuthorizationModeFactory.mockReturnValue(mockAuthorizationMode); - const mockSpaces = Symbol(); - - const authorization = createAuthorizationService( - mockServer as any, - mockXpackInfoFeature as any, - mockXpackMainPlugin as any, - mockSpaces as any - ); - - const application = `kibana-${kibanaIndex}`; - expect(getClient).toHaveBeenCalledWith(mockServer); - expect(actionsFactory).toHaveBeenCalledWith(mockConfig); - expect(checkPrivilegesWithRequestFactory).toHaveBeenCalledWith( - mockActions, - application, - mockShieldClient - ); - expect(checkPrivilegesDynamicallyWithRequestFactory).toHaveBeenCalledWith( - mockCheckPrivilegesWithRequest, - mockSpaces - ); - expect(checkSavedObjectsPrivilegesWithRequestFactory).toHaveBeenCalledWith( - mockCheckPrivilegesWithRequest, - mockSpaces - ); - expect(privilegesFactory).toHaveBeenCalledWith(mockActions, mockXpackMainPlugin); - expect(authorizationModeFactory).toHaveBeenCalledWith(mockXpackInfoFeature); - - expect(authorization).toEqual({ - actions: mockActions, - application, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - mode: mockAuthorizationMode, - privileges: mockPrivilegesService, - }); -}); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/service.ts b/x-pack/legacy/plugins/security/server/lib/authorization/service.ts deleted file mode 100644 index 3d248adb9f8b86..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/authorization/service.ts +++ /dev/null @@ -1,75 +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 { Server } from 'hapi'; - -import { getClient } from '../../../../../server/lib/get_client_shield'; -import { LegacySpacesPlugin } from '../../../../spaces'; -import { XPackFeature, XPackMainPlugin } from '../../../../xpack_main/xpack_main'; -import { APPLICATION_PREFIX } from '../../../common/constants'; -import { OptionalPlugin } from '../../../../../server/lib/optional_plugin'; -import { Actions, actionsFactory } from './actions'; -import { CheckPrivilegesWithRequest, checkPrivilegesWithRequestFactory } from './check_privileges'; -import { - CheckPrivilegesDynamicallyWithRequest, - checkPrivilegesDynamicallyWithRequestFactory, -} from './check_privileges_dynamically'; -import { AuthorizationMode, authorizationModeFactory } from './mode'; -import { privilegesFactory, PrivilegesService } from './privileges'; -import { - CheckSavedObjectsPrivilegesWithRequest, - checkSavedObjectsPrivilegesWithRequestFactory, -} from './check_saved_objects_privileges'; - -export interface AuthorizationService { - actions: Actions; - application: string; - checkPrivilegesWithRequest: CheckPrivilegesWithRequest; - checkPrivilegesDynamicallyWithRequest: CheckPrivilegesDynamicallyWithRequest; - checkSavedObjectsPrivilegesWithRequest: CheckSavedObjectsPrivilegesWithRequest; - mode: AuthorizationMode; - privileges: PrivilegesService; -} - -export function createAuthorizationService( - server: Server, - securityXPackFeature: XPackFeature, - xpackMainPlugin: XPackMainPlugin, - spaces: OptionalPlugin -): AuthorizationService { - const shieldClient = getClient(server); - const config = server.config(); - - const actions = actionsFactory(config); - const application = `${APPLICATION_PREFIX}${config.get('kibana.index')}`; - const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( - actions, - application, - shieldClient - ); - const checkPrivilegesDynamicallyWithRequest = checkPrivilegesDynamicallyWithRequestFactory( - checkPrivilegesWithRequest, - spaces - ); - - const checkSavedObjectsPrivilegesWithRequest = checkSavedObjectsPrivilegesWithRequestFactory( - checkPrivilegesWithRequest, - spaces - ); - - const mode = authorizationModeFactory(securityXPackFeature); - const privileges = privilegesFactory(actions, xpackMainPlugin); - - return { - actions, - application, - checkPrivilegesWithRequest, - checkPrivilegesDynamicallyWithRequest, - checkSavedObjectsPrivilegesWithRequest, - mode, - privileges, - }; -} diff --git a/x-pack/legacy/plugins/security/server/lib/check_license.js b/x-pack/legacy/plugins/security/server/lib/check_license.js deleted file mode 100644 index 2a6650e9e2b0e7..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/check_license.js +++ /dev/null @@ -1,63 +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. - */ - -/** - * @typedef {Object} LicenseCheckResult Result of the license check. - * @property {boolean} showLogin Indicates whether we show login page or skip it. - * @property {boolean} allowLogin Indicates whether we allow login or disable it on the login page. - * @property {boolean} showLinks Indicates whether we show security links throughout the kibana app. - * @property {boolean} allowRoleDocumentLevelSecurity Indicates whether we allow users to define document level - * security in roles. - * @property {boolean} allowRoleFieldLevelSecurity Indicates whether we allow users to define field level security - * in roles - * @property {string} [linksMessage] Message to show when security links are clicked throughout the kibana app. - */ - -/** - * Returns object that defines behavior of the security related areas (login page, user management etc.) based - * on the license information extracted from the xPackInfo. - * @param {XPackInfo} xPackInfo XPackInfo instance to extract license information from. - * @returns {LicenseCheckResult} - */ -export function checkLicense(xPackInfo) { - // If, for some reason, we cannot get license information from Elasticsearch, - // assume worst-case and lock user at login screen. - if (!xPackInfo.isAvailable()) { - return { - showLogin: true, - allowLogin: false, - showLinks: false, - allowRoleDocumentLevelSecurity: false, - allowRoleFieldLevelSecurity: false, - allowRbac: false, - layout: xPackInfo.isXpackUnavailable() ? 'error-xpack-unavailable' : 'error-es-unavailable' - }; - } - - const isEnabledInES = xPackInfo.feature('security').isEnabled(); - if (!isEnabledInES) { - return { - showLogin: false, - allowLogin: false, - showLinks: false, - allowRoleDocumentLevelSecurity: false, - allowRoleFieldLevelSecurity: false, - allowRbac: false, - linksMessage: 'Access is denied because Security is disabled in Elasticsearch.' - }; - } - - const isLicensePlatinumOrTrial = xPackInfo.license.isOneOf(['platinum', 'trial']); - return { - showLogin: true, - allowLogin: true, - showLinks: true, - // Only platinum and trial licenses are compliant with field- and document-level security. - allowRoleDocumentLevelSecurity: isLicensePlatinumOrTrial, - allowRoleFieldLevelSecurity: isLicensePlatinumOrTrial, - allowRbac: true, - }; -} diff --git a/x-pack/legacy/plugins/security/server/lib/deep_freeze.js b/x-pack/legacy/plugins/security/server/lib/deep_freeze.js deleted file mode 100644 index 0f9363cb410f6c..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/deep_freeze.js +++ /dev/null @@ -1,20 +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 { isObject } from 'lodash'; - -export function deepFreeze(object) { - // for any properties that reference an object, makes sure that object is - // recursively frozen as well - Object.keys(object).forEach(key => { - const value = object[key]; - if (isObject(value)) { - deepFreeze(value); - } - }); - - return Object.freeze(object); -} diff --git a/x-pack/legacy/plugins/security/server/lib/deep_freeze.test.js b/x-pack/legacy/plugins/security/server/lib/deep_freeze.test.js deleted file mode 100644 index dd227fa6269bff..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/deep_freeze.test.js +++ /dev/null @@ -1,97 +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 { deepFreeze } from './deep_freeze'; - -test(`freezes result and input`, () => { - const input = {}; - const result = deepFreeze(input); - - Object.isFrozen(input); - Object.isFrozen(result); -}); - -test(`freezes top-level properties that are objects`, () => { - const result = deepFreeze({ - object: {}, - array: [], - fn: () => {}, - number: 1, - string: '', - }); - - Object.isFrozen(result.object); - Object.isFrozen(result.array); - Object.isFrozen(result.fn); - Object.isFrozen(result.number); - Object.isFrozen(result.string); -}); - -test(`freezes child properties that are objects`, () => { - const result = deepFreeze({ - object: { - object: { - }, - array: [], - fn: () => {}, - number: 1, - string: '', - }, - array: [ - {}, - [], - () => {}, - 1, - '', - ], - }); - - Object.isFrozen(result.object.object); - Object.isFrozen(result.object.array); - Object.isFrozen(result.object.fn); - Object.isFrozen(result.object.number); - Object.isFrozen(result.object.string); - Object.isFrozen(result.array[0]); - Object.isFrozen(result.array[1]); - Object.isFrozen(result.array[2]); - Object.isFrozen(result.array[3]); - Object.isFrozen(result.array[4]); -}); - -test(`freezes grand-child properties that are objects`, () => { - const result = deepFreeze({ - object: { - object: { - object: { - }, - array: [], - fn: () => {}, - number: 1, - string: '', - }, - }, - array: [ - [ - {}, - [], - () => {}, - 1, - '', - ], - ], - }); - - Object.isFrozen(result.object.object.object); - Object.isFrozen(result.object.object.array); - Object.isFrozen(result.object.object.fn); - Object.isFrozen(result.object.object.number); - Object.isFrozen(result.object.object.string); - Object.isFrozen(result.array[0][0]); - Object.isFrozen(result.array[0][1]); - Object.isFrozen(result.array[0][2]); - Object.isFrozen(result.array[0][3]); - Object.isFrozen(result.array[0][4]); -}); diff --git a/x-pack/legacy/plugins/security/server/lib/route_pre_check_license.js b/x-pack/legacy/plugins/security/server/lib/route_pre_check_license.js index 41db792b33d943..64816bf4d23d70 100644 --- a/x-pack/legacy/plugins/security/server/lib/route_pre_check_license.js +++ b/x-pack/legacy/plugins/security/server/lib/route_pre_check_license.js @@ -7,10 +7,8 @@ const Boom = require('boom'); export function routePreCheckLicense(server) { - const xpackMainPlugin = server.plugins.xpack_main; - const pluginId = 'security'; return function forbidApiAccess() { - const licenseCheckResults = xpackMainPlugin.info.feature(pluginId).getLicenseCheckResults(); + const licenseCheckResults = server.newPlatform.setup.plugins.security.__legacyCompat.license.getFeatures(); if (!licenseCheckResults.showLinks) { throw Boom.forbidden(licenseCheckResults.linksMessage); } else { diff --git a/x-pack/legacy/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.js b/x-pack/legacy/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.js deleted file mode 100644 index d45e42e430a0bd..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.js +++ /dev/null @@ -1,154 +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 { get, uniq } from 'lodash'; - -export class SecureSavedObjectsClientWrapper { - constructor(options) { - const { - actions, - auditLogger, - baseClient, - checkSavedObjectsPrivilegesWithRequest, - errors, - request, - savedObjectTypes, - } = options; - - this.errors = errors; - this._actions = actions; - this._auditLogger = auditLogger; - this._baseClient = baseClient; - this._checkSavedObjectsPrivileges = checkSavedObjectsPrivilegesWithRequest(request); - this._savedObjectTypes = savedObjectTypes; - } - - async create(type, attributes = {}, options = {}) { - await this._ensureAuthorized( - type, - 'create', - options.namespace, - { type, attributes, options }, - ); - - return await this._baseClient.create(type, attributes, options); - } - - async bulkCreate(objects, options = {}) { - const types = uniq(objects.map(o => o.type)); - await this._ensureAuthorized( - types, - 'bulk_create', - options.namespace, - { objects, options }, - ); - - return await this._baseClient.bulkCreate(objects, options); - } - - async delete(type, id, options = {}) { - await this._ensureAuthorized( - type, - 'delete', - options.namespace, - { type, id, options }, - ); - - return await this._baseClient.delete(type, id, options); - } - - async find(options = {}) { - await this._ensureAuthorized( - options.type, - 'find', - options.namespace, - { options } - ); - - return this._baseClient.find(options); - } - - async bulkGet(objects = [], options = {}) { - const types = uniq(objects.map(o => o.type)); - await this._ensureAuthorized( - types, - 'bulk_get', - options.namespace, - { objects, options }, - ); - - return await this._baseClient.bulkGet(objects, options); - } - - async get(type, id, options = {}) { - await this._ensureAuthorized( - type, - 'get', - options.namespace, - { type, id, options }, - ); - - return await this._baseClient.get(type, id, options); - } - - async update(type, id, attributes, options = {}) { - await this._ensureAuthorized( - type, - 'update', - options.namespace, - { type, id, attributes, options }, - ); - - return await this._baseClient.update(type, id, attributes, options); - } - - async bulkUpdate(objects = [], options) { - const types = uniq(objects.map(o => o.type)); - await this._ensureAuthorized( - types, - 'bulk_update', - options && options.namespace, - { objects, options }, - ); - - return await this._baseClient.bulkUpdate(objects, options); - } - - async _checkPrivileges(actions, namespace) { - try { - return await this._checkSavedObjectsPrivileges(actions, namespace); - } catch (error) { - const { reason } = get(error, 'body.error', {}); - throw this.errors.decorateGeneralError(error, reason); - } - } - - async _ensureAuthorized(typeOrTypes, action, namespace, args) { - const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; - const actionsToTypesMap = new Map(types.map(type => [this._actions.savedObject.get(type, action), type])); - const actions = Array.from(actionsToTypesMap.keys()); - const { hasAllRequested, username, privileges } = await this._checkPrivileges(actions, namespace); - - if (hasAllRequested) { - this._auditLogger.savedObjectsAuthorizationSuccess(username, action, types, args); - } else { - const missingPrivileges = this._getMissingPrivileges(privileges); - this._auditLogger.savedObjectsAuthorizationFailure( - username, - action, - types, - missingPrivileges, - args - ); - const msg = `Unable to ${action} ${missingPrivileges.map(privilege => actionsToTypesMap.get(privilege)).sort().join(',')}`; - throw this.errors.decorateForbiddenError(new Error(msg)); - } - } - - _getMissingPrivileges(response) { - return Object.keys(response).filter(privilege => !response[privilege]); - } -} diff --git a/x-pack/legacy/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.test.js b/x-pack/legacy/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.test.js deleted file mode 100644 index 8bc1aa0fbe2f8f..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.test.js +++ /dev/null @@ -1,1139 +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 { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper'; - -const createMockErrors = () => { - const forbiddenError = new Error('Mock ForbiddenError'); - const generalError = new Error('Mock GeneralError'); - - return { - forbiddenError, - decorateForbiddenError: jest.fn().mockReturnValue(forbiddenError), - generalError, - decorateGeneralError: jest.fn().mockReturnValue(generalError) - }; -}; - -const createMockAuditLogger = () => { - return { - savedObjectsAuthorizationFailure: jest.fn(), - savedObjectsAuthorizationSuccess: jest.fn(), - }; -}; - -const createMockActions = () => { - return { - savedObject: { - get(type, action) { - return `mock-saved_object:${type}/${action}`; - } - } - }; -}; - -describe('#errors', () => { - test(`assigns errors from constructor to .errors`, () => { - const errors = Symbol(); - - const client = new SecureSavedObjectsClientWrapper({ - checkSavedObjectsPrivilegesWithRequest: () => {}, - errors - }); - - expect(client.errors).toBe(errors); - }); -}); - -describe(`spaces disabled`, () => { - describe('#create', () => { - test(`throws decorated GeneralError when checkPrivileges.globally rejects promise`, async () => { - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckSavedObjectsPrivileges = jest.fn(async () => { - throw new Error('An actual error would happen here'); - }); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckSavedObjectsPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - - await expect(client.create(type)).rejects.toThrowError(mockErrors.generalError); - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckSavedObjectsPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'create')], undefined); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const mockActions = createMockActions(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type, 'create')]: false, - } - })); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - const attributes = Symbol(); - const options = Object.freeze({ namespace: Symbol() }); - - await expect(client.create(type, attributes, options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'create')], options.namespace); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'create', - [type], - [mockActions.savedObject.get(type, 'create')], - { - type, - attributes, - options, - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.create when authorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockActions = createMockActions(); - const mockBaseClient = { - create: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type, 'create')]: true, - } - })); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: mockBaseClient, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: null, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - const attributes = Symbol(); - const options = Object.freeze({ namespace: Symbol() }); - - const result = await client.create(type, attributes, options); - - expect(result).toBe(returnValue); - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'create')], options.namespace); - expect(mockBaseClient.create).toHaveBeenCalledWith(type, attributes, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'create', [type], { - type, - attributes, - options, - }); - }); - }); - - describe('#bulkCreate', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn(async () => { - throw new Error('An actual error would happen here'); - }); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - - const options = Object.freeze({ namespace: Symbol() }); - - await expect(client.bulkCreate([{ type }], options)).rejects.toThrowError(mockErrors.generalError); - - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'bulk_create')], options.namespace); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const mockActions = createMockActions(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type1, 'bulk_create')]: false, - [mockActions.savedObject.get(type2, 'bulk_create')]: true, - } - })); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - const objects = [ - { type: type1 }, - { type: type1 }, - { type: type2 }, - ]; - const options = Object.freeze({ namespace: Symbol() }); - - await expect(client.bulkCreate(objects, options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([ - mockActions.savedObject.get(type1, 'bulk_create'), - mockActions.savedObject.get(type2, 'bulk_create'), - ], options.namespace); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'bulk_create', - [type1, type2], - [mockActions.savedObject.get(type1, 'bulk_create')], - { - objects, - options, - } - ); - }); - - test(`returns result of baseClient.bulkCreate when authorized`, async () => { - const username = Symbol(); - const type1 = 'foo'; - const type2 = 'bar'; - const returnValue = Symbol(); - const mockBaseClient = { - bulkCreate: jest.fn().mockReturnValue(returnValue) - }; - const mockActions = createMockActions(); - const mockCheckPrivileges = jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type1, 'bulk_create')]: true, - [mockActions.savedObject.get(type2, 'bulk_create')]: true, - } - })); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: mockBaseClient, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: null, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - const objects = [ - { type: type1, otherThing: 'sup' }, - { type: type2, otherThing: 'everyone' }, - ]; - const options = Object.freeze({ namespace: Symbol() }); - - const result = await client.bulkCreate(objects, options); - - expect(result).toBe(returnValue); - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([ - mockActions.savedObject.get(type1, 'bulk_create'), - mockActions.savedObject.get(type2, 'bulk_create'), - ], options.namespace); - expect(mockBaseClient.bulkCreate).toHaveBeenCalledWith(objects, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'bulk_create', [type1, type2], { - objects, - options, - }); - }); - }); - - describe('#delete', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn(async () => { - throw new Error('An actual error would happen here'); - }); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - - await expect(client.delete(type)).rejects.toThrowError(mockErrors.generalError); - - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'delete')], undefined); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const mockActions = createMockActions(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type, 'delete')]: false, - } - })); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - const id = Symbol(); - - const options = Object.freeze({ namespace: Symbol() }); - - await expect(client.delete(type, id, options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'delete')], options.namespace); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'delete', - [type], - [mockActions.savedObject.get(type, 'delete')], - { - type, - id, - options, - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of internalRepository.delete when authorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockActions = createMockActions(); - const mockBaseClient = { - delete: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type, 'delete')]: true, - } - })); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: mockBaseClient, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: null, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - const id = Symbol(); - const options = Object.freeze({ namespace: Symbol() }); - - const result = await client.delete(type, id, options); - - expect(result).toBe(returnValue); - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'delete')], options.namespace); - expect(mockBaseClient.delete).toHaveBeenCalledWith(type, id, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'delete', [type], { - type, - id, - options, - }); - }); - }); - - describe('#find', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn(async () => { - throw new Error('An actual error would happen here'); - }); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - - await expect(client.find({ type })).rejects.toThrowError(mockErrors.generalError); - - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'find')], undefined); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when type's singular and unauthorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const mockActions = createMockActions(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type, 'find')]: false, - } - })); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - const options = Object.freeze({ type, namespace: Symbol }); - - await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'find')], options.namespace); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'find', - [type], - [mockActions.savedObject.get(type, 'find')], - { - options - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when type's an array and unauthorized`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const mockActions = createMockActions(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type1, 'find')]: false, - [mockActions.savedObject.get(type2, 'find')]: true, - } - })); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - const options = Object.freeze({ type: [type1, type2], namespace: Symbol() }); - - await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([ - mockActions.savedObject.get(type1, 'find'), - mockActions.savedObject.get(type2, 'find') - ], options.namespace); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'find', - [type1, type2], - [mockActions.savedObject.get(type1, 'find')], - { - options - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.find when authorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockActions = createMockActions(); - const mockBaseClient = { - find: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type, 'find')]: true, - } - })); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: mockBaseClient, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: null, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - const options = Object.freeze({ type, namespace: Symbol }); - - const result = await client.find(options); - - expect(result).toBe(returnValue); - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'find')], options.namespace); - expect(mockBaseClient.find).toHaveBeenCalledWith(options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'find', [type], { - options, - }); - }); - }); - - describe('#bulkGet', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn(async () => { - throw new Error('An actual error would happen here'); - }); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - - await expect(client.bulkGet([{ type }])).rejects.toThrowError(mockErrors.generalError); - - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'bulk_get')], undefined); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const mockActions = createMockActions(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type1, 'bulk_get')]: false, - [mockActions.savedObject.get(type2, 'bulk_get')]: true, - } - })); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - const objects = [ - { type: type1 }, - { type: type1 }, - { type: type2 }, - ]; - const options = Object.freeze({ namespace: Symbol }); - - await expect(client.bulkGet(objects, options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([ - mockActions.savedObject.get(type1, 'bulk_get'), - mockActions.savedObject.get(type2, 'bulk_get'), - ], options.namespace); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'bulk_get', - [type1, type2], - [mockActions.savedObject.get(type1, 'bulk_get')], - { - objects, - options, - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.bulkGet when authorized`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const returnValue = Symbol(); - const mockActions = createMockActions(); - const mockBaseClient = { - bulkGet: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type1, 'bulk_get')]: true, - [mockActions.savedObject.get(type2, 'bulk_get')]: true, - } - })); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: mockBaseClient, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: null, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - const objects = [ - { type: type1, id: 'foo-id' }, - { type: type2, id: 'bar-id' }, - ]; - const options = Object.freeze({ namespace: Symbol }); - - const result = await client.bulkGet(objects, options); - - expect(result).toBe(returnValue); - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([ - mockActions.savedObject.get(type1, 'bulk_get'), - mockActions.savedObject.get(type2, 'bulk_get'), - ], options.namespace); - expect(mockBaseClient.bulkGet).toHaveBeenCalledWith(objects, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'bulk_get', [type1, type2], { - objects, - options, - }); - }); - }); - - describe('#get', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn(async () => { - throw new Error('An actual error would happen here'); - }); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - - await expect(client.get(type)).rejects.toThrowError(mockErrors.generalError); - - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'get')], undefined); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const mockActions = createMockActions(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type, 'get')]: false, - } - })); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - const id = Symbol(); - const options = Object.freeze({ namespace: Symbol }); - - await expect(client.get(type, id, options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'get')], options.namespace); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'get', - [type], - [mockActions.savedObject.get(type, 'get')], - { - type, - id, - options, - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.get when authorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockActions = createMockActions(); - const mockBaseClient = { - get: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type, 'get')]: true, - } - })); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: mockBaseClient, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: null, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - const id = Symbol(); - const options = Object.freeze({ namespace: Symbol }); - - const result = await client.get(type, id, options); - - expect(result).toBe(returnValue); - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'get')], options.namespace); - expect(mockBaseClient.get).toHaveBeenCalledWith(type, id, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'get', [type], { - type, - id, - options - }); - }); - }); - - describe('#update', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn(async () => { - throw new Error('An actual error would happen here'); - }); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - - await expect(client.update(type)).rejects.toThrowError(mockErrors.generalError); - - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'update')], undefined); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const mockActions = createMockActions(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type, 'update')]: false, - } - })); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - const id = Symbol(); - const attributes = Symbol(); - const options = Object.freeze({ namespace: Symbol }); - - await expect(client.update(type, id, attributes, options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'update')], options.namespace); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'update', - [type], - [mockActions.savedObject.get(type, 'update')], - { - type, - id, - attributes, - options, - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.update when authorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockActions = createMockActions(); - const mockBaseClient = { - update: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type, 'update')]: true, - } - })); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: mockBaseClient, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: null, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - const id = Symbol(); - const attributes = Symbol(); - const options = Object.freeze({ namespace: Symbol }); - - const result = await client.update(type, id, attributes, options); - - expect(result).toBe(returnValue); - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'update')], options.namespace); - expect(mockBaseClient.update).toHaveBeenCalledWith(type, id, attributes, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'update', [type], { - type, - id, - attributes, - options, - }); - }); - }); - - describe('#bulkUpdate', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn(async () => { - throw new Error('An actual error would happen here'); - }); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - - const objects = [{ - type - }]; - await expect( - client.bulkUpdate(objects) - ).rejects.toThrowError(mockErrors.generalError); - - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'bulk_update')], undefined); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const mockActions = createMockActions(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type, 'bulk_update')]: false, - } - })); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - const id = Symbol(); - const attributes = Symbol(); - const namespace = Symbol(); - - await expect( - client.bulkUpdate([{ type, id, attributes }], { namespace }) - ).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'bulk_update')], namespace); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'bulk_update', - [type], - [mockActions.savedObject.get(type, 'bulk_update')], - { - objects: [ - { - type, - id, - attributes, - } - ], - options: { namespace } - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.bulkUpdate when authorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockActions = createMockActions(); - const mockBaseClient = { - bulkUpdate: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type, 'bulkUpdate')]: true, - } - })); - const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: mockBaseClient, - checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest, - errors: null, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - const id = Symbol(); - const attributes = Symbol(); - const namespace = Symbol(); - - const result = await client.bulkUpdate([{ type, id, attributes }], { namespace }); - - expect(result).toBe(returnValue); - expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'bulk_update')], namespace); - expect(mockBaseClient.bulkUpdate).toHaveBeenCalledWith([{ type, id, attributes }], { namespace }); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'bulk_update', [type], { - objects: [{ - type, - id, - attributes, - }], - options: { namespace } - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/external/privileges/get.test.ts b/x-pack/legacy/plugins/security/server/routes/api/external/privileges/get.test.ts deleted file mode 100644 index 16a1b0f7e35a5f..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/external/privileges/get.test.ts +++ /dev/null @@ -1,129 +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 Boom from 'boom'; -import { Server } from 'hapi'; -import { RawKibanaPrivileges } from '../../../../../common/model'; -import { initGetPrivilegesApi } from './get'; -import { AuthorizationService } from '../../../../lib/authorization/service'; - -const createRawKibanaPrivileges: () => RawKibanaPrivileges = () => { - return { - features: { - feature1: { - all: ['action1'], - }, - feature2: { - all: ['action2'], - }, - }, - space: { - all: ['space*'], - read: ['space:read'], - }, - global: { - all: ['*'], - read: ['something:/read'], - }, - reserved: { - customApplication1: ['custom-action1'], - customApplication2: ['custom-action2'], - }, - }; -}; - -const createMockServer = () => { - const mockServer = new Server({ debug: false, port: 8080 }); - - mockServer.plugins.security = { - authorization: ({ - privileges: { - get: jest.fn().mockImplementation(() => { - return createRawKibanaPrivileges(); - }), - }, - } as unknown) as AuthorizationService, - } as any; - return mockServer; -}; - -interface TestOptions { - preCheckLicenseImpl?: () => void; - includeActions?: boolean; - asserts: { - statusCode: number; - result: Record; - }; -} - -describe('GET privileges', () => { - const getPrivilegesTest = ( - description: string, - { preCheckLicenseImpl = () => null, includeActions, asserts }: TestOptions - ) => { - test(description, async () => { - const mockServer = createMockServer(); - const pre = jest.fn().mockImplementation(preCheckLicenseImpl); - - initGetPrivilegesApi(mockServer, pre); - const headers = { - authorization: 'foo', - }; - - const url = `/api/security/privileges${includeActions ? '?includeActions=true' : ''}`; - - const request = { - method: 'GET', - url, - headers, - }; - const { result, statusCode } = await mockServer.inject(request); - - expect(pre).toHaveBeenCalled(); - expect(statusCode).toBe(asserts.statusCode); - expect(result).toEqual(asserts.result); - }); - }; - - describe('failure', () => { - getPrivilegesTest(`returns result of routePreCheckLicense`, { - preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'), - asserts: { - statusCode: 403, - result: { - error: 'Forbidden', - statusCode: 403, - message: 'test forbidden message', - }, - }, - }); - }); - - describe('success', () => { - getPrivilegesTest(`returns registered application privileges with actions when requested`, { - includeActions: true, - asserts: { - statusCode: 200, - result: createRawKibanaPrivileges(), - }, - }); - - getPrivilegesTest(`returns registered application privileges without actions`, { - includeActions: false, - asserts: { - statusCode: 200, - result: { - global: ['all', 'read'], - space: ['all', 'read'], - features: { - feature1: ['all'], - feature2: ['all'], - }, - reserved: ['customApplication1', 'customApplication2'], - }, - }, - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/external/privileges/get.ts b/x-pack/legacy/plugins/security/server/routes/api/external/privileges/get.ts deleted file mode 100644 index 273af1b3f0eb97..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/external/privileges/get.ts +++ /dev/null @@ -1,48 +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 Joi from 'joi'; -import { RawKibanaPrivileges } from '../../../../../common/model'; - -export function initGetPrivilegesApi( - server: Record, - routePreCheckLicenseFn: () => void -) { - server.route({ - method: 'GET', - path: '/api/security/privileges', - handler(req: Record) { - const { authorization } = server.plugins.security; - const privileges: RawKibanaPrivileges = authorization.privileges.get(); - - if (req.query.includeActions) { - return privileges; - } - - return { - global: Object.keys(privileges.global), - space: Object.keys(privileges.space), - features: Object.entries(privileges.features).reduce( - (acc, [featureId, featurePrivileges]) => { - return { - ...acc, - [featureId]: Object.keys(featurePrivileges), - }; - }, - {} - ), - reserved: Object.keys(privileges.reserved), - }; - }, - config: { - pre: [routePreCheckLicenseFn], - validate: { - query: Joi.object().keys({ - includeActions: Joi.bool(), - }), - }, - }, - }); -} diff --git a/x-pack/legacy/plugins/security/server/routes/api/external/privileges/index.ts b/x-pack/legacy/plugins/security/server/routes/api/external/privileges/index.ts deleted file mode 100644 index 2af1f99ef7f54b..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/external/privileges/index.ts +++ /dev/null @@ -1,14 +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. - */ -// @ts-ignore -import { routePreCheckLicense } from '../../../../lib/route_pre_check_license'; -import { initGetPrivilegesApi } from './get'; - -export function initPrivilegesApi(server: Record) { - const routePreCheckLicenseFn = routePreCheckLicense(server); - - initGetPrivilegesApi(server, routePreCheckLicenseFn); -} diff --git a/x-pack/legacy/plugins/security/server/routes/api/external/roles/delete.js b/x-pack/legacy/plugins/security/server/routes/api/external/roles/delete.js deleted file mode 100644 index 8568321ba19414..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/external/roles/delete.js +++ /dev/null @@ -1,33 +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 Joi from 'joi'; -import { wrapError } from '../../../../../../../../plugins/security/server'; - -export function initDeleteRolesApi(server, callWithRequest, routePreCheckLicenseFn) { - server.route({ - method: 'DELETE', - path: '/api/security/role/{name}', - handler(request, h) { - const { name } = request.params; - return callWithRequest(request, 'shield.deleteRole', { name }).then( - () => h.response().code(204), - wrapError - ); - }, - config: { - validate: { - params: Joi.object() - .keys({ - name: Joi.string() - .required(), - }) - .required(), - }, - pre: [routePreCheckLicenseFn] - } - }); -} diff --git a/x-pack/legacy/plugins/security/server/routes/api/external/roles/delete.test.js b/x-pack/legacy/plugins/security/server/routes/api/external/roles/delete.test.js deleted file mode 100644 index 638edf577da3a6..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/external/roles/delete.test.js +++ /dev/null @@ -1,124 +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 Hapi from 'hapi'; -import Boom from 'boom'; -import { initDeleteRolesApi } from './delete'; - -const createMockServer = () => { - const mockServer = new Hapi.Server({ debug: false, port: 8080 }); - return mockServer; -}; - -const defaultPreCheckLicenseImpl = () => null; - -describe('DELETE role', () => { - const deleteRoleTest = ( - description, - { - name, - preCheckLicenseImpl, - callWithRequestImpl, - asserts, - } - ) => { - test(description, async () => { - const mockServer = createMockServer(); - const pre = jest.fn().mockImplementation(preCheckLicenseImpl); - const mockCallWithRequest = jest.fn(); - if (callWithRequestImpl) { - mockCallWithRequest.mockImplementation(callWithRequestImpl); - } - initDeleteRolesApi(mockServer, mockCallWithRequest, pre); - const headers = { - authorization: 'foo', - }; - - const request = { - method: 'DELETE', - url: `/api/security/role/${name}`, - headers, - }; - const { result, statusCode } = await mockServer.inject(request); - - if (preCheckLicenseImpl) { - expect(pre).toHaveBeenCalled(); - } else { - expect(pre).not.toHaveBeenCalled(); - } - - if (callWithRequestImpl) { - expect(mockCallWithRequest).toHaveBeenCalledWith( - expect.objectContaining({ - headers: expect.objectContaining({ - authorization: headers.authorization, - }), - }), - 'shield.deleteRole', - { name }, - ); - } else { - expect(mockCallWithRequest).not.toHaveBeenCalled(); - } - expect(statusCode).toBe(asserts.statusCode); - expect(result).toEqual(asserts.result); - }); - }; - - describe('failure', () => { - deleteRoleTest(`requires name in params`, { - name: '', - asserts: { - statusCode: 404, - result: { - error: 'Not Found', - message: 'Not Found', - statusCode: 404, - }, - }, - }); - - deleteRoleTest(`returns result of routePreCheckLicense`, { - preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'), - asserts: { - statusCode: 403, - result: { - error: 'Forbidden', - statusCode: 403, - message: 'test forbidden message', - }, - }, - }); - - deleteRoleTest(`returns error from callWithRequest`, { - name: 'foo-role', - preCheckLicenseImpl: defaultPreCheckLicenseImpl, - callWithRequestImpl: async () => { - throw Boom.notFound('test not found message'); - }, - asserts: { - statusCode: 404, - result: { - error: 'Not Found', - statusCode: 404, - message: 'test not found message', - }, - }, - }); - }); - - describe('success', () => { - deleteRoleTest(`deletes role`, { - name: 'foo-role', - preCheckLicenseImpl: defaultPreCheckLicenseImpl, - callWithRequestImpl: async () => {}, - asserts: { - statusCode: 204, - result: null - } - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/external/roles/get.js b/x-pack/legacy/plugins/security/server/routes/api/external/roles/get.js deleted file mode 100644 index 3540d9b7a883bf..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/external/roles/get.js +++ /dev/null @@ -1,222 +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 Boom from 'boom'; -import { GLOBAL_RESOURCE, RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../../../../common/constants'; -import { wrapError } from '../../../../../../../../plugins/security/server'; -import { PrivilegeSerializer, ResourceSerializer } from '../../../../lib/authorization'; - -export function initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, application) { - - const transformKibanaApplicationsFromEs = (roleApplications) => { - const roleKibanaApplications = roleApplications - .filter( - roleApplication => roleApplication.application === application || - roleApplication.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD - ); - - // if any application entry contains an empty resource, we throw an error - if (roleKibanaApplications.some(entry => entry.resources.length === 0)) { - throw new Error(`ES returned an application entry without resources, can't process this`); - } - - // if there is an entry with the reserved privileges application wildcard - // and there are privileges which aren't reserved, we won't transform these - if (roleKibanaApplications.some(entry => - entry.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD && - !entry.privileges.every(privilege => PrivilegeSerializer.isSerializedReservedPrivilege(privilege))) - ) { - return { - success: false - }; - } - - // if space privilege assigned globally, we can't transform these - if (roleKibanaApplications.some(entry => - entry.resources.includes(GLOBAL_RESOURCE) && - entry.privileges.some(privilege => PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege))) - ) { - return { - success: false - }; - } - - // if global base or reserved privilege assigned at a space, we can't transform these - if (roleKibanaApplications.some(entry => - !entry.resources.includes(GLOBAL_RESOURCE) && - entry.privileges.some(privilege => - PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) || - PrivilegeSerializer.isSerializedReservedPrivilege(privilege) - )) - ) { - return { - success: false - }; - } - - // if reserved privilege assigned with feature or base privileges, we won't transform these - if (roleKibanaApplications.some(entry => - entry.privileges.some(privilege => PrivilegeSerializer.isSerializedReservedPrivilege(privilege)) && - entry.privileges.some(privilege => !PrivilegeSerializer.isSerializedReservedPrivilege(privilege))) - ) { - return { - success: false - }; - } - - // if base privilege assigned with feature privileges, we won't transform these - if (roleKibanaApplications.some(entry => - entry.privileges.some(privilege => PrivilegeSerializer.isSerializedFeaturePrivilege(privilege)) && - ( - entry.privileges.some(privilege => PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege)) || - entry.privileges.some(privilege => PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege)) - ) - )) { - return { - success: false - }; - } - - // if any application entry contains the '*' resource in addition to another resource, we can't transform these - if (roleKibanaApplications.some(entry => entry.resources.includes(GLOBAL_RESOURCE) && entry.resources.length > 1)) { - return { - success: false - }; - } - - const allResources = _.flatten(roleKibanaApplications.map(entry => entry.resources)); - // if we have improperly formatted resource entries, we can't transform these - if (allResources.some(resource => resource !== GLOBAL_RESOURCE && !ResourceSerializer.isSerializedSpaceResource(resource))) { - return { - success: false - }; - } - - // if we have resources duplicated in entries, we won't transform these - if (allResources.length !== _.uniq(allResources).length) { - return { - success: false - }; - } - - return { - success: true, - value: roleKibanaApplications.map(({ resources, privileges }) => { - // if we're dealing with a global entry, which we've ensured above is only possible if it's the only item in the array - if (resources.length === 1 && resources[0] === GLOBAL_RESOURCE) { - const reservedPrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedReservedPrivilege(privilege)); - const basePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege)); - const featurePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedFeaturePrivilege(privilege)); - - return { - ...reservedPrivileges.length ? { - _reserved: reservedPrivileges.map(privilege => PrivilegeSerializer.deserializeReservedPrivilege(privilege)) - } : {}, - base: basePrivileges.map(privilege => PrivilegeSerializer.serializeGlobalBasePrivilege(privilege)), - feature: featurePrivileges.reduce((acc, privilege) => { - const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); - return { - ...acc, - [featurePrivilege.featureId]: _.uniq([ - ...acc[featurePrivilege.featureId] || [], - featurePrivilege.privilege - ]) - }; - }, {}), - spaces: ['*'] - }; - } - - const basePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege)); - const featurePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedFeaturePrivilege(privilege)); - return { - base: basePrivileges.map(privilege => PrivilegeSerializer.deserializeSpaceBasePrivilege(privilege)), - feature: featurePrivileges.reduce((acc, privilege) => { - const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); - return { - ...acc, - [featurePrivilege.featureId]: _.uniq([ - ...acc[featurePrivilege.featureId] || [], - featurePrivilege.privilege - ]) - }; - }, {}), - spaces: resources.map(resource => ResourceSerializer.deserializeSpaceResource(resource)) - }; - }) - }; - }; - - const transformUnrecognizedApplicationsFromEs = (roleApplications) => { - return _.uniq(roleApplications - .filter(roleApplication => - roleApplication.application !== application && - roleApplication.application !== RESERVED_PRIVILEGES_APPLICATION_WILDCARD - ) - .map(roleApplication => roleApplication.application)); - }; - - const transformRoleFromEs = (role, name) => { - const kibanaTransformResult = transformKibanaApplicationsFromEs(role.applications); - - return { - name, - metadata: role.metadata, - transient_metadata: role.transient_metadata, - elasticsearch: { - cluster: role.cluster, - indices: role.indices, - run_as: role.run_as, - }, - kibana: kibanaTransformResult.success ? kibanaTransformResult.value : [], - _transform_error: [ - ...(kibanaTransformResult.success ? [] : ['kibana']) - ], - _unrecognized_applications: transformUnrecognizedApplicationsFromEs(role.applications), - }; - }; - - const transformRolesFromEs = (roles) => { - return _.map(roles, (role, name) => transformRoleFromEs(role, name)); - }; - - server.route({ - method: 'GET', - path: '/api/security/role', - async handler(request) { - try { - const response = await callWithRequest(request, 'shield.getRole'); - return _.sortBy(transformRolesFromEs(response), 'name'); - } catch (error) { - return wrapError(error); - } - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'GET', - path: '/api/security/role/{name}', - async handler(request) { - const name = request.params.name; - try { - const response = await callWithRequest(request, 'shield.getRole', { name }); - if (response[name]) { - return transformRoleFromEs(response[name], name); - } - - return Boom.notFound(); - } catch (error) { - return wrapError(error); - } - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); -} diff --git a/x-pack/legacy/plugins/security/server/routes/api/external/roles/get.test.js b/x-pack/legacy/plugins/security/server/routes/api/external/roles/get.test.js deleted file mode 100644 index 24aa4bd6e02b21..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/external/roles/get.test.js +++ /dev/null @@ -1,2378 +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 Hapi from 'hapi'; -import Boom from 'boom'; -import { initGetRolesApi } from './get'; - -const application = 'kibana-.kibana'; -const reservedPrivilegesApplicationWildcard = 'kibana-*'; - -const createMockServer = () => { - const mockServer = new Hapi.Server({ debug: false, port: 8080 }); - return mockServer; -}; - -describe('GET roles', () => { - const getRolesTest = ( - description, - { - preCheckLicenseImpl = () => null, - callWithRequestImpl, - asserts, - } - ) => { - test(description, async () => { - const mockServer = createMockServer(); - const pre = jest.fn().mockImplementation(preCheckLicenseImpl); - const mockCallWithRequest = jest.fn(); - if (callWithRequestImpl) { - mockCallWithRequest.mockImplementation(callWithRequestImpl); - } - initGetRolesApi(mockServer, mockCallWithRequest, pre, application); - const headers = { - authorization: 'foo', - }; - - const request = { - method: 'GET', - url: '/api/security/role', - headers, - }; - const { result, statusCode } = await mockServer.inject(request); - - expect(pre).toHaveBeenCalled(); - if (callWithRequestImpl) { - expect(mockCallWithRequest).toHaveBeenCalledWith( - expect.objectContaining({ - headers: expect.objectContaining({ - authorization: headers.authorization, - }), - }), - 'shield.getRole' - ); - } else { - expect(mockCallWithRequest).not.toHaveBeenCalled(); - } - expect(statusCode).toBe(asserts.statusCode); - expect(result).toEqual(asserts.result); - }); - }; - - describe('failure', () => { - getRolesTest(`returns result of routePreCheckLicense`, { - preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'), - asserts: { - statusCode: 403, - result: { - error: 'Forbidden', - statusCode: 403, - message: 'test forbidden message', - }, - }, - }); - - getRolesTest(`returns error from callWithRequest`, { - callWithRequestImpl: async () => { - throw Boom.notAcceptable('test not acceptable message'); - }, - asserts: { - statusCode: 406, - result: { - error: 'Not Acceptable', - statusCode: 406, - message: 'test not acceptable message', - }, - }, - }); - }); - - describe('success', () => { - getRolesTest(`transforms elasticsearch privileges`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: ['manage_watcher'], - indices: [ - { - names: ['.kibana*'], - privileges: ['read', 'view_index_metadata'], - }, - ], - applications: [], - run_as: ['other_user'], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: ['manage_watcher'], - indices: [ - { - names: ['.kibana*'], - privileges: ['read', 'view_index_metadata'], - }, - ], - run_as: ['other_user'], - }, - kibana: [], - _transform_error: [], - _unrecognized_applications: [], - }, - ], - }, - }); - - describe('global', () => { - getRolesTest(`transforms matching applications with * resource to kibana global base privileges`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['all', 'read'], - resources: ['*'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [ - { - base: ['all', 'read'], - feature: {}, - spaces: ['*'] - } - ], - _transform_error: [], - _unrecognized_applications: [], - }, - ], - }, - }); - - getRolesTest(`transforms matching applications with * resource to kibana global feature privileges`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['feature_foo.foo-privilege-1', 'feature_foo.foo-privilege-2', 'feature_bar.bar-privilege-1'], - resources: ['*'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [ - { - base: [], - feature: { - foo: ['foo-privilege-1', 'foo-privilege-2'], - bar: ['bar-privilege-1'] - }, - spaces: ['*'] - } - ], - _transform_error: [], - _unrecognized_applications: [], - }, - ], - }, - }); - - getRolesTest(`transforms matching applications with * resource to kibana _reserved privileges`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['reserved_customApplication1', 'reserved_customApplication2'], - resources: ['*'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [ - { - _reserved: ['customApplication1', 'customApplication2'], - base: [], - feature: {}, - spaces: ['*'] - } - ], - _transform_error: [], - _unrecognized_applications: [], - }, - ], - }, - }); - - getRolesTest(`transforms applications with wildcard and * resource to kibana _reserved privileges`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application: reservedPrivilegesApplicationWildcard, - privileges: ['reserved_customApplication1', 'reserved_customApplication2'], - resources: ['*'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [ - { - _reserved: ['customApplication1', 'customApplication2'], - base: [], - feature: {}, - spaces: ['*'] - } - ], - _transform_error: [], - _unrecognized_applications: [], - }, - ], - }, - }); - }); - - describe('space', () => { - getRolesTest(`transforms matching applications with space resources to kibana space base privileges`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['space_all', 'space_read'], - resources: ['space:marketing', 'space:sales'], - }, - { - application, - privileges: ['space_read'], - resources: ['space:engineering'], - }, - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [ - { - base: ['all', 'read'], - feature: {}, - spaces: ['marketing', 'sales'], - }, - { - base: ['read'], - feature: {}, - spaces: ['engineering'], - }, - ], - _transform_error: [], - _unrecognized_applications: [], - }, - ], - }, - }); - - getRolesTest(`transforms matching applications with space resources to kibana space feature privileges`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['feature_foo.foo-privilege-1', 'feature_foo.foo-privilege-2', 'feature_bar.bar-privilege-1'], - resources: ['space:marketing', 'space:sales'], - }, - { - application, - privileges: ['feature_foo.foo-privilege-1'], - resources: ['space:engineering'], - }, - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [ - { - base: [], - feature: { - foo: ['foo-privilege-1', 'foo-privilege-2'], - bar: ['bar-privilege-1'] - }, - spaces: ['marketing', 'sales'], - }, - { - base: [], - feature: { - foo: ['foo-privilege-1'], - }, - spaces: ['engineering'] - } - ], - _transform_error: [], - _unrecognized_applications: [], - }, - ], - }, - }); - }); - - getRolesTest(`return error if we have empty resources`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['read'], - resources: [], - }, - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 500, - result: { - error: 'Internal Server Error', - statusCode: 500, - message: 'An internal server error occurred', - }, - }, - }); - - getRolesTest(`resource not * without space: prefix returns empty kibana section with _transform_error set to ['kibana']`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['read'], - resources: ['default'], - }, - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - ], - }, - }); - - getRolesTest(`* and a space in the same entry returns empty kibana section with _transform_error set to ['kibana']`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['all'], - resources: ['*', 'space:engineering'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - ], - }, - }); - - getRolesTest(`* appearing in multiple entries returns empty kibana section with _transform_error set to ['kibana']`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['all'], - resources: ['*'], - }, - { - application, - privileges: ['read'], - resources: ['*'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - ], - }, - }); - - getRolesTest(`space appearing in multiple entries returns empty kibana section with _transform_error set to ['kibana']`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['space_all'], - resources: ['space:engineering'], - }, - { - application, - privileges: ['space_read'], - resources: ['space:engineering'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - ], - }, - }); - - getRolesTest(`space privilege assigned globally returns empty kibana section with _transform_error set to ['kibana']`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['space_all'], - resources: ['*'], - }, - { - application, - privileges: ['space_read'], - resources: ['space:engineering'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - ], - }, - }); - - getRolesTest(`space privilege with application wildcard returns empty kibana section with _transform_error set to ['kibana']`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application: reservedPrivilegesApplicationWildcard, - privileges: ['space_read'], - resources: ['space:engineering'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - ], - }, - }); - - getRolesTest(`global base privilege assigned at a space returns empty kibana section with _transform_error set to ['kibana']`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['all'], - resources: ['space:marketing'], - }, - { - application, - privileges: ['space_read'], - resources: ['space:engineering'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - ], - }, - }); - - getRolesTest(`global base privilege with application wildcard returns empty kibana section with _transform_error set to ['kibana']`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application: reservedPrivilegesApplicationWildcard, - privileges: ['all'], - resources: ['*'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - ], - }, - }); - - getRolesTest(`reserved privilege assigned at a space returns empty kibana section with _transform_error set to ['kibana']`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['reserved_foo'], - resources: ['space:marketing'], - }, - { - application, - privileges: ['space_read'], - resources: ['space:engineering'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - ], - }, - }); - - getRolesTest( - `reserved privilege assigned with a base privilege returns empty kibana section with _transform_error set to ['kibana']`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['reserved_foo', 'read'], - resources: ['*'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - ], - }, - }); - - getRolesTest( - `reserved privilege assigned with a feature privilege returns empty kibana section with _transform_error set to ['kibana']`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['reserved_foo', 'feature_foo.foo-privilege-1'], - resources: ['*'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - ], - }, - }); - - getRolesTest( - `global base privilege assigned with a feature privilege returns empty kibana section with _transform_error set to ['kibana']`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['all', 'feature_foo.foo-privilege-1'], - resources: ['*'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - ], - }, - }); - - getRolesTest( - `space base privilege assigned with a feature privilege returns empty kibana section with _transform_error set to ['kibana']`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['space_all', 'feature_foo.foo-privilege-1'], - resources: ['space:space_1'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - ], - }, - }); - - getRolesTest(`transforms unrecognized applications`, { - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application: 'kibana-.another-kibana', - privileges: ['read'], - resources: ['*'], - }, - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: [], - _unrecognized_applications: ['kibana-.another-kibana'] - }, - ], - }, - }); - - getRolesTest(`returns a sorted list of roles`, { - callWithRequestImpl: async () => ({ - z_role: { - cluster: [], - indices: [], - applications: [ - { - application: 'kibana-.another-kibana', - privileges: ['read'], - resources: ['*'], - }, - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - a_role: { - cluster: [], - indices: [], - applications: [ - { - application: 'kibana-.another-kibana', - privileges: ['read'], - resources: ['*'], - }, - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - b_role: { - cluster: [], - indices: [], - applications: [ - { - application: 'kibana-.another-kibana', - privileges: ['read'], - resources: ['*'], - }, - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: [ - { - name: 'a_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: [], - _unrecognized_applications: ['kibana-.another-kibana'] - }, - { - name: 'b_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: [], - _unrecognized_applications: ['kibana-.another-kibana'] - }, - { - name: 'z_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: [], - _unrecognized_applications: ['kibana-.another-kibana'] - }, - ], - }, - }); - }); -}); - -describe('GET role', () => { - const getRoleTest = ( - description, - { - name, - preCheckLicenseImpl = () => null, - callWithRequestImpl, - asserts, - } - ) => { - test(description, async () => { - const mockServer = createMockServer(); - const pre = jest.fn().mockImplementation(preCheckLicenseImpl); - const mockCallWithRequest = jest.fn(); - if (callWithRequestImpl) { - mockCallWithRequest.mockImplementation(callWithRequestImpl); - } - initGetRolesApi(mockServer, mockCallWithRequest, pre, 'kibana-.kibana'); - const headers = { - authorization: 'foo', - }; - - const request = { - method: 'GET', - url: `/api/security/role/${name}`, - headers, - }; - const { result, statusCode } = await mockServer.inject(request); - - expect(pre).toHaveBeenCalled(); - if (callWithRequestImpl) { - expect(mockCallWithRequest).toHaveBeenCalledWith( - expect.objectContaining({ - headers: expect.objectContaining({ - authorization: headers.authorization, - }), - }), - 'shield.getRole', - { name } - ); - } else { - expect(mockCallWithRequest).not.toHaveBeenCalled(); - } - expect(statusCode).toBe(asserts.statusCode); - expect(result).toEqual(asserts.result); - }); - }; - - describe('failure', () => { - getRoleTest(`returns result of routePreCheckLicense`, { - preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'), - asserts: { - statusCode: 403, - result: { - error: 'Forbidden', - statusCode: 403, - message: 'test forbidden message', - }, - }, - }); - - getRoleTest(`returns error from callWithRequest`, { - name: 'first_role', - callWithRequestImpl: async () => { - throw Boom.notAcceptable('test not acceptable message'); - }, - asserts: { - statusCode: 406, - result: { - error: 'Not Acceptable', - statusCode: 406, - message: 'test not acceptable message', - }, - }, - }); - - getRoleTest(`return error if we have empty resources`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['read'], - resources: [], - }, - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 500, - result: { - error: 'Internal Server Error', - statusCode: 500, - message: 'An internal server error occurred', - }, - }, - }); - }); - - describe('success', () => { - getRoleTest(`transforms elasticsearch privileges`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: ['manage_watcher'], - indices: [ - { - names: ['.kibana*'], - privileges: ['read', 'view_index_metadata'], - }, - ], - applications: [], - run_as: ['other_user'], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: ['manage_watcher'], - indices: [ - { - names: ['.kibana*'], - privileges: ['read', 'view_index_metadata'], - }, - ], - run_as: ['other_user'], - }, - kibana: [], - _transform_error: [], - _unrecognized_applications: [], - }, - }, - }); - - describe('global', () => { - getRoleTest(`transforms matching applications with * resource to kibana global base privileges`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['all', 'read'], - resources: ['*'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [ - { - base: ['all', 'read'], - feature: {}, - spaces: ['*'] - } - ], - _transform_error: [], - _unrecognized_applications: [], - }, - }, - }); - - getRoleTest(`transforms matching applications with * resource to kibana global feature privileges`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['feature_foo.foo-privilege-1', 'feature_foo.foo-privilege-2', 'feature_bar.bar-privilege-1'], - resources: ['*'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [ - { - base: [], - feature: { - foo: ['foo-privilege-1', 'foo-privilege-2'], - bar: ['bar-privilege-1'] - }, - spaces: ['*'] - } - ], - _transform_error: [], - _unrecognized_applications: [], - }, - }, - }); - - getRoleTest(`transforms matching applications with * resource to kibana _reserved privileges`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['reserved_customApplication1', 'reserved_customApplication2'], - resources: ['*'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [ - { - _reserved: ['customApplication1', 'customApplication2'], - base: [], - feature: {}, - spaces: ['*'] - } - ], - _transform_error: [], - _unrecognized_applications: [], - }, - }, - }); - - getRoleTest(`transforms applications with wildcard and * resource to kibana _reserved privileges`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application: reservedPrivilegesApplicationWildcard, - privileges: ['reserved_customApplication1', 'reserved_customApplication2'], - resources: ['*'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [ - { - _reserved: ['customApplication1', 'customApplication2'], - base: [], - feature: {}, - spaces: ['*'] - } - ], - _transform_error: [], - _unrecognized_applications: [], - }, - }, - }); - }); - - describe('space', () => { - getRoleTest(`transforms matching applications with space resources to kibana space base privileges`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['space_all', 'space_read'], - resources: ['space:marketing', 'space:sales'], - }, - { - application, - privileges: ['space_read'], - resources: ['space:engineering'], - }, - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [ - { - base: ['all', 'read'], - feature: {}, - spaces: ['marketing', 'sales'], - }, - { - base: ['read'], - feature: {}, - spaces: ['engineering'], - }, - ], - _transform_error: [], - _unrecognized_applications: [], - }, - }, - }); - - getRoleTest(`transforms matching applications with space resources to kibana space feature privileges`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['feature_foo.foo-privilege-1', 'feature_foo.foo-privilege-2', 'feature_bar.bar-privilege-1'], - resources: ['space:marketing', 'space:sales'], - }, - { - application, - privileges: ['feature_foo.foo-privilege-1'], - resources: ['space:engineering'], - }, - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [ - { - base: [], - feature: { - foo: ['foo-privilege-1', 'foo-privilege-2'], - bar: ['bar-privilege-1'] - }, - spaces: ['marketing', 'sales'], - }, - { - base: [], - feature: { - foo: ['foo-privilege-1'], - }, - spaces: ['engineering'] - } - ], - _transform_error: [], - _unrecognized_applications: [], - }, - }, - }); - }); - - getRoleTest(`resource not * without space: prefix returns empty kibana section with _transform_error set to ['kibana']`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['read'], - resources: ['default'], - }, - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - } - }); - - getRoleTest(`* and a space in the same entry returns empty kibana section with _transform_error set to ['kibana']`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['read'], - resources: ['default'], - }, - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - }, - }); - - getRoleTest(`* appearing in multiple entries returns empty kibana section with _transform_error set to ['kibana']`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['space_all'], - resources: ['space:engineering'], - }, - { - application, - privileges: ['space_read'], - resources: ['space:engineering'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - }, - }); - - getRoleTest(`space privilege assigned globally returns empty kibana section with _transform_error set to ['kibana']`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['space_all'], - resources: ['*'], - }, - { - application, - privileges: ['space_read'], - resources: ['space:engineering'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - }, - }); - - getRoleTest(`space privilege with application wildcard returns empty kibana section with _transform_error set to ['kibana']`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application: reservedPrivilegesApplicationWildcard, - privileges: ['space_read'], - resources: ['space:engineering'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - }, - }); - - getRoleTest(`global base privilege assigned at a space returns empty kibana section with _transform_error set to ['kibana']`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['all'], - resources: ['space:marketing'], - }, - { - application, - privileges: ['space_read'], - resources: ['space:engineering'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - }, - }); - - getRoleTest(`global base privilege with application wildcard returns empty kibana section with _transform_error set to ['kibana']`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application: reservedPrivilegesApplicationWildcard, - privileges: ['all'], - resources: ['*'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - }, - }); - - - getRoleTest(`reserved privilege assigned at a space returns empty kibana section with _transform_error set to ['kibana']`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['reserved_foo'], - resources: ['space:marketing'], - }, - { - application, - privileges: ['space_read'], - resources: ['space:engineering'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - }, - }); - - getRoleTest( - `reserved privilege assigned with a base privilege returns empty kibana section with _transform_error set to ['kibana']`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['reserved_foo', 'read'], - resources: ['*'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - }, - }); - - getRoleTest( - `reserved privilege assigned with a feature privilege returns empty kibana section with _transform_error set to ['kibana']`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['reserved_foo', 'feature_foo.foo-privilege-1'], - resources: ['*'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - }, - }); - - getRoleTest( - `global base privilege assigned with a feature privilege returns empty kibana section with _transform_error set to ['kibana']`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['all', 'feature_foo.foo-privilege-1'], - resources: ['*'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - }, - }); - - getRoleTest( - `space base privilege assigned with a feature privilege returns empty kibana section with _transform_error set to ['kibana']`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application, - privileges: ['space_all', 'feature_foo.foo-privilege-1'], - resources: ['space:space_1'], - } - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: ['kibana'], - _unrecognized_applications: [], - }, - }, - }); - - getRoleTest(`transforms unrecognized applications`, { - name: 'first_role', - callWithRequestImpl: async () => ({ - first_role: { - cluster: [], - indices: [], - applications: [ - { - application: 'kibana-.another-kibana', - privileges: ['read'], - resources: ['*'], - }, - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - }, - }), - asserts: { - statusCode: 200, - result: { - name: 'first_role', - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, - }, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _transform_error: [], - _unrecognized_applications: ['kibana-.another-kibana'] - }, - }, - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/external/roles/index.js b/x-pack/legacy/plugins/security/server/routes/api/external/roles/index.js deleted file mode 100644 index e883e8a6a8631b..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/external/roles/index.js +++ /dev/null @@ -1,23 +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 { getClient } from '../../../../../../../server/lib/get_client_shield'; -import { routePreCheckLicense } from '../../../../lib/route_pre_check_license'; -import { initGetRolesApi } from './get'; -import { initDeleteRolesApi } from './delete'; -import { initPutRolesApi } from './put'; - -export function initExternalRolesApi(server) { - const callWithRequest = getClient(server).callWithRequest; - const routePreCheckLicenseFn = routePreCheckLicense(server); - - const { authorization } = server.plugins.security; - const { application } = authorization; - - initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, application); - initPutRolesApi(server, callWithRequest, routePreCheckLicenseFn, authorization, application); - initDeleteRolesApi(server, callWithRequest, routePreCheckLicenseFn); -} diff --git a/x-pack/legacy/plugins/security/server/routes/api/external/roles/put.js b/x-pack/legacy/plugins/security/server/routes/api/external/roles/put.js deleted file mode 100644 index 681d2220930ef1..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/external/roles/put.js +++ /dev/null @@ -1,164 +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 { flatten, pick, identity, intersection } from 'lodash'; -import Joi from 'joi'; -import { GLOBAL_RESOURCE } from '../../../../../common/constants'; -import { wrapError } from '../../../../../../../../plugins/security/server'; -import { PrivilegeSerializer, ResourceSerializer } from '../../../../lib/authorization'; - -export function initPutRolesApi( - server, - callWithRequest, - routePreCheckLicenseFn, - authorization, - application -) { - - const transformKibanaPrivilegesToEs = (kibanaPrivileges = []) => { - return kibanaPrivileges.map(({ base, feature, spaces }) => { - if (spaces.length === 1 && spaces[0] === GLOBAL_RESOURCE) { - return { - privileges: [ - ...base ? base.map( - privilege => PrivilegeSerializer.serializeGlobalBasePrivilege(privilege) - ) : [], - ...feature ? flatten( - Object.entries(feature).map( - ([featureName, featurePrivileges])=> featurePrivileges.map( - privilege => PrivilegeSerializer.serializeFeaturePrivilege(featureName, privilege) - ) - ) - ) : [] - ], - application, - resources: [GLOBAL_RESOURCE] - }; - } - - return { - privileges: [ - ...base ? base.map( - privilege => PrivilegeSerializer.serializeSpaceBasePrivilege(privilege) - ) : [], - ...feature ? flatten( - Object.entries(feature).map( - ([featureName, featurePrivileges])=> featurePrivileges.map( - privilege => PrivilegeSerializer.serializeFeaturePrivilege(featureName, privilege) - ) - ) - ) : [] - ], - application, - resources: spaces.map(resource => ResourceSerializer.serializeSpaceResource(resource)), - }; - }); - }; - - const transformRolesToEs = ( - payload, - existingApplications = [] - ) => { - const { elasticsearch = {}, kibana = [] } = payload; - const otherApplications = existingApplications.filter( - roleApplication => roleApplication.application !== application - ); - - return pick({ - metadata: payload.metadata, - cluster: elasticsearch.cluster || [], - indices: elasticsearch.indices || [], - run_as: elasticsearch.run_as || [], - applications: [ - ...transformKibanaPrivilegesToEs(kibana), - ...otherApplications, - ], - }, identity); - }; - - const getKibanaSchema = () => { - const privileges = authorization.privileges.get(); - const allSpacesSchema = Joi.array().length(1).items(Joi.string().valid([GLOBAL_RESOURCE])); - return Joi.array().items( - Joi.object({ - base: Joi.alternatives().when('spaces', { - is: allSpacesSchema, - then: Joi.array().items(Joi.string().valid(Object.keys(privileges.global))).empty(Joi.array().length(0)), - otherwise: Joi.array().items(Joi.string().valid(Object.keys(privileges.space))).empty(Joi.array().length(0)), - }), - feature: Joi.object() - .pattern(/^[a-zA-Z0-9_-]+$/, Joi.array().items(Joi.string().regex(/^[a-zA-Z0-9_-]+$/))) - .empty(Joi.object().length(0)), - spaces: Joi.alternatives( - allSpacesSchema, - Joi.array().items(Joi.string().regex(/^[a-z0-9_-]+$/)), - ).default([GLOBAL_RESOURCE]), - }) - // the following can be replaced with .oxor once we upgrade Joi - .without('base', ['feature']) - ).unique((a, b) => { - return intersection(a.spaces, b.spaces).length !== 0; - }); - }; - - const schema = Joi.object().keys({ - metadata: Joi.object().optional(), - elasticsearch: Joi.object().keys({ - cluster: Joi.array().items(Joi.string()), - indices: Joi.array().items({ - names: Joi.array().items(Joi.string()), - field_security: Joi.object().keys({ - grant: Joi.array().items(Joi.string()), - except: Joi.array().items(Joi.string()), - }), - privileges: Joi.array().items(Joi.string()), - query: Joi.string().allow(''), - allow_restricted_indices: Joi.boolean(), - }), - run_as: Joi.array().items(Joi.string()), - }), - kibana: Joi.lazy(() => getKibanaSchema()) - }); - - server.route({ - method: 'PUT', - path: '/api/security/role/{name}', - async handler(request, h) { - const { name } = request.params; - - try { - const existingRoleResponse = await callWithRequest(request, 'shield.getRole', { - name, - ignore: [404], - }); - - const body = transformRolesToEs( - request.payload, - existingRoleResponse[name] ? existingRoleResponse[name].applications : [] - ); - - await callWithRequest(request, 'shield.putRole', { name, body }); - return h.response().code(204); - } catch (err) { - throw wrapError(err); - } - }, - options: { - validate: { - params: Joi.object() - .keys({ - name: Joi.string() - .required() - .min(1) - .max(1024), - }) - .required(), - payload: schema, - }, - pre: [routePreCheckLicenseFn], - }, - }); -} diff --git a/x-pack/legacy/plugins/security/server/routes/api/external/roles/put.test.js b/x-pack/legacy/plugins/security/server/routes/api/external/roles/put.test.js deleted file mode 100644 index 01016b2b077c33..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/external/roles/put.test.js +++ /dev/null @@ -1,963 +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 Hapi from 'hapi'; -import Boom from 'boom'; -import { initPutRolesApi } from './put'; -import { defaultValidationErrorHandler } from '../../../../../../../../../src/core/server/http/http_tools'; -import { GLOBAL_RESOURCE } from '../../../../../common/constants'; - -const application = 'kibana-.kibana'; - -const createMockServer = () => { - const mockServer = new Hapi.Server({ - debug: false, - port: 8080, - routes: { - validate: { - failAction: defaultValidationErrorHandler - } - } - }); - return mockServer; -}; - -const defaultPreCheckLicenseImpl = () => null; - -const privilegeMap = { - global: { - all: [], - read: [], - }, - space: { - all: [], - read: [], - }, - features: { - foo: { - 'foo-privilege-1': [], - 'foo-privilege-2': [], - }, - bar: { - 'bar-privilege-1': [], - 'bar-privilege-2': [], - } - }, - reserved: { - customApplication1: [], - customApplication2: [], - } -}; - -const putRoleTest = ( - description, - { name, payload, preCheckLicenseImpl, callWithRequestImpls = [], asserts } -) => { - test(description, async () => { - const mockServer = createMockServer(); - const mockPreCheckLicense = jest - .fn() - .mockImplementation(preCheckLicenseImpl); - const mockCallWithRequest = jest.fn(); - for (const impl of callWithRequestImpls) { - mockCallWithRequest.mockImplementationOnce(impl); - } - const mockAuthorization = { - privileges: { - get: () => privilegeMap - } - }; - initPutRolesApi( - mockServer, - mockCallWithRequest, - mockPreCheckLicense, - mockAuthorization, - application, - ); - const headers = { - authorization: 'foo', - }; - - const request = { - method: 'PUT', - url: `/api/security/role/${name}`, - headers, - payload, - }; - const response = await mockServer.inject(request); - const { result, statusCode } = response; - - expect(result).toEqual(asserts.result); - expect(statusCode).toBe(asserts.statusCode); - if (preCheckLicenseImpl) { - expect(mockPreCheckLicense).toHaveBeenCalled(); - } else { - expect(mockPreCheckLicense).not.toHaveBeenCalled(); - } - if (asserts.callWithRequests) { - for (const args of asserts.callWithRequests) { - expect(mockCallWithRequest).toHaveBeenCalledWith( - expect.objectContaining({ - headers: expect.objectContaining({ - authorization: headers.authorization, - }), - }), - ...args - ); - } - } else { - expect(mockCallWithRequest).not.toHaveBeenCalled(); - } - }); -}; - -describe('PUT role', () => { - describe('failure', () => { - putRoleTest(`requires name in params`, { - name: '', - payload: {}, - asserts: { - statusCode: 404, - result: { - error: 'Not Found', - message: 'Not Found', - statusCode: 404, - }, - }, - }); - - putRoleTest(`requires name in params to not exceed 1024 characters`, { - name: 'a'.repeat(1025), - payload: {}, - asserts: { - statusCode: 400, - result: { - error: 'Bad Request', - message: `child "name" fails because ["name" length must be less than or equal to 1024 characters long]`, - statusCode: 400, - validation: { - keys: ['name'], - source: 'params', - }, - }, - }, - }); - - putRoleTest(`only allows features that match the pattern`, { - name: 'foo-role', - payload: { - kibana: [ - { - feature: { - '!foo': ['foo'] - } - } - ] - }, - asserts: { - statusCode: 400, - result: { - error: 'Bad Request', - //eslint-disable-next-line max-len - message: `child \"kibana\" fails because [\"kibana\" at position 0 fails because [child \"feature\" fails because [\"!foo\" is not allowed]]]`, - statusCode: 400, - validation: { - keys: ['kibana.0.feature.!foo'], - source: 'payload', - }, - }, - }, - }); - - putRoleTest(`only allows feature privileges that match the pattern`, { - name: 'foo-role', - payload: { - kibana: [ - { - feature: { - foo: ['!foo'] - } - } - ] - }, - asserts: { - statusCode: 400, - result: { - error: 'Bad Request', - //eslint-disable-next-line max-len - message: `child \"kibana\" fails because [\"kibana\" at position 0 fails because [child \"feature\" fails because [child \"foo\" fails because [\"foo\" at position 0 fails because [\"0\" with value \"!foo\" fails to match the required pattern: /^[a-zA-Z0-9_-]+$/]]]]]`, - statusCode: 400, - validation: { - keys: ['kibana.0.feature.foo.0'], - source: 'payload', - }, - }, - }, - }); - - putRoleTest(`doesn't allow both base and feature in the same entry`, { - name: 'foo-role', - payload: { - kibana: [ - { - base: ['all'], - feature: { - foo: ['foo'] - } - } - ] - }, - asserts: { - statusCode: 400, - result: { - error: 'Bad Request', - //eslint-disable-next-line max-len - message: `child \"kibana\" fails because [\"kibana\" at position 0 fails because [\"base\" conflict with forbidden peer \"feature\"]]`, - statusCode: 400, - validation: { - keys: ['kibana.0.base'], - source: 'payload', - }, - }, - }, - }); - - describe('global', () => { - putRoleTest(`only allows known Kibana global base privileges`, { - name: 'foo-role', - payload: { - kibana: [ - { - base: ['foo'], - spaces: ['*'] - } - ] - }, - asserts: { - statusCode: 400, - result: { - error: 'Bad Request', - //eslint-disable-next-line max-len - message: `child \"kibana\" fails because [\"kibana\" at position 0 fails because [child \"base\" fails because [\"base\" at position 0 fails because [\"0\" must be one of [all, read]]]]]`, - statusCode: 400, - validation: { - keys: ['kibana.0.base.0'], - source: 'payload', - }, - }, - }, - }); - - putRoleTest(`doesn't allow Kibana reserved privileges`, { - name: 'foo-role', - payload: { - kibana: [ - { - _reserved: ['customApplication1'], - spaces: ['*'] - } - ] - }, - asserts: { - statusCode: 400, - result: { - error: 'Bad Request', - //eslint-disable-next-line max-len - message: `child \"kibana\" fails because [\"kibana\" at position 0 fails because [\"_reserved\" is not allowed]]`, - statusCode: 400, - validation: { - keys: ['kibana.0._reserved'], - source: 'payload', - }, - }, - }, - }); - - putRoleTest(`only allows one global entry`, { - name: 'foo-role', - payload: { - kibana: [ - { - feature: { - foo: ['foo-privilege-1'] - }, - spaces: ['*'] - }, - { - feature: { - bar: ['bar-privilege-1'] - }, - spaces: ['*'] - } - ] - }, - asserts: { - statusCode: 400, - result: { - error: 'Bad Request', - //eslint-disable-next-line max-len - message: `child \"kibana\" fails because [\"kibana\" position 1 contains a duplicate value]`, - statusCode: 400, - validation: { - keys: ['kibana.1'], - source: 'payload' - } - }, - }, - }); - }); - - describe('space', () => { - - putRoleTest(`doesn't allow * in a space ID`, { - name: 'foo-role', - payload: { - kibana: [ - { - spaces: ['foo-*'] - } - ], - }, - asserts: { - statusCode: 400, - result: { - error: 'Bad Request', - //eslint-disable-next-line max-len - message: `child \"kibana\" fails because [\"kibana\" at position 0 fails because [child \"spaces\" fails because [\"spaces\" at position 0 fails because [\"0\" must be one of [*]], \"spaces\" at position 0 fails because [\"0\" with value \"foo-*\" fails to match the required pattern: /^[a-z0-9_-]+$/]]]]`, - statusCode: 400, - validation: { - keys: ['kibana.0.spaces.0', 'kibana.0.spaces.0'], - source: 'payload', - }, - }, - }, - }); - - putRoleTest(`can't assign space and global in same entry`, { - name: 'foo-role', - payload: { - kibana: [ - { - spaces: ['*', 'foo-space'] - } - ], - }, - asserts: { - statusCode: 400, - result: { - error: 'Bad Request', - //eslint-disable-next-line max-len - message: `child \"kibana\" fails because [\"kibana\" at position 0 fails because [child \"spaces\" fails because [\"spaces\" at position 1 fails because [\"1\" must be one of [*]], \"spaces\" at position 0 fails because [\"0\" with value \"*\" fails to match the required pattern: /^[a-z0-9_-]+$/]]]]`, - statusCode: 400, - validation: { - keys: ['kibana.0.spaces.1', 'kibana.0.spaces.0'], - source: 'payload', - }, - }, - }, - }); - - putRoleTest(`only allows known Kibana space base privileges`, { - name: 'foo-role', - payload: { - kibana: [ - { - base: ['foo'], - spaces: ['foo-space'] - } - ], - }, - asserts: { - statusCode: 400, - result: { - error: 'Bad Request', - //eslint-disable-next-line max-len - message: `child \"kibana\" fails because [\"kibana\" at position 0 fails because [child \"base\" fails because [\"base\" at position 0 fails because [\"0\" must be one of [all, read]]]]]`, - statusCode: 400, - validation: { - keys: ['kibana.0.base.0'], - source: 'payload', - }, - }, - }, - }); - - putRoleTest(`only allows space to be in one entry`, { - name: 'foo-role', - payload: { - kibana: [ - { - feature: { - foo: ['foo-privilege-1'] - }, - spaces: ['marketing'] - }, - { - feature: { - bar: ['bar-privilege-1'] - }, - spaces: ['sales', 'marketing'] - } - ] - }, - asserts: { - statusCode: 400, - result: { - error: 'Bad Request', - //eslint-disable-next-line max-len - message: `child \"kibana\" fails because [\"kibana\" position 1 contains a duplicate value]`, - statusCode: 400, - validation: { - keys: ['kibana.1'], - source: 'payload' - } - }, - }, - }); - - putRoleTest(`doesn't allow Kibana reserved privileges`, { - name: 'foo-role', - payload: { - kibana: [ - { - _reserved: ['customApplication1'], - spaces: ['marketing'] - }, - ] - }, - asserts: { - statusCode: 400, - result: { - error: 'Bad Request', - //eslint-disable-next-line max-len - message: `child \"kibana\" fails because [\"kibana\" at position 0 fails because [\"_reserved\" is not allowed]]`, - statusCode: 400, - validation: { - keys: ['kibana.0._reserved'], - source: 'payload' - } - }, - }, - }); - }); - - putRoleTest(`returns result of routePreCheckLicense`, { - name: 'foo-role', - payload: {}, - preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'), - asserts: { - statusCode: 403, - result: { - error: 'Forbidden', - statusCode: 403, - message: 'test forbidden message', - }, - }, - }); - }); - - describe('success', () => { - putRoleTest(`creates empty role`, { - name: 'foo-role', - payload: {}, - preCheckLicenseImpl: defaultPreCheckLicenseImpl, - callWithRequestImpls: [async () => ({}), async () => { }], - asserts: { - callWithRequests: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', - { - name: 'foo-role', - body: { - cluster: [], - indices: [], - run_as: [], - applications: [], - }, - }, - ], - ], - statusCode: 204, - result: null, - }, - }); - - putRoleTest(`if spaces isn't specifed, defaults to global`, { - name: 'foo-role', - payload: { - kibana: [ - { - base: ['all'], - } - ] - }, - preCheckLicenseImpl: defaultPreCheckLicenseImpl, - callWithRequestImpls: [async () => ({}), async () => { }], - asserts: { - callWithRequests: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', - { - name: 'foo-role', - body: { - cluster: [], - indices: [], - run_as: [], - applications: [ - { - application, - privileges: [ - 'all', - ], - resources: [GLOBAL_RESOURCE], - }, - ], - }, - }, - ], - ], - statusCode: 204, - result: null, - }, - }); - - putRoleTest(`allows base with empty array and feature in the same entry`, { - name: 'foo-role', - payload: { - kibana: [ - { - base: [], - feature: { - foo: ['foo'] - } - } - ] - }, - preCheckLicenseImpl: defaultPreCheckLicenseImpl, - callWithRequestImpls: [async () => ({}), async () => { }], - asserts: { - callWithRequests: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', - { - name: 'foo-role', - body: { - cluster: [], - indices: [], - run_as: [], - applications: [ - { - application, - privileges: [ - 'feature_foo.foo', - ], - resources: [GLOBAL_RESOURCE], - }, - ], - }, - }, - ], - ], - statusCode: 204, - result: null, - }, - }); - - putRoleTest(`allows base and feature with empty object in the same entry`, { - name: 'foo-role', - payload: { - kibana: [ - { - base: ['all'], - feature: {} - } - ] - }, - preCheckLicenseImpl: defaultPreCheckLicenseImpl, - callWithRequestImpls: [async () => ({}), async () => { }], - asserts: { - callWithRequests: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', - { - name: 'foo-role', - body: { - cluster: [], - indices: [], - run_as: [], - applications: [ - { - application, - privileges: [ - 'all', - ], - resources: [GLOBAL_RESOURCE], - }, - ], - }, - }, - ], - ], - statusCode: 204, - result: null, - }, - }); - - putRoleTest(`creates role with everything`, { - name: 'foo-role', - payload: { - metadata: { - foo: 'test-metadata', - }, - elasticsearch: { - cluster: ['test-cluster-privilege'], - indices: [ - { - field_security: { - grant: ['test-field-security-grant-1', 'test-field-security-grant-2'], - except: ['test-field-security-except-1', 'test-field-security-except-2'] - }, - names: ['test-index-name-1', 'test-index-name-2'], - privileges: ['test-index-privilege-1', 'test-index-privilege-2'], - query: `{ "match": { "title": "foo" } }`, - }, - ], - run_as: ['test-run-as-1', 'test-run-as-2'], - }, - kibana: [ - { - base: ['all', 'read'], - spaces: ['*'], - }, - { - base: ['all', 'read'], - spaces: ['test-space-1', 'test-space-2'] - }, - { - feature: { - foo: ['foo-privilege-1', 'foo-privilege-2'], - }, - spaces: ['test-space-3'] - } - ] - }, - preCheckLicenseImpl: defaultPreCheckLicenseImpl, - callWithRequestImpls: [async () => ({}), async () => { }], - asserts: { - callWithRequests: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', - { - name: 'foo-role', - body: { - applications: [ - { - application, - privileges: [ - 'all', - 'read', - ], - resources: [GLOBAL_RESOURCE], - }, - { - application, - privileges: [ - 'space_all', - 'space_read', - ], - resources: ['space:test-space-1', 'space:test-space-2'] - }, - { - application, - privileges: [ - 'feature_foo.foo-privilege-1', - 'feature_foo.foo-privilege-2', - ], - resources: ['space:test-space-3'] - }, - ], - cluster: ['test-cluster-privilege'], - indices: [ - { - field_security: { - grant: ['test-field-security-grant-1', 'test-field-security-grant-2'], - except: ['test-field-security-except-1', 'test-field-security-except-2'] - }, - names: ['test-index-name-1', 'test-index-name-2'], - privileges: [ - 'test-index-privilege-1', - 'test-index-privilege-2', - ], - query: `{ "match": { "title": "foo" } }`, - }, - ], - metadata: { foo: 'test-metadata' }, - run_as: ['test-run-as-1', 'test-run-as-2'], - }, - }, - ], - ], - statusCode: 204, - result: null, - }, - }); - - putRoleTest(`updates role which has existing kibana privileges`, { - name: 'foo-role', - payload: { - metadata: { - foo: 'test-metadata', - }, - elasticsearch: { - cluster: ['test-cluster-privilege'], - indices: [ - { - field_security: { - grant: ['test-field-security-grant-1', 'test-field-security-grant-2'], - except: ['test-field-security-except-1', 'test-field-security-except-2'] - }, - names: ['test-index-name-1', 'test-index-name-2'], - privileges: ['test-index-privilege-1', 'test-index-privilege-2'], - query: `{ "match": { "title": "foo" } }`, - }, - ], - run_as: ['test-run-as-1', 'test-run-as-2'], - }, - kibana: [ - { - feature: { - foo: ['foo-privilege-1'], - bar: ['bar-privilege-1'] - }, - spaces: ['*'] - }, - { - base: ['all'], - spaces: ['test-space-1', 'test-space-2'] - }, - { - feature: { - bar: ['bar-privilege-2'] - }, - spaces: ['test-space-3'] - } - ], - }, - preCheckLicenseImpl: defaultPreCheckLicenseImpl, - callWithRequestImpls: [ - async () => ({ - 'foo-role': { - metadata: { - bar: 'old-metadata', - }, - transient_metadata: { - enabled: true, - }, - cluster: ['old-cluster-privilege'], - indices: [ - { - field_security: { - grant: ['old-field-security-grant-1', 'old-field-security-grant-2'], - except: ['old-field-security-except-1', 'old-field-security-except-2'] - }, - names: ['old-index-name'], - privileges: ['old-privilege'], - query: `{ "match": { "old-title": "foo" } }`, - }, - ], - run_as: ['old-run-as'], - applications: [ - { - application, - privileges: ['old-kibana-privilege'], - resources: ['old-resource'], - }, - ], - }, - }), - async () => { }, - ], - asserts: { - callWithRequests: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', - { - name: 'foo-role', - body: { - applications: [ - { - application, - privileges: [ - 'feature_foo.foo-privilege-1', - 'feature_bar.bar-privilege-1', - ], - resources: [GLOBAL_RESOURCE], - }, - { - application, - privileges: [ - 'space_all', - ], - resources: ['space:test-space-1', 'space:test-space-2'] - }, - { - application, - privileges: [ - 'feature_bar.bar-privilege-2', - ], - resources: ['space:test-space-3'] - }, - ], - cluster: ['test-cluster-privilege'], - indices: [ - { - field_security: { - grant: ['test-field-security-grant-1', 'test-field-security-grant-2'], - except: ['test-field-security-except-1', 'test-field-security-except-2'] - }, - names: ['test-index-name-1', 'test-index-name-2'], - privileges: [ - 'test-index-privilege-1', - 'test-index-privilege-2', - ], - query: `{ "match": { "title": "foo" } }`, - }, - ], - metadata: { foo: 'test-metadata' }, - run_as: ['test-run-as-1', 'test-run-as-2'], - }, - }, - ], - ], - statusCode: 204, - result: null, - }, - }); - - putRoleTest( - `updates role which has existing other application privileges`, - { - name: 'foo-role', - payload: { - metadata: { - foo: 'test-metadata', - }, - elasticsearch: { - cluster: ['test-cluster-privilege'], - indices: [ - { - names: ['test-index-name-1', 'test-index-name-2'], - privileges: [ - 'test-index-privilege-1', - 'test-index-privilege-2', - ], - }, - ], - run_as: ['test-run-as-1', 'test-run-as-2'], - }, - kibana: [ - { - base: ['all', 'read'], - spaces: ['*'] - } - ] - }, - preCheckLicenseImpl: defaultPreCheckLicenseImpl, - callWithRequestImpls: [ - async () => ({ - 'foo-role': { - metadata: { - bar: 'old-metadata', - }, - transient_metadata: { - enabled: true, - }, - cluster: ['old-cluster-privilege'], - indices: [ - { - names: ['old-index-name'], - privileges: ['old-privilege'], - }, - ], - run_as: ['old-run-as'], - applications: [ - { - application, - privileges: ['old-kibana-privilege'], - resources: ['old-resource'], - }, - { - application: 'logstash-foo', - privileges: ['logstash-privilege'], - resources: ['logstash-resource'], - }, - { - application: 'beats-foo', - privileges: ['beats-privilege'], - resources: ['beats-resource'], - }, - ], - }, - }), - async () => { }, - ], - asserts: { - callWithRequests: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', - { - name: 'foo-role', - body: { - applications: [ - { - application, - privileges: [ - 'all', - 'read', - ], - resources: [GLOBAL_RESOURCE], - }, - { - application: 'logstash-foo', - privileges: ['logstash-privilege'], - resources: ['logstash-resource'], - }, - { - application: 'beats-foo', - privileges: ['beats-privilege'], - resources: ['beats-resource'], - }, - ], - cluster: ['test-cluster-privilege'], - indices: [ - { - names: ['test-index-name-1', 'test-index-name-2'], - privileges: [ - 'test-index-privilege-1', - 'test-index-privilege-2', - ], - }, - ], - metadata: { foo: 'test-metadata' }, - run_as: ['test-run-as-1', 'test-run-as-2'], - }, - }, - ], - ], - statusCode: 204, - result: null, - }, - } - ); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/index.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/index.js index ade1f0974096c5..fc55bdcc386616 100644 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/index.js +++ b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/index.js @@ -14,10 +14,7 @@ export function initApiKeysApi(server) { const callWithRequest = getClient(server).callWithRequest; const routePreCheckLicenseFn = routePreCheckLicense(server); - const { authorization } = server.plugins.security; - const { application } = authorization; - - initCheckPrivilegesApi(server, callWithRequest, routePreCheckLicenseFn, application); - initGetApiKeysApi(server, callWithRequest, routePreCheckLicenseFn, application); - initInvalidateApiKeysApi(server, callWithRequest, routePreCheckLicenseFn, application); + initCheckPrivilegesApi(server, callWithRequest, routePreCheckLicenseFn); + initGetApiKeysApi(server, callWithRequest, routePreCheckLicenseFn); + initInvalidateApiKeysApi(server, callWithRequest, routePreCheckLicenseFn); } diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js b/x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js index d22cf0aef4db73..f37c9a2fd917f1 100644 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js +++ b/x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js @@ -11,7 +11,7 @@ import { canRedirectRequest, wrapError, OIDCAuthenticationFlow } from '../../../ import { KibanaRequest } from '../../../../../../../../src/core/server'; import { createCSPRuleString } from '../../../../../../../../src/legacy/server/csp'; -export function initAuthenticateApi({ authc: { login, logout }, config }, server) { +export function initAuthenticateApi({ authc: { login, logout }, __legacyCompat: { config } }, server) { function prepareCustomResourceResponse(response, contentType) { return response .header('cache-control', 'private, no-cache, no-store') diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/users.js b/x-pack/legacy/plugins/security/server/routes/api/v1/users.js index 1d47dc88753486..595182653fa231 100644 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/users.js +++ b/x-pack/legacy/plugins/security/server/routes/api/v1/users.js @@ -13,7 +13,7 @@ import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; import { wrapError } from '../../../../../../../plugins/security/server'; import { KibanaRequest } from '../../../../../../../../src/core/server'; -export function initUsersApi({ authc: { login }, config }, server) { +export function initUsersApi({ authc: { login }, __legacyCompat: { config } }, server) { const callWithRequest = getClient(server).callWithRequest; const routePreCheckLicenseFn = routePreCheckLicense(server); diff --git a/x-pack/legacy/plugins/security/server/routes/views/logged_out.js b/x-pack/legacy/plugins/security/server/routes/views/logged_out.js index 51867631b57bee..25905aaab6f3fc 100644 --- a/x-pack/legacy/plugins/security/server/routes/views/logged_out.js +++ b/x-pack/legacy/plugins/security/server/routes/views/logged_out.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export function initLoggedOutView({ config: { cookieName } }, server) { +export function initLoggedOutView({ __legacyCompat: { config: { cookieName } } }, server) { const config = server.config(); const loggedOut = server.getHiddenUiAppById('logged_out'); diff --git a/x-pack/legacy/plugins/security/server/routes/views/login.js b/x-pack/legacy/plugins/security/server/routes/views/login.js index f7e7f2933efcc6..7e2b50b40f7276 100644 --- a/x-pack/legacy/plugins/security/server/routes/views/login.js +++ b/x-pack/legacy/plugins/security/server/routes/views/login.js @@ -8,16 +8,13 @@ import { get } from 'lodash'; import { parseNext } from '../../lib/parse_next'; -export function initLoginView({ config: { cookieName } }, server, xpackMainPlugin) { +export function initLoginView({ __legacyCompat: { config: { cookieName }, license } }, server) { const config = server.config(); const login = server.getHiddenUiAppById('login'); function shouldShowLogin() { - if (xpackMainPlugin && xpackMainPlugin.info) { - const licenseCheckResults = xpackMainPlugin.info.feature('security').getLicenseCheckResults(); - if (licenseCheckResults) { - return Boolean(licenseCheckResults.showLogin); - } + if (license.isEnabled()) { + return Boolean(license.getFeatures().showLogin); } // default to true if xpack info isn't available or diff --git a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js index 5692ce27218701..9b2a38e372ae3c 100644 --- a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js +++ b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js @@ -28,10 +28,10 @@ const path = require('path'); // this type of information. You usually will want to make any hand edits after // doing a search to KQL conversion before posting it as a signal or checking it // into another repository. -const INTERVAL = '24h'; +const INTERVAL = '5m'; const SEVERITY = 'low'; const TYPE = 'query'; -const FROM = 'now-24h'; +const FROM = 'now-6m'; const TO = 'now'; const INDEX = ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*']; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts index 2caf00ed0179cf..038effbd30086d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts @@ -57,10 +57,12 @@ export const updateIfIdExists = async ({ }); return signal; } catch (err) { - // This happens when we cannot get a saved object back from reading a signal. - // So we continue normally as we have nothing we can upsert. + if (err.output.statusCode === 404) { + return null; + } else { + return err; + } } - return null; }; export const createSignals = async ({ @@ -124,7 +126,7 @@ export const createSignals = async ({ return alertsClient.create({ data: { - name: 'SIEM Alert', + name, alertTypeId: SIGNALS_ID, alertTypeParams: { description, @@ -137,7 +139,6 @@ export const createSignals = async ({ savedId, filters, maxSignals, - name, severity, to, type, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts index ebddf6ac5b3c74..23f4e38a95eeab 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts @@ -7,16 +7,12 @@ import { SIGNALS_ID } from '../../../../common/constants'; import { FindSignalParams } from './types'; -// TODO: Change this from a search to a filter once this ticket is solved: -// https://github.com/elastic/kibana/projects/26#card-27462236 -export const findSignals = async ({ alertsClient, perPage, page, fields }: FindSignalParams) => { - return alertsClient.find({ +export const findSignals = async ({ alertsClient, perPage, page, fields }: FindSignalParams) => + alertsClient.find({ options: { fields, page, perPage, - searchFields: ['alertTypeId'], - search: SIGNALS_ID, + filter: `alert.attributes.alertTypeId: ${SIGNALS_ID}`, }, }); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts index fc18c1b552198e..6673b7bd1423b6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts @@ -11,11 +11,7 @@ import { Logger } from '../../../../../../../../src/core/server'; // TODO: Remove this for the build_events_query call eventually import { buildEventsReIndex } from './build_events_reindex'; -// TODO: Comment this in and use this instead of the reIndex API -// once scrolling and other things are done with it. import { buildEventsSearchQuery } from './build_events_query'; - -// bulk scroll class import { searchAfterAndBulkIndex } from './utils'; import { SignalAlertTypeDefinition } from './types'; import { getFilter } from './get_filter'; @@ -37,7 +33,6 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp query: schema.nullable(schema.string()), filters: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), maxSignals: schema.number({ defaultValue: 100 }), - name: schema.string(), severity: schema.string(), to: schema.string(), type: schema.string(), @@ -46,8 +41,6 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp }), }, async executor({ services, params }) { - const instance = services.alertInstanceFactory('siem-signals'); - const { description, filter, @@ -80,7 +73,6 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp index, }); - // TODO: Turn these options being sent in into a template for the alert type const noReIndex = buildEventsSearchQuery({ index, from, @@ -90,11 +82,7 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp }); try { - logger.info('Starting SIEM signal job'); - - // TODO: Comment this in eventually and use this for manual insertion of the - // signals instead of the ReIndex() api - + logger.debug(`Starting signal rule "${id}"`); if (process.env.USE_REINDEX_API === 'true') { const reIndex = buildEventsReIndex({ index, @@ -115,14 +103,19 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp references, }); const result = await services.callCluster('reindex', reIndex); - - // TODO: Error handling here and writing of any errors that come back from ES by - logger.info(`Result of reindex: ${JSON.stringify(result, null, 2)}`); + if (result.total > 0) { + logger.info( + `Total signals found from signal rule "${id}" (reindex algorithm): ${result.total}` + ); + } } else { - logger.info(`[+] Initial search call`); - + logger.debug(`[+] Initial search call of signal rule "${id}"`); const noReIndexResult = await services.callCluster('search', noReIndex); - logger.info(`Total docs to reindex: ${noReIndexResult.hits.total.value}`); + if (noReIndexResult.hits.total.value !== 0) { + logger.info( + `Total signals found from signal rule "${id}": ${noReIndexResult.hits.total.value}` + ); + } const bulkIndexResult = await searchAfterAndBulkIndex( noReIndexResult, @@ -132,19 +125,23 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp ); if (bulkIndexResult) { - logger.info('Finished SIEM signal job'); + logger.debug(`Finished signal rule "${id}"`); } else { - logger.error('Error processing SIEM signal job'); + logger.error(`Error processing signal rule "${id}"`); } } } catch (err) { // TODO: Error handling and writing of errors into a signal that has error // handling/conditions - logger.error(`You encountered an error of: ${err.message}`); + logger.error(`Error from signal rule "${id}", ${err.message}`); } + // TODO: Schedule and fire any and all actions configured for the signals rule + // such as email/slack/etc... Note you will not be able to save in-memory state + // without calling this at least once but we are not using in-memory state at the moment. // Schedule the default action which is nothing if it's a plain signal. - instance.scheduleActions('default'); + // const instance = services.alertInstanceFactory('siem-signals'); + // instance.scheduleActions('default'); }, }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts index b8d7af5c453033..96a319c944bd90 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts @@ -47,6 +47,10 @@ export type SignalAlertParamsRest = Omit> & { + id: SignalAlertParams['id']; +}; + export interface FindParamsRest { per_page: number; page: number; @@ -61,6 +65,10 @@ export interface Clients { export type SignalParams = SignalAlertParams & Clients; +export type UpdateSignalParams = Partial> & { + id: SignalAlertParams['id']; +} & Clients; + export type DeleteSignalParams = Clients & { id: string }; export interface FindSignalsRequest extends Omit { @@ -95,6 +103,10 @@ export interface SignalsRequest extends Hapi.Request { payload: SignalAlertParamsRest; } +export interface UpdateSignalsRequest extends Hapi.Request { + payload: UpdateSignalAlertParamsRest; +} + export type SignalExecutorOptions = Omit & { params: SignalAlertParams & { scrollSize: number; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts index 46ebfe98ce3d98..39f7951a8eab98 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { calculateInterval } from './update_signals'; +import { calculateInterval, calculateName } from './update_signals'; describe('update_signals', () => { describe('#calculateInterval', () => { @@ -23,4 +23,26 @@ describe('update_signals', () => { expect(interval).toEqual('5m'); }); }); + + describe('#calculateName', () => { + test('should return the updated name when it and originalName is there', () => { + const name = calculateName({ updatedName: 'updated', originalName: 'original' }); + expect(name).toEqual('updated'); + }); + + test('should return the updated name when originalName is undefined', () => { + const name = calculateName({ updatedName: 'updated', originalName: undefined }); + expect(name).toEqual('updated'); + }); + + test('should return the original name when updatedName is undefined', () => { + const name = calculateName({ updatedName: undefined, originalName: 'original' }); + expect(name).toEqual('original'); + }); + + test('should return untitled when both updatedName and originalName is undefined', () => { + const name = calculateName({ updatedName: undefined, originalName: undefined }); + expect(name).toEqual('untitled'); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts index 9cb30a29173d70..422d9123a286bb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts @@ -7,7 +7,7 @@ import { defaults } from 'lodash/fp'; import { AlertAction } from '../../../../../alerting/server/types'; import { readSignals } from './read_signals'; -import { SignalParams } from './types'; +import { UpdateSignalParams } from './types'; export const calculateInterval = ( interval: string | undefined, @@ -22,6 +22,25 @@ export const calculateInterval = ( } }; +export const calculateName = ({ + updatedName, + originalName, +}: { + updatedName: string | undefined; + originalName: string | undefined; +}): string => { + if (updatedName != null) { + return updatedName; + } else if (originalName != null) { + return originalName; + } else { + // You really should never get to this point. This is a fail safe way to send back + // the name of "untitled" just in case a signal rule name became null or undefined at + // some point since TypeScript allows it. + return 'untitled'; + } +}; + export const updateSignal = async ({ alertsClient, actionsClient, // TODO: Use this whenever we add feature support for different action types @@ -42,7 +61,7 @@ export const updateSignal = async ({ to, type, references, -}: SignalParams) => { +}: UpdateSignalParams) => { // TODO: Error handling and abstraction. Right now if this is an error then what happens is we get the error of // "message": "Saved object [alert/{id}] not found" const signal = await readSignals({ alertsClient, id }); @@ -67,7 +86,6 @@ export const updateSignal = async ({ filters, index, maxSignals, - name, severity, to, type, @@ -84,7 +102,7 @@ export const updateSignal = async ({ return alertsClient.update({ id: signal.id, data: { - name: 'SIEM Alert', + name: calculateName({ updatedName: name, originalName: signal.name }), interval: calculateInterval(interval, signal.interval), actions, alertTypeParams: nextAlertTypeParams, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts index 08e99de0f2581d..a514baa186fd26 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts @@ -43,8 +43,7 @@ export const singleBulkIndex = async ( logger: Logger ): Promise => { if (sr.hits.hits.length === 0) { - logger.warn('First search result yielded 0 documents'); - return false; + return true; } const bulkBody = sr.hits.hits.flatMap(doc => [ { @@ -62,8 +61,8 @@ export const singleBulkIndex = async ( body: bulkBody, }); const time2 = performance.now(); - logger.info(`individual bulk process time took: ${time2 - time1} milliseconds`); - logger.info(`took property says bulk took: ${firstResult.took} milliseconds`); + logger.debug(`individual bulk process time took: ${time2 - time1} milliseconds`); + logger.debug(`took property says bulk took: ${firstResult.took} milliseconds`); if (firstResult.errors) { logger.error(`[-] bulkResponse had errors: ${JSON.stringify(firstResult.errors, null, 2)}}`); return false; @@ -105,20 +104,24 @@ export const searchAfterAndBulkIndex = async ( service: AlertServices, logger: Logger ): Promise => { - logger.info('[+] starting bulk insertion'); + if (someResult.hits.hits.length === 0) { + return true; + } + + logger.debug('[+] starting bulk insertion'); const firstBulkIndexSuccess = await singleBulkIndex(someResult, params, service, logger); if (!firstBulkIndexSuccess) { - logger.warn('First bulk index was unsuccessful'); + logger.error('First bulk index was unsuccessful'); return false; } const totalHits = typeof someResult.hits.total === 'number' ? someResult.hits.total : someResult.hits.total.value; let size = someResult.hits.hits.length - 1; - logger.info(`first size: ${size}`); + logger.debug(`first size: ${size}`); let sortIds = someResult.hits.hits[0].sort; if (sortIds == null && totalHits > 0) { - logger.warn('sortIds was empty on first search but expected more '); + logger.error(`sortIds was empty on first search when encountering ${totalHits}`); return false; } else if (sortIds == null && totalHits === 0) { return true; @@ -130,7 +133,7 @@ export const searchAfterAndBulkIndex = async ( while (size < totalHits) { // utilize track_total_hits instead of true try { - logger.info(`sortIds: ${sortIds}`); + logger.debug(`sortIds: ${sortIds}`); const searchAfterResult: SignalSearchResponse = await singleSearchAfter( sortId, params, @@ -138,24 +141,24 @@ export const searchAfterAndBulkIndex = async ( logger ); size += searchAfterResult.hits.hits.length - 1; - logger.info(`size: ${size}`); + logger.debug(`size adjusted: ${size}`); sortIds = searchAfterResult.hits.hits[0].sort; if (sortIds == null) { - logger.warn('sortIds was empty search'); + logger.error('sortIds was empty search when running a signal rule'); return false; } sortId = sortIds[0]; - logger.info('next bulk index'); + logger.debug('next bulk index'); const bulkSuccess = await singleBulkIndex(searchAfterResult, params, service, logger); - logger.info('finished next bulk index'); + logger.debug('finished next bulk index'); if (!bulkSuccess) { - logger.error('[-] bulk index failed'); + logger.error('[-] bulk index failed but continuing'); } } catch (exc) { logger.error(`[-] search_after and bulk threw an error ${exc}`); return false; } } - logger.info(`[+] completed bulk index of ${totalHits}`); + logger.debug(`[+] completed bulk index of ${totalHits}`); return true; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts index 08307ef633ffe2..8248d1505143a5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts @@ -8,7 +8,7 @@ import Hapi from 'hapi'; import Joi from 'joi'; import { isFunction } from 'lodash/fp'; import { updateSignal } from '../alerts/update_signals'; -import { SignalsRequest } from '../alerts/types'; +import { UpdateSignalsRequest } from '../alerts/types'; import { updateSignalSchema } from './schemas'; export const createUpdateSignalsRoute: Hapi.ServerRoute = { @@ -30,7 +30,7 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { payload: updateSignalSchema, }, }, - async handler(request: SignalsRequest, headers) { + async handler(request: UpdateSignalsRequest, headers) { const { description, enabled, @@ -53,14 +53,13 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { type, references, } = request.payload; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; if (!alertsClient || !actionsClient) { return headers.response().code(404); } - return updateSignal({ alertsClient, actionsClient, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_all_api_keys.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_all_api_keys.sh new file mode 100755 index 00000000000000..efcee69a0152c6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_all_api_keys.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_all_api_keys.sh +# https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-invalidate-api-key.html +curl -s -k \ + -H "Content-Type: application/json" \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE ${ELASTICSEARCH_URL}/_security/api_key \ + --data "{ + \"username\": \"${ELASTICSEARCH_USERNAME}\" + }" \ + | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh index ee8fa18e1234d3..9c37e93831ccbe 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh @@ -14,3 +14,4 @@ set -e ./delete_all_alert_tasks.sh ./delete_signal_index.sh ./put_signal_index.sh +./delete_all_api_keys.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json index 71d79903a01db1..589583d417a133 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json @@ -1,14 +1,14 @@ { "id": "rule-1", - "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], - "interval": "5m", - "name": "Detect Root/Admin Users", + "description": "Changed Description of only detecting root user", + "index": ["auditbeat-*"], + "interval": "50m", + "name": "A different name", "severity": "high", "type": "query", "from": "now-6m", - "to": "now", - "query": "user.name: root or user.name: admin", + "to": "now-5m", + "query": "user.name: root", "language": "kuery", - "references": [] + "references": ["https://update1.example.com", "https://update2.example.com"] } diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index a92fdcb9304cd2..598d115a39e499 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -10,13 +10,11 @@ import { Legacy } from 'kibana'; import { KibanaRequest } from '../../../../src/core/server'; import { SpacesServiceSetup } from '../../../plugins/spaces/server/spaces_service/spaces_service'; import { SpacesPluginSetup } from '../../../plugins/spaces/server'; -import { createOptionalPlugin } from '../../server/lib/optional_plugin'; // @ts-ignore import { AuditLogger } from '../../server/lib/audit_logger'; import mappings from './mappings.json'; import { wrapError } from './server/lib/errors'; import { migrateToKibana660 } from './server/lib/migrations'; -import { SecurityPlugin } from '../security'; // @ts-ignore import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize'; import { initSpaceSelectorView, initEnterSpaceView } from './server/routes/views'; @@ -139,12 +137,6 @@ export const spaces = (kibana: Record) => create: (pluginId: string) => new AuditLogger(server, pluginId, server.config(), server.plugins.xpack_main.info), }, - security: createOptionalPlugin( - server.config(), - 'xpack.security', - server.plugins, - 'security' - ), xpackMain: server.plugins.xpack_main, }); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.test.ts index 0be475f137d92f..9cd41c8cbe8264 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.test.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.test.ts @@ -915,7 +915,6 @@ describe('reindexService', () => { expect(updatedOp.attributes.reindexTaskPercComplete).toEqual(1); expect(callCluster).toHaveBeenCalledWith('delete', { index: '.tasks', - type: 'task', id: 'xyz', }); }); @@ -950,7 +949,6 @@ describe('reindexService', () => { expect(updatedOp.attributes.status).toEqual(ReindexStatus.cancelled); expect(callCluster).toHaveBeenCalledWith('delete', { index: '.tasks', - type: 'task', id: 'xyz', }); }); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.ts index 20f097f39c7272..9b1c2517118f0e 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.ts @@ -376,7 +376,6 @@ export const reindexServiceFactory = ( // Delete the task from ES .tasks index const deleteTaskResp = await callCluster('delete', { index: '.tasks', - type: 'task', id: taskId, }); diff --git a/x-pack/legacy/plugins/uptime/common/constants/plugin.ts b/x-pack/legacy/plugins/uptime/common/constants/plugin.ts index eeb0683fc9bbb8..93c3f00a0a45c1 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/plugin.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/plugin.ts @@ -7,6 +7,6 @@ export const PLUGIN = { APP_ROOT_ID: 'react-uptime-root', ID: 'uptime', - ROUTER_BASE_NAME: '/app/uptime', + ROUTER_BASE_NAME: '/app/uptime#', LOCAL_STORAGE_KEY: 'xpack.uptime', }; diff --git a/x-pack/legacy/server/lib/__snapshots__/optional_plugin.test.ts.snap b/x-pack/legacy/server/lib/__snapshots__/optional_plugin.test.ts.snap deleted file mode 100644 index a57512e7e8dc6d..00000000000000 --- a/x-pack/legacy/server/lib/__snapshots__/optional_plugin.test.ts.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`throws error when invoked before it's available 1`] = `"Plugin accessed before it's available"`; - -exports[`throws error when invoked before it's available 2`] = `"Plugin accessed before it's available"`; - -exports[`throws error when invoked before it's available 3`] = `"Plugin accessed before it's available"`; - -exports[`throws error when invoked when it's not enabled 1`] = `"Plugin isn't enabled, check isEnabled before using"`; - -exports[`throws error when invoked when it's not enabled 2`] = `"Plugin isn't enabled, check isEnabled before using"`; - -exports[`throws error when invoked when it's not enabled 3`] = `"Plugin isn't enabled, check isEnabled before using"`; diff --git a/x-pack/legacy/server/lib/optional_plugin.test.ts b/x-pack/legacy/server/lib/optional_plugin.test.ts deleted file mode 100644 index 8645a61cb8fd14..00000000000000 --- a/x-pack/legacy/server/lib/optional_plugin.test.ts +++ /dev/null @@ -1,85 +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 { createOptionalPlugin } from './optional_plugin'; - -class FooPlugin { - public get aProp() { - return 'a prop'; - } - - public aField = 'a field'; - - public aMethod() { - return 'a method'; - } -} - -const createMockConfig = (settings: Record) => { - return { - get: (key: string) => { - if (!Object.keys(settings).includes(key)) { - throw new Error('Unknown config key'); - } - - return settings[key]; - }, - }; -}; - -describe('isEnabled', () => { - test('returns true when config.get(`${configPrefix}.enabled`) is true', () => { - const config = createMockConfig({ 'xpack.fooPlugin.enabled': true }); - const conditionalFooPlugin = createOptionalPlugin(config, 'xpack.fooPlugin', {}, 'fooPlugin'); - expect(conditionalFooPlugin.isEnabled).toBe(true); - }); - - test('returns false when config.get(`${configPrefix}.enabled`) is false', () => { - const config = createMockConfig({ 'xpack.fooPlugin.enabled': false }); - const conditionalFooPlugin = createOptionalPlugin(config, 'xpack.fooPlugin', {}, 'fooPlugin'); - expect(conditionalFooPlugin.isEnabled).toBe(false); - }); -}); - -test(`throws error when invoked before it's available`, () => { - const config = createMockConfig({ 'xpack.fooPlugin.enabled': true }); - const conditionalFooPlugin = createOptionalPlugin( - config, - 'xpack.fooPlugin', - {}, - 'fooPlugin' - ); - expect(() => conditionalFooPlugin.aProp).toThrowErrorMatchingSnapshot(); - expect(() => conditionalFooPlugin.aMethod()).toThrowErrorMatchingSnapshot(); - expect(() => conditionalFooPlugin.aField).toThrowErrorMatchingSnapshot(); -}); - -test(`throws error when invoked when it's not enabled`, () => { - const config = createMockConfig({ 'xpack.fooPlugin.enabled': false }); - const conditionalFooPlugin = createOptionalPlugin( - config, - 'xpack.fooPlugin', - {}, - 'fooPlugin' - ); - expect(() => conditionalFooPlugin.aProp).toThrowErrorMatchingSnapshot(); - expect(() => conditionalFooPlugin.aMethod()).toThrowErrorMatchingSnapshot(); - expect(() => conditionalFooPlugin.aField).toThrowErrorMatchingSnapshot(); -}); - -test(`behaves normally when it's enabled and available`, () => { - const config = createMockConfig({ 'xpack.fooPlugin.enabled': false }); - const conditionalFooPlugin = createOptionalPlugin( - config, - 'xpack.fooPlugin', - { - fooPlugin: new FooPlugin(), - }, - 'fooPlugin' - ); - expect(conditionalFooPlugin.aProp).toBe('a prop'); - expect(conditionalFooPlugin.aMethod()).toBe('a method'); - expect(conditionalFooPlugin.aField).toBe('a field'); -}); diff --git a/x-pack/legacy/server/lib/optional_plugin.ts b/x-pack/legacy/server/lib/optional_plugin.ts deleted file mode 100644 index 16522091d01cfa..00000000000000 --- a/x-pack/legacy/server/lib/optional_plugin.ts +++ /dev/null @@ -1,48 +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. - */ - -interface Config { - get(key: string): any; -} - -interface Plugins { - [key: string]: any; -} - -interface IsEnabled { - isEnabled: boolean; -} - -export type OptionalPlugin = IsEnabled & T; - -export function createOptionalPlugin( - config: Config, - configPrefix: string, - plugins: Plugins, - pluginId: string -): OptionalPlugin { - return new Proxy( - {}, - { - get(obj, prop) { - const isEnabled = config.get(`${configPrefix}.enabled`); - if (prop === 'isEnabled') { - return isEnabled; - } - - if (!plugins[pluginId] && isEnabled) { - throw new Error(`Plugin accessed before it's available`); - } - - if (!plugins[pluginId] && !isEnabled) { - throw new Error(`Plugin isn't enabled, check isEnabled before using`); - } - - return plugins[pluginId][prop]; - }, - } - ) as OptionalPlugin; -} diff --git a/x-pack/plugins/security/common/constants.ts b/x-pack/plugins/security/common/constants.ts new file mode 100644 index 00000000000000..44b6601daa7ff9 --- /dev/null +++ b/x-pack/plugins/security/common/constants.ts @@ -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 const GLOBAL_RESOURCE = '*'; +export const APPLICATION_PREFIX = 'kibana-'; +export const RESERVED_PRIVILEGES_APPLICATION_WILDCARD = 'kibana-*'; diff --git a/x-pack/legacy/plugins/security/common/model/builtin_es_privileges.ts b/x-pack/plugins/security/common/model/builtin_es_privileges.ts similarity index 100% rename from x-pack/legacy/plugins/security/common/model/builtin_es_privileges.ts rename to x-pack/plugins/security/common/model/builtin_es_privileges.ts diff --git a/x-pack/legacy/plugins/security/common/model/features_privileges.ts b/x-pack/plugins/security/common/model/features_privileges.ts similarity index 100% rename from x-pack/legacy/plugins/security/common/model/features_privileges.ts rename to x-pack/plugins/security/common/model/features_privileges.ts diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts index 00b17548c47ac2..c6ccd2518d2610 100644 --- a/x-pack/plugins/security/common/model/index.ts +++ b/x-pack/plugins/security/common/model/index.ts @@ -6,3 +6,8 @@ export { User, EditUser, getUserDisplayName } from './user'; export { AuthenticatedUser, canUserChangePassword } from './authenticated_user'; +export { BuiltinESPrivileges } from './builtin_es_privileges'; +export { FeaturesPrivileges } from './features_privileges'; +export { RawKibanaPrivileges, RawKibanaFeaturePrivileges } from './raw_kibana_privileges'; +export { Role, RoleIndexPrivilege, RoleKibanaPrivilege } from './role'; +export { KibanaPrivileges } from './kibana_privileges'; diff --git a/x-pack/legacy/plugins/security/common/model/kibana_privileges/feature_privileges.ts b/x-pack/plugins/security/common/model/kibana_privileges/feature_privileges.ts similarity index 100% rename from x-pack/legacy/plugins/security/common/model/kibana_privileges/feature_privileges.ts rename to x-pack/plugins/security/common/model/kibana_privileges/feature_privileges.ts diff --git a/x-pack/legacy/plugins/security/common/model/kibana_privileges/global_privileges.ts b/x-pack/plugins/security/common/model/kibana_privileges/global_privileges.ts similarity index 100% rename from x-pack/legacy/plugins/security/common/model/kibana_privileges/global_privileges.ts rename to x-pack/plugins/security/common/model/kibana_privileges/global_privileges.ts diff --git a/x-pack/legacy/plugins/security/common/model/kibana_privileges/index.ts b/x-pack/plugins/security/common/model/kibana_privileges/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/common/model/kibana_privileges/index.ts rename to x-pack/plugins/security/common/model/kibana_privileges/index.ts diff --git a/x-pack/legacy/plugins/security/common/model/kibana_privileges/kibana_privileges.ts b/x-pack/plugins/security/common/model/kibana_privileges/kibana_privileges.ts similarity index 100% rename from x-pack/legacy/plugins/security/common/model/kibana_privileges/kibana_privileges.ts rename to x-pack/plugins/security/common/model/kibana_privileges/kibana_privileges.ts diff --git a/x-pack/legacy/plugins/security/common/model/kibana_privileges/spaces_privileges.ts b/x-pack/plugins/security/common/model/kibana_privileges/spaces_privileges.ts similarity index 100% rename from x-pack/legacy/plugins/security/common/model/kibana_privileges/spaces_privileges.ts rename to x-pack/plugins/security/common/model/kibana_privileges/spaces_privileges.ts diff --git a/x-pack/legacy/plugins/security/common/model/raw_kibana_privileges.ts b/x-pack/plugins/security/common/model/raw_kibana_privileges.ts similarity index 100% rename from x-pack/legacy/plugins/security/common/model/raw_kibana_privileges.ts rename to x-pack/plugins/security/common/model/raw_kibana_privileges.ts diff --git a/x-pack/legacy/plugins/security/common/model/role.ts b/x-pack/plugins/security/common/model/role.ts similarity index 100% rename from x-pack/legacy/plugins/security/common/model/role.ts rename to x-pack/plugins/security/common/model/role.ts diff --git a/x-pack/legacy/plugins/security/common/privilege_calculator_utils.test.ts b/x-pack/plugins/security/common/privilege_calculator_utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/common/privilege_calculator_utils.test.ts rename to x-pack/plugins/security/common/privilege_calculator_utils.test.ts diff --git a/x-pack/legacy/plugins/security/common/privilege_calculator_utils.ts b/x-pack/plugins/security/common/privilege_calculator_utils.ts similarity index 100% rename from x-pack/legacy/plugins/security/common/privilege_calculator_utils.ts rename to x-pack/plugins/security/common/privilege_calculator_utils.ts diff --git a/x-pack/plugins/security/kibana.json b/x-pack/plugins/security/kibana.json index 9f243a7dfb2fc1..32f860b1423d30 100644 --- a/x-pack/plugins/security/kibana.json +++ b/x-pack/plugins/security/kibana.json @@ -3,6 +3,7 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "security"], + "requiredPlugins": ["features", "licensing"], "server": true, "ui": true } diff --git a/x-pack/legacy/plugins/security/server/lib/audit_logger.test.js b/x-pack/plugins/security/server/audit/audit_logger.test.ts similarity index 90% rename from x-pack/legacy/plugins/security/server/lib/audit_logger.test.js rename to x-pack/plugins/security/server/audit/audit_logger.test.ts index 716946adab41cb..2ae8b6762c5d4c 100644 --- a/x-pack/legacy/plugins/security/server/lib/audit_logger.test.js +++ b/x-pack/plugins/security/server/audit/audit_logger.test.ts @@ -7,22 +7,21 @@ import { SecurityAuditLogger } from './audit_logger'; const createMockAuditLogger = () => { return { - log: jest.fn() + log: jest.fn(), }; }; describe(`#savedObjectsAuthorizationFailure`, () => { - test('logs via auditLogger', () => { const auditLogger = createMockAuditLogger(); const securityAuditLogger = new SecurityAuditLogger(auditLogger); const username = 'foo-user'; const action = 'foo-action'; - const types = [ 'foo-type-1', 'foo-type-2' ]; + const types = ['foo-type-1', 'foo-type-2']; const missing = [`saved_object:${types[0]}/foo-action`, `saved_object:${types[1]}/foo-action`]; const args = { - 'foo': 'bar', - 'baz': 'quz', + foo: 'bar', + baz: 'quz', }; securityAuditLogger.savedObjectsAuthorizationFailure(username, action, types, missing, args); @@ -47,10 +46,10 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { const securityAuditLogger = new SecurityAuditLogger(auditLogger); const username = 'foo-user'; const action = 'foo-action'; - const types = [ 'foo-type-1', 'foo-type-2' ]; + const types = ['foo-type-1', 'foo-type-2']; const args = { - 'foo': 'bar', - 'baz': 'quz', + foo: 'bar', + baz: 'quz', }; securityAuditLogger.savedObjectsAuthorizationSuccess(username, action, types, args); diff --git a/x-pack/legacy/plugins/security/server/lib/audit_logger.js b/x-pack/plugins/security/server/audit/audit_logger.ts similarity index 59% rename from x-pack/legacy/plugins/security/server/lib/audit_logger.js rename to x-pack/plugins/security/server/audit/audit_logger.ts index 1326aaf3ee4b54..4c2c57d0e029e7 100644 --- a/x-pack/legacy/plugins/security/server/lib/audit_logger.js +++ b/x-pack/plugins/security/server/audit/audit_logger.ts @@ -4,13 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LegacyAPI } from '../plugin'; + export class SecurityAuditLogger { - constructor(auditLogger) { - this._auditLogger = auditLogger; - } + constructor(private readonly auditLogger: LegacyAPI['auditLogger']) {} - savedObjectsAuthorizationFailure(username, action, types, missing, args) { - this._auditLogger.log( + savedObjectsAuthorizationFailure( + username: string, + action: string, + types: string[], + missing: string[], + args?: Record + ) { + this.auditLogger.log( 'saved_objects_authorization_failure', `${username} unauthorized to ${action} ${types.join(',')}, missing ${missing.join(',')}`, { @@ -18,13 +24,18 @@ export class SecurityAuditLogger { action, types, missing, - args + args, } ); } - savedObjectsAuthorizationSuccess(username, action, types, args) { - this._auditLogger.log( + savedObjectsAuthorizationSuccess( + username: string, + action: string, + types: string[], + args?: Record + ) { + this.auditLogger.log( 'saved_objects_authorization_success', `${username} authorized to ${action} ${types.join(',')}`, { diff --git a/x-pack/plugins/security/server/audit/index.mock.ts b/x-pack/plugins/security/server/audit/index.mock.ts new file mode 100644 index 00000000000000..c14b98ed4781eb --- /dev/null +++ b/x-pack/plugins/security/server/audit/index.mock.ts @@ -0,0 +1,16 @@ +/* + * 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 { SecurityAuditLogger } from './audit_logger'; + +export const securityAuditLoggerMock = { + create() { + return ({ + savedObjectsAuthorizationFailure: jest.fn(), + savedObjectsAuthorizationSuccess: jest.fn(), + } as unknown) as jest.Mocked; + }, +}; diff --git a/x-pack/plugins/security/server/audit/index.ts b/x-pack/plugins/security/server/audit/index.ts new file mode 100644 index 00000000000000..3ab253151b805d --- /dev/null +++ b/x-pack/plugins/security/server/audit/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { SecurityAuditLogger } from './audit_logger'; diff --git a/x-pack/plugins/security/server/authentication/api_keys.test.ts b/x-pack/plugins/security/server/authentication/api_keys.test.ts index 7ecff1682465cf..3fca1007413d4c 100644 --- a/x-pack/plugins/security/server/authentication/api_keys.test.ts +++ b/x-pack/plugins/security/server/authentication/api_keys.test.ts @@ -4,19 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { APIKeys } from './api_keys'; import { IClusterClient, IScopedClusterClient } from '../../../../../src/core/server'; +import { SecurityLicense } from '../licensing'; +import { APIKeys } from './api_keys'; + import { httpServerMock, loggingServiceMock, elasticsearchServiceMock, } from '../../../../../src/core/server/mocks'; +import { licenseMock } from '../licensing/index.mock'; describe('API Keys', () => { let apiKeys: APIKeys; let mockClusterClient: jest.Mocked; let mockScopedClusterClient: jest.Mocked; - const mockIsSecurityFeatureDisabled = jest.fn(); + let mockLicense: jest.Mocked; beforeEach(() => { mockClusterClient = elasticsearchServiceMock.createClusterClient(); @@ -24,17 +27,20 @@ describe('API Keys', () => { mockClusterClient.asScoped.mockReturnValue((mockScopedClusterClient as unknown) as jest.Mocked< IScopedClusterClient >); - mockIsSecurityFeatureDisabled.mockReturnValue(false); + + mockLicense = licenseMock.create(); + mockLicense.isEnabled.mockReturnValue(true); + apiKeys = new APIKeys({ clusterClient: mockClusterClient, logger: loggingServiceMock.create().get('api-keys'), - isSecurityFeatureDisabled: mockIsSecurityFeatureDisabled, + license: mockLicense, }); }); describe('create()', () => { it('returns null when security feature is disabled', async () => { - mockIsSecurityFeatureDisabled.mockReturnValue(true); + mockLicense.isEnabled.mockReturnValue(false); const result = await apiKeys.create(httpServerMock.createKibanaRequest(), { name: '', role_descriptors: {}, @@ -44,7 +50,7 @@ describe('API Keys', () => { }); it('calls callCluster with proper parameters', async () => { - mockIsSecurityFeatureDisabled.mockReturnValue(false); + mockLicense.isEnabled.mockReturnValue(true); mockScopedClusterClient.callAsCurrentUser.mockResolvedValueOnce({ id: '123', name: 'key-name', @@ -77,7 +83,7 @@ describe('API Keys', () => { describe('invalidate()', () => { it('returns null when security feature is disabled', async () => { - mockIsSecurityFeatureDisabled.mockReturnValue(true); + mockLicense.isEnabled.mockReturnValue(false); const result = await apiKeys.invalidate(httpServerMock.createKibanaRequest(), { id: '123', }); @@ -86,7 +92,7 @@ describe('API Keys', () => { }); it('calls callCluster with proper parameters', async () => { - mockIsSecurityFeatureDisabled.mockReturnValue(false); + mockLicense.isEnabled.mockReturnValue(true); mockScopedClusterClient.callAsCurrentUser.mockResolvedValueOnce({ invalidated_api_keys: ['api-key-id-1'], previously_invalidated_api_keys: [], @@ -111,7 +117,7 @@ describe('API Keys', () => { }); it(`Only passes id as a parameter`, async () => { - mockIsSecurityFeatureDisabled.mockReturnValue(false); + mockLicense.isEnabled.mockReturnValue(true); mockScopedClusterClient.callAsCurrentUser.mockResolvedValueOnce({ invalidated_api_keys: ['api-key-id-1'], previously_invalidated_api_keys: [], diff --git a/x-pack/plugins/security/server/authentication/api_keys.ts b/x-pack/plugins/security/server/authentication/api_keys.ts index 3709e8e7195fe6..b207e227c56af0 100644 --- a/x-pack/plugins/security/server/authentication/api_keys.ts +++ b/x-pack/plugins/security/server/authentication/api_keys.ts @@ -5,6 +5,7 @@ */ import { IClusterClient, KibanaRequest, Logger } from '../../../../../src/core/server'; +import { SecurityLicense } from '../licensing'; /** * Represents the options to create an APIKey class instance that will be @@ -13,7 +14,7 @@ import { IClusterClient, KibanaRequest, Logger } from '../../../../../src/core/s export interface ConstructorOptions { logger: Logger; clusterClient: IClusterClient; - isSecurityFeatureDisabled: () => boolean; + license: SecurityLicense; } /** @@ -92,12 +93,12 @@ export interface InvalidateAPIKeyResult { export class APIKeys { private readonly logger: Logger; private readonly clusterClient: IClusterClient; - private readonly isSecurityFeatureDisabled: () => boolean; + private readonly license: SecurityLicense; - constructor({ logger, clusterClient, isSecurityFeatureDisabled }: ConstructorOptions) { + constructor({ logger, clusterClient, license }: ConstructorOptions) { this.logger = logger; this.clusterClient = clusterClient; - this.isSecurityFeatureDisabled = isSecurityFeatureDisabled; + this.license = license; } /** @@ -109,7 +110,7 @@ export class APIKeys { request: KibanaRequest, params: CreateAPIKeyParams ): Promise { - if (this.isSecurityFeatureDisabled()) { + if (!this.license.isEnabled()) { return null; } @@ -139,7 +140,7 @@ export class APIKeys { request: KibanaRequest, params: InvalidateAPIKeyParams ): Promise { - if (this.isSecurityFeatureDisabled()) { + if (!this.license.isEnabled()) { return null; } diff --git a/x-pack/plugins/security/server/authentication/index.test.ts b/x-pack/plugins/security/server/authentication/index.test.ts index 9342cce577dfb6..ff7cf876adbef2 100644 --- a/x-pack/plugins/security/server/authentication/index.test.ts +++ b/x-pack/plugins/security/server/authentication/index.test.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { licenseMock } from '../licensing/index.mock'; + jest.mock('./api_keys'); jest.mock('./authenticator'); @@ -41,32 +43,19 @@ import { InvalidateAPIKeyResult, InvalidateAPIKeyParams, } from './api_keys'; - -function mockXPackFeature({ isEnabled = true }: Partial<{ isEnabled: boolean }> = {}) { - return { - isEnabled: jest.fn().mockReturnValue(isEnabled), - isAvailable: jest.fn().mockReturnValue(true), - registerLicenseCheckResultsGenerator: jest.fn(), - getLicenseCheckResults: jest.fn(), - }; -} +import { SecurityLicense } from '../licensing'; describe('setupAuthentication()', () => { let mockSetupAuthenticationParams: { config: ConfigType; loggers: LoggerFactory; - getLegacyAPI(): LegacyAPI; - core: MockedKeys; + getLegacyAPI(): Pick; + http: jest.Mocked; clusterClient: jest.Mocked; + license: jest.Mocked; }; - let mockXpackInfo: jest.Mocked; let mockScopedClusterClient: jest.Mocked>; beforeEach(async () => { - mockXpackInfo = { - isAvailable: jest.fn().mockReturnValue(true), - feature: jest.fn().mockReturnValue(mockXPackFeature()), - }; - const mockConfig$ = createConfig$( coreMock.createPluginInitializerContext({ encryptionKey: 'ab'.repeat(16), @@ -77,11 +66,12 @@ describe('setupAuthentication()', () => { true ); mockSetupAuthenticationParams = { - core: coreMock.createSetup(), + http: coreMock.createSetup().http, config: await mockConfig$.pipe(first()).toPromise(), clusterClient: elasticsearchServiceMock.createClusterClient(), + license: licenseMock.create(), loggers: loggingServiceMock.create(), - getLegacyAPI: jest.fn().mockReturnValue({ xpackInfo: mockXpackInfo }), + getLegacyAPI: jest.fn(), }; mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); @@ -102,16 +92,16 @@ describe('setupAuthentication()', () => { await setupAuthentication(mockSetupAuthenticationParams); - expect(mockSetupAuthenticationParams.core.http.registerAuth).toHaveBeenCalledTimes(1); - expect(mockSetupAuthenticationParams.core.http.registerAuth).toHaveBeenCalledWith( + expect(mockSetupAuthenticationParams.http.registerAuth).toHaveBeenCalledTimes(1); + expect(mockSetupAuthenticationParams.http.registerAuth).toHaveBeenCalledWith( expect.any(Function) ); expect( - mockSetupAuthenticationParams.core.http.createCookieSessionStorageFactory + mockSetupAuthenticationParams.http.createCookieSessionStorageFactory ).toHaveBeenCalledTimes(1); expect( - mockSetupAuthenticationParams.core.http.createCookieSessionStorageFactory + mockSetupAuthenticationParams.http.createCookieSessionStorageFactory ).toHaveBeenCalledWith({ encryptionKey: config.encryptionKey, isSecure: config.secureCookies, @@ -129,7 +119,7 @@ describe('setupAuthentication()', () => { await setupAuthentication(mockSetupAuthenticationParams); - authHandler = mockSetupAuthenticationParams.core.http.registerAuth.mock.calls[0][0]; + authHandler = mockSetupAuthenticationParams.http.registerAuth.mock.calls[0][0]; authenticate = jest.requireMock('./authenticator').Authenticator.mock.instances[0] .authenticate; }); @@ -138,7 +128,7 @@ describe('setupAuthentication()', () => { const mockRequest = httpServerMock.createKibanaRequest(); const mockResponse = httpServerMock.createLifecycleResponseFactory(); - mockXpackInfo.feature.mockReturnValue(mockXPackFeature({ isEnabled: false })); + mockSetupAuthenticationParams.license.isEnabled.mockReturnValue(false); await authHandler(mockRequest, mockResponse, mockAuthToolkit); @@ -302,7 +292,7 @@ describe('setupAuthentication()', () => { }); it('returns `null` if Security is disabled', async () => { - mockXpackInfo.feature.mockReturnValue(mockXPackFeature({ isEnabled: false })); + mockSetupAuthenticationParams.license.isEnabled.mockReturnValue(false); await expect(getCurrentUser(httpServerMock.createKibanaRequest())).resolves.toBe(null); }); @@ -331,7 +321,7 @@ describe('setupAuthentication()', () => { }); it('returns `true` if Security is disabled', async () => { - mockXpackInfo.feature.mockReturnValue(mockXPackFeature({ isEnabled: false })); + mockSetupAuthenticationParams.license.isEnabled.mockReturnValue(false); await expect(isAuthenticated(httpServerMock.createKibanaRequest())).resolves.toBe(true); }); diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 9553ddd09b2c13..df16dd375e858a 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -16,6 +16,7 @@ import { getErrorStatusCode } from '../errors'; import { Authenticator, ProviderSession } from './authenticator'; import { LegacyAPI } from '../plugin'; import { APIKeys, CreateAPIKeyParams, InvalidateAPIKeyParams } from './api_keys'; +import { SecurityLicense } from '../licensing'; export { canRedirectRequest } from './can_redirect_request'; export { Authenticator, ProviderLoginAttempt } from './authenticator'; @@ -30,35 +31,32 @@ export { } from './api_keys'; interface SetupAuthenticationParams { - core: CoreSetup; + http: CoreSetup['http']; clusterClient: IClusterClient; config: ConfigType; + license: SecurityLicense; loggers: LoggerFactory; - getLegacyAPI(): LegacyAPI; + getLegacyAPI(): Pick; } export type Authentication = UnwrapPromise>; export async function setupAuthentication({ - core, + http, clusterClient, config, + license, loggers, getLegacyAPI, }: SetupAuthenticationParams) { const authLogger = loggers.get('authentication'); - const isSecurityFeatureDisabled = () => { - const xpackInfo = getLegacyAPI().xpackInfo; - return xpackInfo.isAvailable() && !xpackInfo.feature('security').isEnabled(); - }; - /** * Retrieves currently authenticated user associated with the specified request. * @param request */ const getCurrentUser = async (request: KibanaRequest) => { - if (isSecurityFeatureDisabled()) { + if (!license.isEnabled()) { return null; } @@ -69,11 +67,11 @@ export async function setupAuthentication({ const authenticator = new Authenticator({ clusterClient, - basePath: core.http.basePath, + basePath: http.basePath, config: { sessionTimeout: config.sessionTimeout, authc: config.authc }, isSystemAPIRequest: (request: KibanaRequest) => getLegacyAPI().isSystemAPIRequest(request), loggers, - sessionStorageFactory: await core.http.createCookieSessionStorageFactory({ + sessionStorageFactory: await http.createCookieSessionStorageFactory({ encryptionKey: config.encryptionKey, isSecure: config.secureCookies, name: config.cookieName, @@ -84,9 +82,9 @@ export async function setupAuthentication({ authLogger.debug('Successfully initialized authenticator.'); - core.http.registerAuth(async (request, response, t) => { + http.registerAuth(async (request, response, t) => { // If security is disabled continue with no user credentials and delete the client cookie as well. - if (isSecurityFeatureDisabled()) { + if (!license.isEnabled()) { return t.authenticated(); } @@ -148,7 +146,7 @@ export async function setupAuthentication({ const apiKeys = new APIKeys({ clusterClient, logger: loggers.get('api-key'), - isSecurityFeatureDisabled, + license, }); return { login: authenticator.login.bind(authenticator), diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/__snapshots__/check_privileges.test.ts.snap b/x-pack/plugins/security/server/authorization/__snapshots__/check_privileges.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/__snapshots__/check_privileges.test.ts.snap rename to x-pack/plugins/security/server/authorization/__snapshots__/check_privileges.test.ts.snap diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/__snapshots__/disable_ui_capabilities.test.ts.snap b/x-pack/plugins/security/server/authorization/__snapshots__/disable_ui_capabilities.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/__snapshots__/disable_ui_capabilities.test.ts.snap rename to x-pack/plugins/security/server/authorization/__snapshots__/disable_ui_capabilities.test.ts.snap diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/__snapshots__/privilege_serializer.test.ts.snap b/x-pack/plugins/security/server/authorization/__snapshots__/privilege_serializer.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/__snapshots__/privilege_serializer.test.ts.snap rename to x-pack/plugins/security/server/authorization/__snapshots__/privilege_serializer.test.ts.snap diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/__snapshots__/privileges_serializer.test.ts.snap b/x-pack/plugins/security/server/authorization/__snapshots__/privileges_serializer.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/__snapshots__/privileges_serializer.test.ts.snap rename to x-pack/plugins/security/server/authorization/__snapshots__/privileges_serializer.test.ts.snap diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/__snapshots__/resource_serializer.test.ts.snap b/x-pack/plugins/security/server/authorization/__snapshots__/resource_serializer.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/__snapshots__/resource_serializer.test.ts.snap rename to x-pack/plugins/security/server/authorization/__snapshots__/resource_serializer.test.ts.snap diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/__snapshots__/validate_es_response.test.ts.snap b/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/__snapshots__/validate_es_response.test.ts.snap rename to x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/actions/__snapshots__/api.test.ts.snap b/x-pack/plugins/security/server/authorization/actions/__snapshots__/api.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/actions/__snapshots__/api.test.ts.snap rename to x-pack/plugins/security/server/authorization/actions/__snapshots__/api.test.ts.snap diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/actions/__snapshots__/app.test.ts.snap b/x-pack/plugins/security/server/authorization/actions/__snapshots__/app.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/actions/__snapshots__/app.test.ts.snap rename to x-pack/plugins/security/server/authorization/actions/__snapshots__/app.test.ts.snap diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/actions/__snapshots__/saved_object.test.ts.snap b/x-pack/plugins/security/server/authorization/actions/__snapshots__/saved_object.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/actions/__snapshots__/saved_object.test.ts.snap rename to x-pack/plugins/security/server/authorization/actions/__snapshots__/saved_object.test.ts.snap diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/actions/__snapshots__/ui.test.ts.snap b/x-pack/plugins/security/server/authorization/actions/__snapshots__/ui.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/actions/__snapshots__/ui.test.ts.snap rename to x-pack/plugins/security/server/authorization/actions/__snapshots__/ui.test.ts.snap diff --git a/x-pack/plugins/security/server/authorization/actions/actions.test.ts b/x-pack/plugins/security/server/authorization/actions/actions.test.ts new file mode 100644 index 00000000000000..384d25ca3b9710 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/actions/actions.test.ts @@ -0,0 +1,32 @@ +/* + * 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 { Actions } from '.'; + +describe('#constructor', () => { + test(`doesn't allow an empty string`, () => { + expect(() => new Actions('')).toThrowErrorMatchingInlineSnapshot( + `"version can't be an empty string"` + ); + }); +}); + +describe('#login', () => { + test('returns login:', () => { + const actions = new Actions('mock-version'); + + expect(actions.login).toBe('login:'); + }); +}); + +describe('#version', () => { + test("returns `version:${config.get('pkg.version')}`", () => { + const version = 'mock-version'; + const actions = new Actions(version); + + expect(actions.version).toBe(`version:${version}`); + }); +}); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/actions/actions.ts b/x-pack/plugins/security/server/authorization/actions/actions.ts similarity index 80% rename from x-pack/legacy/plugins/security/server/lib/authorization/actions/actions.ts rename to x-pack/plugins/security/server/authorization/actions/actions.ts index e10a0c9bc93131..4bf7a41550cc65 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/actions/actions.ts +++ b/x-pack/plugins/security/server/authorization/actions/actions.ts @@ -36,18 +36,9 @@ export class Actions { public readonly version = `version:${this.versionNumber}`; - constructor(private readonly versionNumber: string) {} -} - -export function actionsFactory(config: any) { - const version = config.get('pkg.version'); - if (typeof version !== 'string') { - throw new Error('version should be a string'); + constructor(private readonly versionNumber: string) { + if (versionNumber === '') { + throw new Error(`version can't be an empty string`); + } } - - if (version === '') { - throw new Error(`version can't be an empty string`); - } - - return new Actions(version); } diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/actions/api.test.ts b/x-pack/plugins/security/server/authorization/actions/api.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/actions/api.test.ts rename to x-pack/plugins/security/server/authorization/actions/api.test.ts diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/actions/api.ts b/x-pack/plugins/security/server/authorization/actions/api.ts similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/actions/api.ts rename to x-pack/plugins/security/server/authorization/actions/api.ts diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/actions/app.test.ts b/x-pack/plugins/security/server/authorization/actions/app.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/actions/app.test.ts rename to x-pack/plugins/security/server/authorization/actions/app.test.ts diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/actions/app.ts b/x-pack/plugins/security/server/authorization/actions/app.ts similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/actions/app.ts rename to x-pack/plugins/security/server/authorization/actions/app.ts diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/actions/index.ts b/x-pack/plugins/security/server/authorization/actions/index.ts similarity index 82% rename from x-pack/legacy/plugins/security/server/lib/authorization/actions/index.ts rename to x-pack/plugins/security/server/authorization/actions/index.ts index 34af70cd479e3f..d844ef5f4ae333 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/actions/index.ts +++ b/x-pack/plugins/security/server/authorization/actions/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { Actions, actionsFactory } from './actions'; +export { Actions } from './actions'; diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/actions/saved_object.test.ts b/x-pack/plugins/security/server/authorization/actions/saved_object.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/actions/saved_object.test.ts rename to x-pack/plugins/security/server/authorization/actions/saved_object.test.ts diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/actions/saved_object.ts b/x-pack/plugins/security/server/authorization/actions/saved_object.ts similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/actions/saved_object.ts rename to x-pack/plugins/security/server/authorization/actions/saved_object.ts diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/actions/space.test.ts b/x-pack/plugins/security/server/authorization/actions/space.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/actions/space.test.ts rename to x-pack/plugins/security/server/authorization/actions/space.test.ts diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/actions/space.ts b/x-pack/plugins/security/server/authorization/actions/space.ts similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/actions/space.ts rename to x-pack/plugins/security/server/authorization/actions/space.ts diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/actions/ui.test.ts b/x-pack/plugins/security/server/authorization/actions/ui.test.ts similarity index 94% rename from x-pack/legacy/plugins/security/server/lib/authorization/actions/ui.test.ts rename to x-pack/plugins/security/server/authorization/actions/ui.test.ts index 7f486dc3a8c983..f91b7baf78baad 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/actions/ui.test.ts +++ b/x-pack/plugins/security/server/authorization/actions/ui.test.ts @@ -29,10 +29,10 @@ describe('#allCatalogueEntries', () => { }); }); -describe('#allManagmentLinks', () => { +describe('#allManagementLinks', () => { test('returns `ui:${version}:management/*`', () => { const uiActions = new UIActions(version); - expect(uiActions.allManagmentLinks).toBe('ui:1.0.0-zeta1:management/*'); + expect(uiActions.allManagementLinks).toBe('ui:1.0.0-zeta1:management/*'); }); }); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/actions/ui.ts b/x-pack/plugins/security/server/authorization/actions/ui.ts similarity index 88% rename from x-pack/legacy/plugins/security/server/lib/authorization/actions/ui.ts rename to x-pack/plugins/security/server/authorization/actions/ui.ts index ec5af3496eae63..c243b4f0bbdc18 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/actions/ui.ts +++ b/x-pack/plugins/security/server/authorization/actions/ui.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { isString } from 'lodash'; -import { UICapabilities } from 'ui/capabilities'; -import { uiCapabilitiesRegex } from '../../../../../../../plugins/features/server'; +import { Capabilities as UICapabilities } from '../../../../../../src/core/public'; +import { uiCapabilitiesRegex } from '../../../../features/server'; export class UIActions { private readonly prefix: string; @@ -26,7 +26,7 @@ export class UIActions { return `${this.prefix}catalogue/*`; } - public get allManagmentLinks(): string { + public get allManagementLinks(): string { return `${this.prefix}management/*`; } diff --git a/x-pack/plugins/security/server/authorization/api_authorization.test.ts b/x-pack/plugins/security/server/authorization/api_authorization.test.ts new file mode 100644 index 00000000000000..a5902f251b0829 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/api_authorization.test.ts @@ -0,0 +1,155 @@ +/* + * 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 { initAPIAuthorization } from './api_authorization'; + +import { + coreMock, + httpServerMock, + httpServiceMock, + loggingServiceMock, +} from '../../../../../src/core/server/mocks'; +import { authorizationMock } from './index.mock'; + +describe('initAPIAuthorization', () => { + test(`route that doesn't start with "/api/" continues`, async () => { + const mockHTTPSetup = coreMock.createSetup().http; + initAPIAuthorization( + mockHTTPSetup, + authorizationMock.create(), + loggingServiceMock.create().get() + ); + + const [[postAuthHandler]] = mockHTTPSetup.registerOnPostAuth.mock.calls; + + const mockRequest = httpServerMock.createKibanaRequest({ method: 'get', path: '/app/foo' }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockPostAuthToolkit = httpServiceMock.createOnPostAuthToolkit(); + + await postAuthHandler(mockRequest, mockResponse, mockPostAuthToolkit); + + expect(mockResponse.notFound).not.toHaveBeenCalled(); + expect(mockPostAuthToolkit.next).toHaveBeenCalledTimes(1); + }); + + test(`protected route that starts with "/api/", but "mode.useRbacForRequest()" returns false continues`, async () => { + const mockHTTPSetup = coreMock.createSetup().http; + const mockAuthz = authorizationMock.create(); + initAPIAuthorization(mockHTTPSetup, mockAuthz, loggingServiceMock.create().get()); + + const [[postAuthHandler]] = mockHTTPSetup.registerOnPostAuth.mock.calls; + + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: '/api/foo', + routeTags: ['access:foo'], + }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockPostAuthToolkit = httpServiceMock.createOnPostAuthToolkit(); + + mockAuthz.mode.useRbacForRequest.mockReturnValue(false); + + await postAuthHandler(mockRequest, mockResponse, mockPostAuthToolkit); + + expect(mockResponse.notFound).not.toHaveBeenCalled(); + expect(mockPostAuthToolkit.next).toHaveBeenCalledTimes(1); + expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); + }); + + test(`unprotected route that starts with "/api/", but "mode.useRbacForRequest()" returns true continues`, async () => { + const mockHTTPSetup = coreMock.createSetup().http; + const mockAuthz = authorizationMock.create(); + initAPIAuthorization(mockHTTPSetup, mockAuthz, loggingServiceMock.create().get()); + + const [[postAuthHandler]] = mockHTTPSetup.registerOnPostAuth.mock.calls; + + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: '/api/foo', + routeTags: ['not-access:foo'], + }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockPostAuthToolkit = httpServiceMock.createOnPostAuthToolkit(); + + mockAuthz.mode.useRbacForRequest.mockReturnValue(true); + + await postAuthHandler(mockRequest, mockResponse, mockPostAuthToolkit); + + expect(mockResponse.notFound).not.toHaveBeenCalled(); + expect(mockPostAuthToolkit.next).toHaveBeenCalledTimes(1); + expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); + }); + + test(`protected route that starts with "/api/", "mode.useRbacForRequest()" returns true and user is authorized continues`, async () => { + const mockHTTPSetup = coreMock.createSetup().http; + const mockAuthz = authorizationMock.create({ version: '1.0.0-zeta1' }); + initAPIAuthorization(mockHTTPSetup, mockAuthz, loggingServiceMock.create().get()); + + const [[postAuthHandler]] = mockHTTPSetup.registerOnPostAuth.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: '/api/foo', + headers, + routeTags: ['access:foo'], + }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockPostAuthToolkit = httpServiceMock.createOnPostAuthToolkit(); + + const mockCheckPrivileges = jest.fn().mockReturnValue({ hasAllRequested: true }); + mockAuthz.mode.useRbacForRequest.mockReturnValue(true); + mockAuthz.checkPrivilegesDynamicallyWithRequest.mockImplementation(request => { + // hapi conceals the actual "request" from us, so we make sure that the headers are passed to + // "checkPrivilegesDynamicallyWithRequest" because this is what we're really concerned with + expect(request.headers).toMatchObject(headers); + + return mockCheckPrivileges; + }); + + await postAuthHandler(mockRequest, mockResponse, mockPostAuthToolkit); + + expect(mockResponse.notFound).not.toHaveBeenCalled(); + expect(mockPostAuthToolkit.next).toHaveBeenCalledTimes(1); + expect(mockCheckPrivileges).toHaveBeenCalledWith([mockAuthz.actions.api.get('foo')]); + expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); + }); + + test(`protected route that starts with "/api/", "mode.useRbacForRequest()" returns true and user isn't authorized responds with a 404`, async () => { + const mockHTTPSetup = coreMock.createSetup().http; + const mockAuthz = authorizationMock.create({ version: '1.0.0-zeta1' }); + initAPIAuthorization(mockHTTPSetup, mockAuthz, loggingServiceMock.create().get()); + + const [[postAuthHandler]] = mockHTTPSetup.registerOnPostAuth.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: '/api/foo', + headers, + routeTags: ['access:foo'], + }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockPostAuthToolkit = httpServiceMock.createOnPostAuthToolkit(); + + const mockCheckPrivileges = jest.fn().mockReturnValue({ hasAllRequested: false }); + mockAuthz.mode.useRbacForRequest.mockReturnValue(true); + mockAuthz.checkPrivilegesDynamicallyWithRequest.mockImplementation(request => { + // hapi conceals the actual "request" from us, so we make sure that the headers are passed to + // "checkPrivilegesDynamicallyWithRequest" because this is what we're really concerned with + expect(request.headers).toMatchObject(headers); + + return mockCheckPrivileges; + }); + + await postAuthHandler(mockRequest, mockResponse, mockPostAuthToolkit); + + expect(mockResponse.notFound).toHaveBeenCalledTimes(1); + expect(mockPostAuthToolkit.next).not.toHaveBeenCalled(); + expect(mockCheckPrivileges).toHaveBeenCalledWith([mockAuthz.actions.api.get('foo')]); + expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); + }); +}); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/api_authorization.ts b/x-pack/plugins/security/server/authorization/api_authorization.ts similarity index 54% rename from x-pack/legacy/plugins/security/server/lib/authorization/api_authorization.ts rename to x-pack/plugins/security/server/authorization/api_authorization.ts index 57dd9a4802a5ac..b280cc74c230f5 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/api_authorization.ts +++ b/x-pack/plugins/security/server/authorization/api_authorization.ts @@ -4,26 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; -import { Request, ResponseToolkit, Server } from 'hapi'; -import { AuthorizationService } from './service'; - -export function initAPIAuthorization(server: Server, authorization: AuthorizationService) { - const { actions, checkPrivilegesDynamicallyWithRequest, mode } = authorization; - - server.ext('onPostAuth', async (request: Request, h: ResponseToolkit) => { +import { CoreSetup, Logger } from '../../../../../src/core/server'; +import { Authorization } from '.'; + +export function initAPIAuthorization( + http: CoreSetup['http'], + { actions, checkPrivilegesDynamicallyWithRequest, mode }: Authorization, + logger: Logger +) { + http.registerOnPostAuth(async (request, response, toolkit) => { // if the api doesn't start with "/api/" or we aren't using RBAC for this request, just continue - if (!request.path.startsWith('/api/') || !mode.useRbacForRequest(request)) { - return h.continue; + if (!request.url.path!.startsWith('/api/') || !mode.useRbacForRequest(request)) { + return toolkit.next(); } - const { tags = [] } = request.route.settings; + const tags = request.route.options.tags; const tagPrefix = 'access:'; const actionTags = tags.filter(tag => tag.startsWith(tagPrefix)); // if there are no tags starting with "access:", just continue if (actionTags.length === 0) { - return h.continue; + logger.debug('API endpoint is not marked with "access:" tags, skipping.'); + return toolkit.next(); } const apiActions = actionTags.map(tag => actions.api.get(tag.substring(tagPrefix.length))); @@ -32,9 +34,11 @@ export function initAPIAuthorization(server: Server, authorization: Authorizatio // we've actually authorized the request if (checkPrivilegesResponse.hasAllRequested) { - return h.continue; + logger.debug(`authorized for "${request.url.path}"`); + return toolkit.next(); } - return Boom.notFound(); + logger.debug(`not authorized for "${request.url.path}"`); + return response.notFound(); }); } diff --git a/x-pack/plugins/security/server/authorization/app_authorization.test.ts b/x-pack/plugins/security/server/authorization/app_authorization.test.ts new file mode 100644 index 00000000000000..6d233330223025 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/app_authorization.test.ts @@ -0,0 +1,175 @@ +/* + * 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 { PluginSetupContract as FeaturesSetupContract } from '../../../features/server'; +import { initAppAuthorization } from './app_authorization'; + +import { + loggingServiceMock, + coreMock, + httpServerMock, + httpServiceMock, +} from '../../../../../src/core/server/mocks'; +import { authorizationMock } from './index.mock'; + +const createFeaturesSetupContractMock = (): FeaturesSetupContract => { + return { + getFeatures: () => [{ id: 'foo', name: 'Foo', app: ['foo'], privileges: {} }], + } as FeaturesSetupContract; +}; + +describe('initAppAuthorization', () => { + test(`route that doesn't start with "/app/" continues`, async () => { + const mockHTTPSetup = coreMock.createSetup().http; + initAppAuthorization( + mockHTTPSetup, + authorizationMock.create(), + loggingServiceMock.create().get(), + createFeaturesSetupContractMock() + ); + + const [[postAuthHandler]] = mockHTTPSetup.registerOnPostAuth.mock.calls; + + const mockRequest = httpServerMock.createKibanaRequest({ method: 'get', path: '/api/foo' }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockPostAuthToolkit = httpServiceMock.createOnPostAuthToolkit(); + + await postAuthHandler(mockRequest, mockResponse, mockPostAuthToolkit); + + expect(mockResponse.notFound).not.toHaveBeenCalled(); + expect(mockPostAuthToolkit.next).toHaveBeenCalledTimes(1); + }); + + test(`protected route that starts with "/app/", but "mode.useRbacForRequest()" returns false continues`, async () => { + const mockHTTPSetup = coreMock.createSetup().http; + const mockAuthz = authorizationMock.create(); + initAppAuthorization( + mockHTTPSetup, + mockAuthz, + loggingServiceMock.create().get(), + createFeaturesSetupContractMock() + ); + + const [[postAuthHandler]] = mockHTTPSetup.registerOnPostAuth.mock.calls; + + const mockRequest = httpServerMock.createKibanaRequest({ method: 'get', path: '/app/foo' }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockPostAuthToolkit = httpServiceMock.createOnPostAuthToolkit(); + + mockAuthz.mode.useRbacForRequest.mockReturnValue(false); + + await postAuthHandler(mockRequest, mockResponse, mockPostAuthToolkit); + + expect(mockResponse.notFound).not.toHaveBeenCalled(); + expect(mockPostAuthToolkit.next).toHaveBeenCalledTimes(1); + expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); + }); + + test(`unprotected route that starts with "/app/", and "mode.useRbacForRequest()" returns true continues`, async () => { + const mockHTTPSetup = coreMock.createSetup().http; + const mockAuthz = authorizationMock.create(); + initAppAuthorization( + mockHTTPSetup, + mockAuthz, + loggingServiceMock.create().get(), + createFeaturesSetupContractMock() + ); + + const [[postAuthHandler]] = mockHTTPSetup.registerOnPostAuth.mock.calls; + + const mockRequest = httpServerMock.createKibanaRequest({ method: 'get', path: '/app/bar' }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockPostAuthToolkit = httpServiceMock.createOnPostAuthToolkit(); + + mockAuthz.mode.useRbacForRequest.mockReturnValue(true); + + await postAuthHandler(mockRequest, mockResponse, mockPostAuthToolkit); + + expect(mockResponse.notFound).not.toHaveBeenCalled(); + expect(mockPostAuthToolkit.next).toHaveBeenCalledTimes(1); + expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); + }); + + test(`protected route that starts with "/app/", "mode.useRbacForRequest()" returns true and user is authorized continues`, async () => { + const mockHTTPSetup = coreMock.createSetup().http; + const mockAuthz = authorizationMock.create({ version: '1.0.0-zeta1' }); + + initAppAuthorization( + mockHTTPSetup, + mockAuthz, + loggingServiceMock.create().get(), + createFeaturesSetupContractMock() + ); + + const [[postAuthHandler]] = mockHTTPSetup.registerOnPostAuth.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: '/app/foo', + headers, + }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockPostAuthToolkit = httpServiceMock.createOnPostAuthToolkit(); + + const mockCheckPrivileges = jest.fn().mockReturnValue({ hasAllRequested: true }); + mockAuthz.mode.useRbacForRequest.mockReturnValue(true); + mockAuthz.checkPrivilegesDynamicallyWithRequest.mockImplementation(request => { + // hapi conceals the actual "request" from us, so we make sure that the headers are passed to + // "checkPrivilegesDynamicallyWithRequest" because this is what we're really concerned with + expect(request.headers).toMatchObject(headers); + + return mockCheckPrivileges; + }); + + await postAuthHandler(mockRequest, mockResponse, mockPostAuthToolkit); + + expect(mockResponse.notFound).not.toHaveBeenCalled(); + expect(mockPostAuthToolkit.next).toHaveBeenCalledTimes(1); + expect(mockCheckPrivileges).toHaveBeenCalledWith(mockAuthz.actions.app.get('foo')); + expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); + }); + + test(`protected route that starts with "/app/", "mode.useRbacForRequest()" returns true and user isn't authorized responds with a 404`, async () => { + const mockHTTPSetup = coreMock.createSetup().http; + const mockAuthz = authorizationMock.create({ version: '1.0.0-zeta1' }); + + initAppAuthorization( + mockHTTPSetup, + mockAuthz, + loggingServiceMock.create().get(), + createFeaturesSetupContractMock() + ); + + const [[postAuthHandler]] = mockHTTPSetup.registerOnPostAuth.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: '/app/foo', + headers, + }); + const mockResponse = httpServerMock.createResponseFactory(); + const mockPostAuthToolkit = httpServiceMock.createOnPostAuthToolkit(); + + const mockCheckPrivileges = jest.fn().mockReturnValue({ hasAllRequested: false }); + mockAuthz.mode.useRbacForRequest.mockReturnValue(true); + mockAuthz.checkPrivilegesDynamicallyWithRequest.mockImplementation(request => { + // hapi conceals the actual "request" from us, so we make sure that the headers are passed to + // "checkPrivilegesDynamicallyWithRequest" because this is what we're really concerned with + expect(request.headers).toMatchObject(headers); + + return mockCheckPrivileges; + }); + + await postAuthHandler(mockRequest, mockResponse, mockPostAuthToolkit); + + expect(mockResponse.notFound).toHaveBeenCalledTimes(1); + expect(mockPostAuthToolkit.next).not.toHaveBeenCalled(); + expect(mockCheckPrivileges).toHaveBeenCalledWith(mockAuthz.actions.app.get('foo')); + expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); + }); +}); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/app_authorization.ts b/x-pack/plugins/security/server/authorization/app_authorization.ts similarity index 51% rename from x-pack/legacy/plugins/security/server/lib/authorization/app_authorization.ts rename to x-pack/plugins/security/server/authorization/app_authorization.ts index dd44050ec3e2ab..8516e8228ab5ad 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/app_authorization.ts +++ b/x-pack/plugins/security/server/authorization/app_authorization.ts @@ -4,22 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; -import { Request, ResponseToolkit, Server } from 'hapi'; -import { flatten } from 'lodash'; -import { XPackMainPlugin } from '../../../../xpack_main/xpack_main'; -import { AuthorizationService } from './service'; +import { CoreSetup, Logger } from '../../../../../src/core/server'; +import { FeaturesService } from '../plugin'; +import { Authorization } from '.'; + class ProtectedApplications { private applications: Set | null = null; - constructor(private readonly xpackMainPlugin: XPackMainPlugin) {} + constructor(private readonly featuresService: FeaturesService) {} public shouldProtect(appId: string) { // Currently, once we get the list of features we essentially "lock" additional - // features from being added. This is enforced by the xpackMain plugin. As such, + // features from being added. This is enforced by the Features plugin. As such, // we wait until we actually need to consume these before getting them if (this.applications == null) { this.applications = new Set( - flatten(this.xpackMainPlugin.getFeatures().map(feature => feature.app)) + this.featuresService + .getFeatures() + .map(feature => feature.app) + .flat() ); } @@ -28,45 +30,49 @@ class ProtectedApplications { } export function initAppAuthorization( - server: Server, - xpackMainPlugin: XPackMainPlugin, - authorization: AuthorizationService + http: CoreSetup['http'], + { + actions, + checkPrivilegesDynamicallyWithRequest, + mode, + }: Pick, + logger: Logger, + featuresService: FeaturesService ) { - const { actions, checkPrivilegesDynamicallyWithRequest, mode } = authorization; - const protectedApplications = new ProtectedApplications(xpackMainPlugin); - const log = (msg: string) => server.log(['security', 'app-authorization', 'debug'], msg); + const protectedApplications = new ProtectedApplications(featuresService); + + http.registerOnPostAuth(async (request, response, toolkit) => { + const path = request.url.pathname!; - server.ext('onPostAuth', async (request: Request, h: ResponseToolkit) => { - const { path } = request; // if the path doesn't start with "/app/", just continue if (!path.startsWith('/app/')) { - return h.continue; + return toolkit.next(); } // if we aren't using RBAC, just continue if (!mode.useRbacForRequest(request)) { - return h.continue; + return toolkit.next(); } const appId = path.split('/', 3)[2]; if (!protectedApplications.shouldProtect(appId)) { - log(`not authorizing - "${appId}" isn't a protected application`); - return h.continue; + logger.debug(`not authorizing - "${appId}" isn't a protected application`); + return toolkit.next(); } const checkPrivileges = checkPrivilegesDynamicallyWithRequest(request); const appAction = actions.app.get(appId); const checkPrivilegesResponse = await checkPrivileges(appAction); - log(`authorizing access to "${appId}"`); + logger.debug(`authorizing access to "${appId}"`); // we've actually authorized the request if (checkPrivilegesResponse.hasAllRequested) { - log(`authorized for "${appId}"`); - return h.continue; + logger.debug(`authorized for "${appId}"`); + return toolkit.next(); } - log(`not authorized for "${appId}"`); - return Boom.notFound(); + logger.debug(`not authorized for "${appId}"`); + return response.notFound(); }); } diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_privileges.test.ts similarity index 94% rename from x-pack/legacy/plugins/security/server/lib/authorization/check_privileges.test.ts rename to x-pack/plugins/security/server/authorization/check_privileges.test.ts index b418e02474f4ab..b1cb78008da008 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.test.ts @@ -5,10 +5,12 @@ */ import { uniq } from 'lodash'; -import { GLOBAL_RESOURCE } from '../../../common/constants'; +import { GLOBAL_RESOURCE } from '../../common/constants'; import { checkPrivilegesWithRequestFactory } from './check_privileges'; import { HasPrivilegesResponse } from './types'; +import { elasticsearchServiceMock, httpServerMock } from '../../../../../src/core/server/mocks'; + const application = 'kibana-our_application'; const mockActions = { @@ -18,14 +20,14 @@ const mockActions = { const savedObjectTypes = ['foo-type', 'bar-type']; -const createMockShieldClient = (response: any) => { - const mockCallWithRequest = jest.fn(); +const createMockClusterClient = (response: any) => { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(response); - mockCallWithRequest.mockImplementationOnce(async () => response); + const mockClusterClient = elasticsearchServiceMock.createClusterClient(); + mockClusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - return { - callWithRequest: mockCallWithRequest, - }; + return { mockClusterClient, mockScopedClusterClient }; }; describe('#atSpace', () => { @@ -40,13 +42,15 @@ describe('#atSpace', () => { } ) => { test(description, async () => { - const mockShieldClient = createMockShieldClient(options.esHasPrivilegesResponse); + const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( + options.esHasPrivilegesResponse + ); const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( mockActions, - application, - mockShieldClient + mockClusterClient, + () => application ); - const request = { foo: Symbol() }; + const request = httpServerMock.createKibanaRequest(); const checkPrivileges = checkPrivilegesWithRequest(request); let actualResult; @@ -60,8 +64,8 @@ describe('#atSpace', () => { errorThrown = err; } - expect(mockShieldClient.callWithRequest).toHaveBeenCalledWith( - request, + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( 'shield.hasPrivileges', { body: { @@ -281,13 +285,15 @@ describe('#atSpaces', () => { } ) => { test(description, async () => { - const mockShieldClient = createMockShieldClient(options.esHasPrivilegesResponse); + const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( + options.esHasPrivilegesResponse + ); const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( mockActions, - application, - mockShieldClient + mockClusterClient, + () => application ); - const request = { foo: Symbol() }; + const request = httpServerMock.createKibanaRequest(); const checkPrivileges = checkPrivilegesWithRequest(request); let actualResult; @@ -301,8 +307,8 @@ describe('#atSpaces', () => { errorThrown = err; } - expect(mockShieldClient.callWithRequest).toHaveBeenCalledWith( - request, + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( 'shield.hasPrivileges', { body: { @@ -760,13 +766,15 @@ describe('#globally', () => { } ) => { test(description, async () => { - const mockShieldClient = createMockShieldClient(options.esHasPrivilegesResponse); + const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( + options.esHasPrivilegesResponse + ); const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( mockActions, - application, - mockShieldClient + mockClusterClient, + () => application ); - const request = { foo: Symbol() }; + const request = httpServerMock.createKibanaRequest(); const checkPrivileges = checkPrivilegesWithRequest(request); let actualResult; @@ -777,8 +785,8 @@ describe('#globally', () => { errorThrown = err; } - expect(mockShieldClient.callWithRequest).toHaveBeenCalledWith( - request, + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( 'shield.hasPrivileges', { body: { diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges.ts b/x-pack/plugins/security/server/authorization/check_privileges.ts similarity index 85% rename from x-pack/legacy/plugins/security/server/lib/authorization/check_privileges.ts rename to x-pack/plugins/security/server/authorization/check_privileges.ts index a23f89a4bd7a53..5bc3ce075452d4 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.ts @@ -5,7 +5,8 @@ */ import { pick, transform, uniq } from 'lodash'; -import { GLOBAL_RESOURCE } from '../../../common/constants'; +import { IClusterClient, KibanaRequest } from '../../../../../src/core/server'; +import { GLOBAL_RESOURCE } from '../../common/constants'; import { ResourceSerializer } from './resource_serializer'; import { HasPrivilegesResponse, HasPrivilegesResponseApplication } from './types'; import { validateEsPrivilegeResponse } from './validate_es_response'; @@ -43,7 +44,7 @@ export interface CheckPrivilegesAtSpacesResponse { }; } -export type CheckPrivilegesWithRequest = (request: Record) => CheckPrivileges; +export type CheckPrivilegesWithRequest = (request: KibanaRequest) => CheckPrivileges; export interface CheckPrivileges { atSpace( @@ -59,12 +60,10 @@ export interface CheckPrivileges { export function checkPrivilegesWithRequestFactory( actions: CheckPrivilegesActions, - application: string, - shieldClient: any + clusterClient: IClusterClient, + getApplicationName: () => string ) { - const { callWithRequest } = shieldClient; - - const hasIncompatibileVersion = ( + const hasIncompatibleVersion = ( applicationPrivilegesResponse: HasPrivilegesResponseApplication ) => { return Object.values(applicationPrivilegesResponse).some( @@ -72,7 +71,7 @@ export function checkPrivilegesWithRequestFactory( ); }; - return function checkPrivilegesWithRequest(request: Record): CheckPrivileges { + return function checkPrivilegesWithRequest(request: KibanaRequest): CheckPrivileges { const checkPrivilegesAtResources = async ( resources: string[], privilegeOrPrivileges: string | string[] @@ -82,21 +81,14 @@ export function checkPrivilegesWithRequestFactory( : [privilegeOrPrivileges]; const allApplicationPrivileges = uniq([actions.version, actions.login, ...privileges]); - const hasPrivilegesResponse: HasPrivilegesResponse = await callWithRequest( - request, - 'shield.hasPrivileges', - { + const application = getApplicationName(); + const hasPrivilegesResponse = (await clusterClient + .asScoped(request) + .callAsCurrentUser('shield.hasPrivileges', { body: { - applications: [ - { - application, - resources, - privileges: allApplicationPrivileges, - }, - ], + applications: [{ application, resources, privileges: allApplicationPrivileges }], }, - } - ); + })) as HasPrivilegesResponse; validateEsPrivilegeResponse( hasPrivilegesResponse, @@ -107,7 +99,7 @@ export function checkPrivilegesWithRequestFactory( const applicationPrivilegesResponse = hasPrivilegesResponse.application[application]; - if (hasIncompatibileVersion(applicationPrivilegesResponse)) { + if (hasIncompatibleVersion(applicationPrivilegesResponse)) { throw new Error( 'Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.' ); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.test.ts similarity index 73% rename from x-pack/legacy/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts rename to x-pack/plugins/security/server/authorization/check_privileges_dynamically.test.ts index 6df9d6801e2dc7..2206748597635b 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacySpacesPlugin } from '../../../../spaces'; -import { OptionalPlugin } from '../../../../../server/lib/optional_plugin'; import { checkPrivilegesDynamicallyWithRequestFactory } from './check_privileges_dynamically'; +import { httpServerMock } from '../../../../../src/core/server/mocks'; + test(`checkPrivileges.atSpace when spaces is enabled`, async () => { const expectedResult = Symbol(); const spaceId = 'foo-space'; @@ -15,21 +15,15 @@ test(`checkPrivileges.atSpace when spaces is enabled`, async () => { atSpace: jest.fn().mockReturnValue(expectedResult), }; const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockSpaces = { - isEnabled: true, - getSpaceId: jest.fn().mockReturnValue(spaceId), - spaceIdToNamespace: jest.fn(), - namespaceToSpaceId: jest.fn(), - getBasePath: jest.fn(), - getScopedSpacesClient: jest.fn(), - getActiveSpace: jest.fn(), - } as OptionalPlugin; - const request = Symbol(); + const request = httpServerMock.createKibanaRequest(); const privilegeOrPrivileges = ['foo', 'bar']; const checkPrivilegesDynamically = checkPrivilegesDynamicallyWithRequestFactory( mockCheckPrivilegesWithRequest, - mockSpaces - )(request as any); + () => ({ + getSpaceId: jest.fn().mockReturnValue(spaceId), + namespaceToSpaceId: jest.fn(), + }) + )(request); const result = await checkPrivilegesDynamically(privilegeOrPrivileges); expect(result).toBe(expectedResult); @@ -43,15 +37,12 @@ test(`checkPrivileges.globally when spaces is disabled`, async () => { globally: jest.fn().mockReturnValue(expectedResult), }; const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockSpaces = { - isEnabled: false, - } as OptionalPlugin; - const request = Symbol(); + const request = httpServerMock.createKibanaRequest(); const privilegeOrPrivileges = ['foo', 'bar']; const checkPrivilegesDynamically = checkPrivilegesDynamicallyWithRequestFactory( mockCheckPrivilegesWithRequest, - mockSpaces - )(request as any); + () => undefined + )(request); const result = await checkPrivilegesDynamically(privilegeOrPrivileges); expect(result).toBe(expectedResult); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges_dynamically.ts b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts similarity index 56% rename from x-pack/legacy/plugins/security/server/lib/authorization/check_privileges_dynamically.ts rename to x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts index 243ad100c5715e..0377dd06eb6692 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges_dynamically.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts @@ -4,39 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; +import { KibanaRequest } from '../../../../../src/core/server'; +import { SpacesService } from '../plugin'; import { CheckPrivilegesAtResourceResponse, CheckPrivilegesWithRequest } from './check_privileges'; -/* - * 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 { LegacySpacesPlugin } from '../../../../spaces'; -import { OptionalPlugin } from '../../../../../server/lib/optional_plugin'; - export type CheckPrivilegesDynamically = ( privilegeOrPrivileges: string | string[] ) => Promise; export type CheckPrivilegesDynamicallyWithRequest = ( - request: Legacy.Request + request: KibanaRequest ) => CheckPrivilegesDynamically; export function checkPrivilegesDynamicallyWithRequestFactory( checkPrivilegesWithRequest: CheckPrivilegesWithRequest, - spaces: OptionalPlugin + getSpacesService: () => SpacesService | undefined ): CheckPrivilegesDynamicallyWithRequest { - return function checkPrivilegesDynamicallyWithRequest(request: Legacy.Request) { + return function checkPrivilegesDynamicallyWithRequest(request: KibanaRequest) { const checkPrivileges = checkPrivilegesWithRequest(request); return async function checkPrivilegesDynamically(privilegeOrPrivileges: string | string[]) { - if (spaces.isEnabled) { - const spaceId = spaces.getSpaceId(request); - return await checkPrivileges.atSpace(spaceId, privilegeOrPrivileges); - } else { - return await checkPrivileges.globally(privilegeOrPrivileges); - } + const spacesService = getSpacesService(); + return spacesService + ? await checkPrivileges.atSpace(spacesService.getSpaceId(request), privilegeOrPrivileges) + : await checkPrivileges.globally(privilegeOrPrivileges); }; }; } diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/check_saved_objects_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts similarity index 73% rename from x-pack/legacy/plugins/security/server/lib/authorization/check_saved_objects_privileges.test.ts rename to x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts index 7fa02330fac979..4618e8e6641fcf 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/check_saved_objects_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacySpacesPlugin } from '../../../../spaces'; -import { OptionalPlugin } from '../../../../../server/lib/optional_plugin'; import { checkSavedObjectsPrivilegesWithRequestFactory } from './check_saved_objects_privileges'; +import { httpServerMock } from '../../../../../src/core/server/mocks'; + test(`checkPrivileges.atSpace when spaces is enabled`, async () => { const expectedResult = Symbol(); const spaceId = 'foo-space'; @@ -15,19 +15,17 @@ test(`checkPrivileges.atSpace when spaces is enabled`, async () => { atSpace: jest.fn().mockReturnValue(expectedResult), }; const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - - const mockSpaces = ({ - isEnabled: true, - namespaceToSpaceId: jest.fn().mockReturnValue(spaceId), - } as unknown) as OptionalPlugin; - const request = Symbol(); - + const request = httpServerMock.createKibanaRequest(); const privilegeOrPrivileges = ['foo', 'bar']; + const mockSpacesService = { + getSpaceId: jest.fn(), + namespaceToSpaceId: jest.fn().mockReturnValue(spaceId), + }; const checkSavedObjectsPrivileges = checkSavedObjectsPrivilegesWithRequestFactory( mockCheckPrivilegesWithRequest, - mockSpaces - )(request as any); + () => mockSpacesService + )(request); const namespace = 'foo'; @@ -36,7 +34,7 @@ test(`checkPrivileges.atSpace when spaces is enabled`, async () => { expect(result).toBe(expectedResult); expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, privilegeOrPrivileges); - expect(mockSpaces.namespaceToSpaceId).toBeCalledWith(namespace); + expect(mockSpacesService.namespaceToSpaceId).toBeCalledWith(namespace); }); test(`checkPrivileges.globally when spaces is disabled`, async () => { @@ -45,21 +43,15 @@ test(`checkPrivileges.globally when spaces is disabled`, async () => { globally: jest.fn().mockReturnValue(expectedResult), }; const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockSpaces = ({ - isEnabled: false, - namespaceToSpaceId: jest.fn().mockImplementation(() => { - throw new Error('should not be called'); - }), - } as unknown) as OptionalPlugin; - const request = Symbol(); + const request = httpServerMock.createKibanaRequest(); const privilegeOrPrivileges = ['foo', 'bar']; const checkSavedObjectsPrivileges = checkSavedObjectsPrivilegesWithRequestFactory( mockCheckPrivilegesWithRequest, - mockSpaces - )(request as any); + () => undefined + )(request); const namespace = 'foo'; @@ -68,5 +60,4 @@ test(`checkPrivileges.globally when spaces is disabled`, async () => { expect(result).toBe(expectedResult); expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivileges.globally).toHaveBeenCalledWith(privilegeOrPrivileges); - expect(mockSpaces.namespaceToSpaceId).not.toHaveBeenCalled(); }); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/check_saved_objects_privileges.ts b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts similarity index 65% rename from x-pack/legacy/plugins/security/server/lib/authorization/check_saved_objects_privileges.ts rename to x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts index fb1d258b5a05fb..02958fe265efac 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/check_saved_objects_privileges.ts +++ b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; -import { LegacySpacesPlugin } from '../../../../spaces'; -import { OptionalPlugin } from '../../../../../server/lib/optional_plugin'; +import { KibanaRequest } from '../../../../../src/core/server'; +import { SpacesService } from '../plugin'; import { CheckPrivilegesAtResourceResponse, CheckPrivilegesWithRequest } from './check_privileges'; export type CheckSavedObjectsPrivilegesWithRequest = ( - request: Legacy.Request + request: KibanaRequest ) => CheckSavedObjectsPrivileges; export type CheckSavedObjectsPrivileges = ( actions: string | string[], @@ -19,20 +18,20 @@ export type CheckSavedObjectsPrivileges = ( export const checkSavedObjectsPrivilegesWithRequestFactory = ( checkPrivilegesWithRequest: CheckPrivilegesWithRequest, - spaces: OptionalPlugin + getSpacesService: () => SpacesService | undefined ): CheckSavedObjectsPrivilegesWithRequest => { - return function checkSavedObjectsPrivilegesWithRequest(request: Legacy.Request) { + return function checkSavedObjectsPrivilegesWithRequest(request: KibanaRequest) { return async function checkSavedObjectsPrivileges( actions: string | string[], namespace?: string ) { - if (spaces.isEnabled) { - return checkPrivilegesWithRequest(request).atSpace( - spaces.namespaceToSpaceId(namespace), - actions - ); - } - return checkPrivilegesWithRequest(request).globally(actions); + const spacesService = getSpacesService(); + return spacesService + ? await checkPrivilegesWithRequest(request).atSpace( + spacesService.namespaceToSpaceId(namespace), + actions + ) + : await checkPrivilegesWithRequest(request).globally(actions); }; }; }; diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/disable_ui_capabilities.test.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts similarity index 54% rename from x-pack/legacy/plugins/security/server/lib/authorization/disable_ui_capabilities.test.ts rename to x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts index 198a36177c55ac..49c9db2d0e6e36 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/disable_ui_capabilities.test.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts @@ -5,79 +5,48 @@ */ import { Actions } from '.'; -import { Feature } from '../../../../../../plugins/features/server'; -import { disableUICapabilitesFactory } from './disable_ui_capabilities'; +import { disableUICapabilitiesFactory } from './disable_ui_capabilities'; -interface MockServerOptions { - checkPrivileges: { - reject?: any; - resolve?: any; - }; - features: Feature[]; -} +import { httpServerMock, loggingServiceMock } from '../../../../../src/core/server/mocks'; +import { authorizationMock } from './index.mock'; -const actions = new Actions('1.0.0-zeta1'); -const mockRequest = { - foo: Symbol(), -}; +type MockAuthzOptions = { rejectCheckPrivileges: any } | { resolveCheckPrivileges: any }; -const createMockServer = (options: MockServerOptions) => { - const mockAuthorizationService = { - actions, - checkPrivilegesDynamicallyWithRequest(request: any) { - expect(request).toBe(mockRequest); - - return jest.fn().mockImplementation(checkActions => { - if (options.checkPrivileges.reject) { - throw options.checkPrivileges.reject; - } - - if (options.checkPrivileges.resolve) { - expect(checkActions).toEqual(Object.keys(options.checkPrivileges.resolve.privileges)); - return options.checkPrivileges.resolve; - } +const actions = new Actions('1.0.0-zeta1'); +const mockRequest = httpServerMock.createKibanaRequest(); - throw new Error('resolve or reject should have been provided'); - }); - }, - }; +const createMockAuthz = (options: MockAuthzOptions) => { + const mock = authorizationMock.create({ version: '1.0.0-zeta1' }); + mock.checkPrivilegesDynamicallyWithRequest.mockImplementation(request => { + expect(request).toBe(mockRequest); - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(options.features), - }; + return jest.fn().mockImplementation(checkActions => { + if ('rejectCheckPrivileges' in options) { + throw options.rejectCheckPrivileges; + } - return { - log: jest.fn(), - plugins: { - security: { - authorization: mockAuthorizationService, - }, - xpack_main: mockXPackMainPlugin, - }, - }; + expect(checkActions).toEqual(Object.keys(options.resolveCheckPrivileges.privileges)); + return options.resolveCheckPrivileges; + }); + }); + return mock; }; describe('usingPrivileges', () => { describe('checkPrivileges errors', () => { test(`disables uiCapabilities when a 401 is thrown`, async () => { - const mockServer = createMockServer({ - checkPrivileges: { - reject: { - statusCode: 401, - message: 'super informative message', - }, - }, - features: [ - { - id: 'fooFeature', - name: 'Foo Feature', - app: [], - navLinkId: 'foo', - privileges: {}, - }, - ], + const mockAuthz = createMockAuthz({ + rejectCheckPrivileges: { statusCode: 401, message: 'super informative message' }, }); - const { usingPrivileges } = disableUICapabilitesFactory(mockServer, mockRequest); + const mockLoggers = loggingServiceMock.create(); + + const { usingPrivileges } = disableUICapabilitiesFactory( + mockRequest, + [{ id: 'fooFeature', name: 'Foo Feature', app: [], navLinkId: 'foo', privileges: {} }], + mockLoggers.get(), + mockAuthz + ); + const result = await usingPrivileges( Object.freeze({ navLinks: { @@ -122,46 +91,28 @@ describe('usingPrivileges', () => { }, }); - expect(mockServer.log).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Array [ - "security", - "debug", - ], - "Disabling all uiCapabilities because we received a 401: super informative message", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], -} -`); + expect(loggingServiceMock.collect(mockLoggers).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "Disabling all uiCapabilities because we received a 401: super informative message", + ], + ] + `); }); test(`disables uiCapabilities when a 403 is thrown`, async () => { - const mockServer = createMockServer({ - checkPrivileges: { - reject: { - statusCode: 403, - message: 'even more super informative message', - }, - }, - features: [ - { - id: 'fooFeature', - name: 'Foo Feature', - navLinkId: 'foo', - app: [], - privileges: {}, - }, - ], + const mockAuthz = createMockAuthz({ + rejectCheckPrivileges: { statusCode: 403, message: 'even more super informative message' }, }); - const { usingPrivileges } = disableUICapabilitesFactory(mockServer, mockRequest); + const mockLoggers = loggingServiceMock.create(); + + const { usingPrivileges } = disableUICapabilitiesFactory( + mockRequest, + [{ id: 'fooFeature', name: 'Foo Feature', app: [], navLinkId: 'foo', privileges: {} }], + mockLoggers.get(), + mockAuthz + ); + const result = await usingPrivileges( Object.freeze({ navLinks: { @@ -205,35 +156,28 @@ describe('usingPrivileges', () => { bar: false, }, }); - expect(mockServer.log).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Array [ - "security", - "debug", - ], - "Disabling all uiCapabilities because we received a 403: even more super informative message", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], -} -`); + expect(loggingServiceMock.collect(mockLoggers).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "Disabling all uiCapabilities because we received a 403: even more super informative message", + ], + ] + `); }); test(`otherwise it throws the error`, async () => { - const mockServer = createMockServer({ - checkPrivileges: { - reject: new Error('something else entirely'), - }, - features: [], + const mockAuthz = createMockAuthz({ + rejectCheckPrivileges: new Error('something else entirely'), }); - const { usingPrivileges } = disableUICapabilitesFactory(mockServer, mockRequest); + const mockLoggers = loggingServiceMock.create(); + + const { usingPrivileges } = disableUICapabilitiesFactory( + mockRequest, + [], + mockLoggers.get(), + mockAuthz + ); + await expect( usingPrivileges({ navLinks: { @@ -248,28 +192,40 @@ describe('usingPrivileges', () => { catalogue: {}, }) ).rejects.toThrowErrorMatchingSnapshot(); - expect(mockServer.log).not.toHaveBeenCalled(); + expect(loggingServiceMock.collect(mockLoggers)).toMatchInlineSnapshot(` + Object { + "debug": Array [], + "error": Array [], + "fatal": Array [], + "info": Array [], + "log": Array [], + "trace": Array [], + "warn": Array [], + } + `); }); }); test(`disables ui capabilities when they don't have privileges`, async () => { - const mockServer = createMockServer({ - checkPrivileges: { - resolve: { - privileges: { - [actions.ui.get('navLinks', 'foo')]: true, - [actions.ui.get('navLinks', 'bar')]: false, - [actions.ui.get('navLinks', 'quz')]: false, - [actions.ui.get('management', 'kibana', 'indices')]: true, - [actions.ui.get('management', 'kibana', 'settings')]: false, - [actions.ui.get('fooFeature', 'foo')]: true, - [actions.ui.get('fooFeature', 'bar')]: false, - [actions.ui.get('barFeature', 'foo')]: true, - [actions.ui.get('barFeature', 'bar')]: false, - }, + const mockAuthz = createMockAuthz({ + resolveCheckPrivileges: { + privileges: { + [actions.ui.get('navLinks', 'foo')]: true, + [actions.ui.get('navLinks', 'bar')]: false, + [actions.ui.get('navLinks', 'quz')]: false, + [actions.ui.get('management', 'kibana', 'indices')]: true, + [actions.ui.get('management', 'kibana', 'settings')]: false, + [actions.ui.get('fooFeature', 'foo')]: true, + [actions.ui.get('fooFeature', 'bar')]: false, + [actions.ui.get('barFeature', 'foo')]: true, + [actions.ui.get('barFeature', 'bar')]: false, }, }, - features: [ + }); + + const { usingPrivileges } = disableUICapabilitiesFactory( + mockRequest, + [ { id: 'fooFeature', name: 'Foo Feature', @@ -285,8 +241,10 @@ describe('usingPrivileges', () => { privileges: {}, }, ], - }); - const { usingPrivileges } = disableUICapabilitesFactory(mockServer, mockRequest); + loggingServiceMock.create().get(), + mockAuthz + ); + const result = await usingPrivileges( Object.freeze({ navLinks: { @@ -337,21 +295,23 @@ describe('usingPrivileges', () => { }); test(`doesn't re-enable disabled uiCapabilities`, async () => { - const mockServer = createMockServer({ - checkPrivileges: { - resolve: { - privileges: { - [actions.ui.get('navLinks', 'foo')]: true, - [actions.ui.get('navLinks', 'bar')]: true, - [actions.ui.get('management', 'kibana', 'indices')]: true, - [actions.ui.get('fooFeature', 'foo')]: true, - [actions.ui.get('fooFeature', 'bar')]: true, - [actions.ui.get('barFeature', 'foo')]: true, - [actions.ui.get('barFeature', 'bar')]: true, - }, + const mockAuthz = createMockAuthz({ + resolveCheckPrivileges: { + privileges: { + [actions.ui.get('navLinks', 'foo')]: true, + [actions.ui.get('navLinks', 'bar')]: true, + [actions.ui.get('management', 'kibana', 'indices')]: true, + [actions.ui.get('fooFeature', 'foo')]: true, + [actions.ui.get('fooFeature', 'bar')]: true, + [actions.ui.get('barFeature', 'foo')]: true, + [actions.ui.get('barFeature', 'bar')]: true, }, }, - features: [ + }); + + const { usingPrivileges } = disableUICapabilitiesFactory( + mockRequest, + [ { id: 'fooFeature', name: 'Foo Feature', @@ -367,8 +327,10 @@ describe('usingPrivileges', () => { privileges: {}, }, ], - }); - const { usingPrivileges } = disableUICapabilitesFactory(mockServer, mockRequest); + loggingServiceMock.create().get(), + mockAuthz + ); + const result = await usingPrivileges( Object.freeze({ navLinks: { @@ -417,21 +379,15 @@ describe('usingPrivileges', () => { describe('all', () => { test(`disables uiCapabilities`, () => { - const mockServer = createMockServer({ - checkPrivileges: { - reject: new Error(`Don't use me`), - }, - features: [ - { - id: 'fooFeature', - name: 'Foo Feature', - navLinkId: 'foo', - app: [], - privileges: {}, - }, - ], - }); - const { all } = disableUICapabilitesFactory(mockServer, mockRequest); + const mockAuthz = createMockAuthz({ rejectCheckPrivileges: new Error(`Don't use me`) }); + + const { all } = disableUICapabilitiesFactory( + mockRequest, + [{ id: 'fooFeature', name: 'Foo Feature', app: [], navLinkId: 'foo', privileges: {} }], + loggingServiceMock.create().get(), + mockAuthz + ); + const result = all( Object.freeze({ navLinks: { diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/disable_ui_capabilities.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts similarity index 81% rename from x-pack/legacy/plugins/security/server/lib/authorization/disable_ui_capabilities.ts rename to x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts index 4d952bca20a3da..be26f52fbf7562 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/disable_ui_capabilities.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts @@ -6,26 +6,22 @@ import { flatten, isObject, mapValues } from 'lodash'; import { UICapabilities } from 'ui/capabilities'; -import { Feature } from '../../../../../../plugins/features/server'; -import { Actions } from './actions'; +import { KibanaRequest, Logger } from '../../../../../src/core/server'; +import { Feature } from '../../../features/server'; + import { CheckPrivilegesAtResourceResponse } from './check_privileges'; -import { CheckPrivilegesDynamically } from './check_privileges_dynamically'; +import { Authorization } from './index'; -export function disableUICapabilitesFactory( - server: Record, - request: Record +export function disableUICapabilitiesFactory( + request: KibanaRequest, + features: Feature[], + logger: Logger, + authz: Authorization ) { - const { - security: { authorization }, - xpack_main: xpackMainPlugin, - } = server.plugins; - - const features: Feature[] = xpackMainPlugin.getFeatures(); const featureNavLinkIds = features .map(feature => feature.navLinkId) .filter(navLinkId => navLinkId != null); - const actions: Actions = authorization.actions; const shouldDisableFeatureUICapability = ( featureId: keyof UICapabilities, uiCapability: string @@ -61,10 +57,10 @@ export function disableUICapabilitesFactory( value: boolean | Record ): string[] { if (typeof value === 'boolean') { - return [actions.ui.get(featureId, uiCapability)]; + return [authz.actions.ui.get(featureId, uiCapability)]; } if (isObject(value)) { - return Object.keys(value).map(item => actions.ui.get(featureId, uiCapability, item)); + return Object.keys(value).map(item => authz.actions.ui.get(featureId, uiCapability, item)); } throw new Error(`Expected value type of boolean or object, but found ${value}`); } @@ -83,17 +79,14 @@ export function disableUICapabilitesFactory( let checkPrivilegesResponse: CheckPrivilegesAtResourceResponse; try { - const checkPrivilegesDynamically: CheckPrivilegesDynamically = authorization.checkPrivilegesDynamicallyWithRequest( - request - ); + const checkPrivilegesDynamically = authz.checkPrivilegesDynamicallyWithRequest(request); checkPrivilegesResponse = await checkPrivilegesDynamically(uiActions); } catch (err) { // if we get a 401/403, then we want to disable all uiCapabilities, as this // is generally when the user hasn't authenticated yet and we're displaying the // login screen, which isn't driven any uiCapabilities if (err.statusCode === 401 || err.statusCode === 403) { - server.log( - ['security', 'debug'], + logger.debug( `Disabling all uiCapabilities because we received a ${err.statusCode}: ${err.message}` ); return disableAll(uiCapabilities); @@ -107,11 +100,11 @@ export function disableUICapabilitesFactory( ...uiCapabilityParts: string[] ) => { // if the uiCapability has already been disabled, we don't want to re-enable it - if (enabled === false) { + if (!enabled) { return false; } - const action = actions.ui.get(featureId, ...uiCapabilityParts); + const action = authz.actions.ui.get(featureId, ...uiCapabilityParts); return checkPrivilegesResponse.privileges[action] === true; }; diff --git a/x-pack/plugins/security/server/authorization/index.mock.ts b/x-pack/plugins/security/server/authorization/index.mock.ts new file mode 100644 index 00000000000000..2e700745c69dcc --- /dev/null +++ b/x-pack/plugins/security/server/authorization/index.mock.ts @@ -0,0 +1,22 @@ +/* + * 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 { Actions } from '.'; +import { AuthorizationMode } from './mode'; + +export const authorizationMock = { + create: ({ version = 'mock-version' }: { version?: string } = {}) => ({ + actions: new Actions(version), + checkPrivilegesWithRequest: jest.fn(), + checkPrivilegesDynamicallyWithRequest: jest.fn(), + checkSavedObjectsPrivilegesWithRequest: jest.fn(), + getApplicationName: jest.fn().mockReturnValue('mock-application'), + mode: { useRbacForRequest: jest.fn() } as jest.Mocked, + privileges: { get: jest.fn() }, + registerPrivilegesWithCluster: jest.fn(), + disableUnauthorizedCapabilities: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/server/authorization/index.test.ts b/x-pack/plugins/security/server/authorization/index.test.ts new file mode 100644 index 00000000000000..24179e062230a9 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/index.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { + mockAuthorizationModeFactory, + mockCheckPrivilegesDynamicallyWithRequestFactory, + mockCheckPrivilegesWithRequestFactory, + mockCheckSavedObjectsPrivilegesWithRequestFactory, + mockPrivilegesFactory, +} from './service.test.mocks'; + +import { checkPrivilegesWithRequestFactory } from './check_privileges'; +import { checkPrivilegesDynamicallyWithRequestFactory } from './check_privileges_dynamically'; +import { checkSavedObjectsPrivilegesWithRequestFactory } from './check_saved_objects_privileges'; +import { authorizationModeFactory } from './mode'; +import { privilegesFactory } from './privileges'; +import { setupAuthorization } from '.'; + +import { + coreMock, + elasticsearchServiceMock, + loggingServiceMock, +} from '../../../../../src/core/server/mocks'; +import { licenseMock } from '../licensing/index.mock'; + +test(`returns exposed services`, () => { + const kibanaIndexName = '.a-kibana-index'; + const application = `kibana-${kibanaIndexName}`; + + const mockCheckPrivilegesWithRequest = Symbol(); + mockCheckPrivilegesWithRequestFactory.mockReturnValue(mockCheckPrivilegesWithRequest); + + const mockCheckPrivilegesDynamicallyWithRequest = Symbol(); + mockCheckPrivilegesDynamicallyWithRequestFactory.mockReturnValue( + mockCheckPrivilegesDynamicallyWithRequest + ); + + const mockCheckSavedObjectsPrivilegesWithRequest = Symbol(); + mockCheckSavedObjectsPrivilegesWithRequestFactory.mockReturnValue( + mockCheckSavedObjectsPrivilegesWithRequest + ); + + const mockPrivilegesService = Symbol(); + mockPrivilegesFactory.mockReturnValue(mockPrivilegesService); + const mockAuthorizationMode = Symbol(); + mockAuthorizationModeFactory.mockReturnValue(mockAuthorizationMode); + + const mockClusterClient = elasticsearchServiceMock.createClusterClient(); + const mockGetSpacesService = jest + .fn() + .mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }); + const mockFeaturesService = { getFeatures: () => [] }; + const mockGetLegacyAPI = () => ({ kibanaIndexName }); + const mockLicense = licenseMock.create(); + + const authz = setupAuthorization({ + http: coreMock.createSetup().http, + clusterClient: mockClusterClient, + license: mockLicense, + loggers: loggingServiceMock.create(), + getLegacyAPI: mockGetLegacyAPI, + packageVersion: 'some-version', + featuresService: mockFeaturesService, + getSpacesService: mockGetSpacesService, + }); + + expect(authz.actions.version).toBe('version:some-version'); + expect(authz.getApplicationName()).toBe(application); + + expect(authz.checkPrivilegesWithRequest).toBe(mockCheckPrivilegesWithRequest); + expect(checkPrivilegesWithRequestFactory).toHaveBeenCalledWith( + authz.actions, + mockClusterClient, + authz.getApplicationName + ); + + expect(authz.checkPrivilegesDynamicallyWithRequest).toBe( + mockCheckPrivilegesDynamicallyWithRequest + ); + expect(checkPrivilegesDynamicallyWithRequestFactory).toHaveBeenCalledWith( + mockCheckPrivilegesWithRequest, + mockGetSpacesService + ); + + expect(authz.checkSavedObjectsPrivilegesWithRequest).toBe( + mockCheckSavedObjectsPrivilegesWithRequest + ); + expect(checkSavedObjectsPrivilegesWithRequestFactory).toHaveBeenCalledWith( + mockCheckPrivilegesWithRequest, + mockGetSpacesService + ); + + expect(authz.privileges).toBe(mockPrivilegesService); + expect(privilegesFactory).toHaveBeenCalledWith(authz.actions, mockFeaturesService); + + expect(authz.mode).toBe(mockAuthorizationMode); + expect(authorizationModeFactory).toHaveBeenCalledWith(mockLicense); +}); diff --git a/x-pack/plugins/security/server/authorization/index.ts b/x-pack/plugins/security/server/authorization/index.ts new file mode 100644 index 00000000000000..b5f9efadbd8d0e --- /dev/null +++ b/x-pack/plugins/security/server/authorization/index.ts @@ -0,0 +1,134 @@ +/* + * 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 { UICapabilities } from 'ui/capabilities'; +import { + CoreSetup, + LoggerFactory, + KibanaRequest, + IClusterClient, +} from '../../../../../src/core/server'; + +import { FeaturesService, LegacyAPI, SpacesService } from '../plugin'; +import { Actions } from './actions'; +import { CheckPrivilegesWithRequest, checkPrivilegesWithRequestFactory } from './check_privileges'; +import { + CheckPrivilegesDynamicallyWithRequest, + checkPrivilegesDynamicallyWithRequestFactory, +} from './check_privileges_dynamically'; +import { + CheckSavedObjectsPrivilegesWithRequest, + checkSavedObjectsPrivilegesWithRequestFactory, +} from './check_saved_objects_privileges'; +import { AuthorizationMode, authorizationModeFactory } from './mode'; +import { privilegesFactory, PrivilegesService } from './privileges'; +import { initAppAuthorization } from './app_authorization'; +import { initAPIAuthorization } from './api_authorization'; +import { disableUICapabilitiesFactory } from './disable_ui_capabilities'; +import { validateFeaturePrivileges } from './validate_feature_privileges'; +import { registerPrivilegesWithCluster } from './register_privileges_with_cluster'; +import { APPLICATION_PREFIX } from '../../common/constants'; +import { SecurityLicense } from '../licensing'; + +export { Actions } from './actions'; +export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges'; + +interface SetupAuthorizationParams { + packageVersion: string; + http: CoreSetup['http']; + clusterClient: IClusterClient; + license: SecurityLicense; + loggers: LoggerFactory; + featuresService: FeaturesService; + getLegacyAPI(): Pick; + getSpacesService(): SpacesService | undefined; +} + +export interface Authorization { + actions: Actions; + checkPrivilegesWithRequest: CheckPrivilegesWithRequest; + checkPrivilegesDynamicallyWithRequest: CheckPrivilegesDynamicallyWithRequest; + checkSavedObjectsPrivilegesWithRequest: CheckSavedObjectsPrivilegesWithRequest; + getApplicationName: () => string; + mode: AuthorizationMode; + privileges: PrivilegesService; + disableUnauthorizedCapabilities: ( + request: KibanaRequest, + capabilities: UICapabilities + ) => Promise; + registerPrivilegesWithCluster: () => Promise; +} + +export function setupAuthorization({ + http, + packageVersion, + clusterClient, + license, + loggers, + featuresService, + getLegacyAPI, + getSpacesService, +}: SetupAuthorizationParams): Authorization { + const actions = new Actions(packageVersion); + const mode = authorizationModeFactory(license); + const getApplicationName = () => `${APPLICATION_PREFIX}${getLegacyAPI().kibanaIndexName}`; + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( + actions, + clusterClient, + getApplicationName + ); + const privileges = privilegesFactory(actions, featuresService); + const logger = loggers.get('authorization'); + + const authz = { + actions, + getApplicationName, + checkPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: checkPrivilegesDynamicallyWithRequestFactory( + checkPrivilegesWithRequest, + getSpacesService + ), + checkSavedObjectsPrivilegesWithRequest: checkSavedObjectsPrivilegesWithRequestFactory( + checkPrivilegesWithRequest, + getSpacesService + ), + mode, + privileges, + + async disableUnauthorizedCapabilities(request: KibanaRequest, capabilities: UICapabilities) { + // If we have a license which doesn't enable security, or we're a legacy user we shouldn't + // disable any ui capabilities + if (!mode.useRbacForRequest(request)) { + return capabilities; + } + + const disableUICapabilities = disableUICapabilitiesFactory( + request, + featuresService.getFeatures(), + logger, + authz + ); + + // if we're an anonymous route, we disable all ui capabilities + if (request.route.options.authRequired === false) { + return disableUICapabilities.all(capabilities); + } + + return await disableUICapabilities.usingPrivileges(capabilities); + }, + + registerPrivilegesWithCluster: async () => { + validateFeaturePrivileges(actions, featuresService.getFeatures()); + + await registerPrivilegesWithCluster(logger, privileges, getApplicationName(), clusterClient); + }, + }; + + initAPIAuthorization(http, authz, loggers.get('api-authorization')); + initAppAuthorization(http, authz, loggers.get('app-authorization'), featuresService); + + return authz; +} diff --git a/x-pack/plugins/security/server/authorization/mode.test.ts b/x-pack/plugins/security/server/authorization/mode.test.ts new file mode 100644 index 00000000000000..3f6aa1f68ff0dc --- /dev/null +++ b/x-pack/plugins/security/server/authorization/mode.test.ts @@ -0,0 +1,71 @@ +/* + * 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 { authorizationModeFactory } from './mode'; + +import { httpServerMock } from '../../../../../src/core/server/mocks'; +import { licenseMock } from '../licensing/index.mock'; +import { SecurityLicenseFeatures } from '../licensing/license_features'; +import { SecurityLicense } from '../licensing'; + +describe(`#useRbacForRequest`, () => { + let mockLicense: jest.Mocked; + beforeEach(() => { + mockLicense = licenseMock.create(); + mockLicense.getFeatures.mockReturnValue({ allowRbac: false } as SecurityLicenseFeatures); + }); + + test(`throws an Error if request isn't specified`, async () => { + const mode = authorizationModeFactory(mockLicense); + expect(() => mode.useRbacForRequest(undefined as any)).toThrowErrorMatchingInlineSnapshot( + `"Invalid value used as weak map key"` + ); + }); + + test(`throws an Error if request is "null"`, async () => { + const mode = authorizationModeFactory(mockLicense); + + expect(() => mode.useRbacForRequest(null as any)).toThrowErrorMatchingInlineSnapshot( + `"Invalid value used as weak map key"` + ); + }); + + test(`returns false if "allowRbac" is false`, async () => { + const mode = authorizationModeFactory(mockLicense); + + const result = mode.useRbacForRequest(httpServerMock.createKibanaRequest()); + expect(result).toBe(false); + }); + + test(`returns false if "allowRbac" is initially false, and changes to true`, async () => { + const mode = authorizationModeFactory(mockLicense); + const request = httpServerMock.createKibanaRequest(); + + expect(mode.useRbacForRequest(request)).toBe(false); + + mockLicense.getFeatures.mockReturnValue({ allowRbac: true } as SecurityLicenseFeatures); + expect(mode.useRbacForRequest(request)).toBe(false); + }); + + test(`returns true if "allowRbac" is true`, async () => { + mockLicense.getFeatures.mockReturnValue({ allowRbac: true } as SecurityLicenseFeatures); + const mode = authorizationModeFactory(mockLicense); + + const result = mode.useRbacForRequest(httpServerMock.createKibanaRequest()); + expect(result).toBe(true); + }); + + test(`returns true if "allowRbac" is initially true, and changes to false`, async () => { + mockLicense.getFeatures.mockReturnValue({ allowRbac: true } as SecurityLicenseFeatures); + const mode = authorizationModeFactory(mockLicense); + const request = httpServerMock.createKibanaRequest(); + + expect(mode.useRbacForRequest(request)).toBe(true); + + mockLicense.getFeatures.mockReturnValue({ allowRbac: false } as SecurityLicenseFeatures); + expect(mode.useRbacForRequest(request)).toBe(true); + }); +}); diff --git a/x-pack/plugins/security/server/authorization/mode.ts b/x-pack/plugins/security/server/authorization/mode.ts new file mode 100644 index 00000000000000..43ac8f43436fd5 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/mode.ts @@ -0,0 +1,25 @@ +/* + * 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 { KibanaRequest } from '../../../../../src/core/server'; +import { SecurityLicense } from '../licensing'; + +export interface AuthorizationMode { + useRbacForRequest(request: KibanaRequest): boolean; +} + +export function authorizationModeFactory(license: SecurityLicense) { + const useRbacForRequestCache = new WeakMap(); + return { + useRbacForRequest(request: KibanaRequest) { + if (!useRbacForRequestCache.has(request)) { + useRbacForRequestCache.set(request, license.getFeatures().allowRbac); + } + + return useRbacForRequestCache.get(request)!; + }, + }; +} diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privilege_serializer.test.ts b/x-pack/plugins/security/server/authorization/privilege_serializer.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/privilege_serializer.test.ts rename to x-pack/plugins/security/server/authorization/privilege_serializer.test.ts diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privilege_serializer.ts b/x-pack/plugins/security/server/authorization/privilege_serializer.ts similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/privilege_serializer.ts rename to x-pack/plugins/security/server/authorization/privilege_serializer.ts diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/api.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/api.ts similarity index 95% rename from x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/api.ts rename to x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/api.ts index 901c002bfde06d..b13132f6efbe5f 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/api.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/api.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugins/features/server'; +import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeApiBuilder extends BaseFeaturePrivilegeBuilder { diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/app.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/app.ts similarity index 95% rename from x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/app.ts rename to x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/app.ts index 4362c79dc550e9..c874886d908eb2 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/app.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/app.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugins/features/server'; +import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeAppBuilder extends BaseFeaturePrivilegeBuilder { diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/catalogue.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/catalogue.ts similarity index 95% rename from x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/catalogue.ts rename to x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/catalogue.ts index 5ed649b2726c2f..3dbe71db93f4a3 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/catalogue.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/catalogue.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugins/features/server'; +import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeCatalogueBuilder extends BaseFeaturePrivilegeBuilder { diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/feature_privilege_builder.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/feature_privilege_builder.ts similarity index 95% rename from x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/feature_privilege_builder.ts rename to x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/feature_privilege_builder.ts index 48078a26839bb7..172ab24eb7e514 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/feature_privilege_builder.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/feature_privilege_builder.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugins/features/server'; +import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { Actions } from '../../actions'; export interface FeaturePrivilegeBuilder { diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/index.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts similarity index 97% rename from x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/index.ts rename to x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts index 78e1db7a980f3c..c293319070419f 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/index.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts @@ -5,7 +5,7 @@ */ import { flatten } from 'lodash'; -import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugins/features/server'; +import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { Actions } from '../../actions'; import { FeaturePrivilegeApiBuilder } from './api'; import { FeaturePrivilegeAppBuilder } from './app'; diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/management.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/management.ts similarity index 96% rename from x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/management.ts rename to x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/management.ts index 4a008fdb096195..99a4d11fb13b77 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/management.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/management.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugins/features/server'; +import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeManagementBuilder extends BaseFeaturePrivilegeBuilder { diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/navlink.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts similarity index 94% rename from x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/navlink.ts rename to x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts index 3cd75233beffb1..dd076477a9c114 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/navlink.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugins/features/server'; +import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeNavlinkBuilder extends BaseFeaturePrivilegeBuilder { diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/saved_object.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts similarity index 97% rename from x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/saved_object.ts rename to x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts index 9bc67594b357a8..9baa8dadc29237 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/saved_object.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts @@ -5,7 +5,7 @@ */ import { flatten, uniq } from 'lodash'; -import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugins/features/server'; +import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; const readOperations: string[] = ['bulk_get', 'get', 'find']; diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/ui.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/ui.ts similarity index 94% rename from x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/ui.ts rename to x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/ui.ts index fd770b4c6263b9..28a22285c2b8f0 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/ui.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/ui.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugins/features/server'; +import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeUIBuilder extends BaseFeaturePrivilegeBuilder { diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/index.ts b/x-pack/plugins/security/server/authorization/privileges/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/privileges/index.ts rename to x-pack/plugins/security/server/authorization/privileges/index.ts diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts similarity index 99% rename from x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.test.ts rename to x-pack/plugins/security/server/authorization/privileges/privileges.test.ts index 3d673cef405348..38d4d413c591e5 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../../../../../plugins/features/server'; +import { Feature } from '../../../../features/server'; import { Actions } from '../actions'; import { privilegesFactory } from './privileges'; @@ -42,11 +42,8 @@ describe('features', () => { }, ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), - }; - - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any); + const mockFeaturesService = { getFeatures: jest.fn().mockReturnValue(features) }; + const privileges = privilegesFactory(actions, mockFeaturesService); const actual = privileges.get(); expect(actual).toHaveProperty('features.foo-feature', { diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts similarity index 90% rename from x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.ts rename to x-pack/plugins/security/server/authorization/privileges/privileges.ts index aad48584a9fca5..c73c4be8f36ac8 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts @@ -5,22 +5,22 @@ */ import { flatten, mapValues, uniq } from 'lodash'; -import { Feature } from '../../../../../../../plugins/features/server'; -import { XPackMainPlugin } from '../../../../../xpack_main/xpack_main'; -import { RawKibanaFeaturePrivileges, RawKibanaPrivileges } from '../../../../common/model'; +import { Feature } from '../../../../features/server'; +import { RawKibanaFeaturePrivileges, RawKibanaPrivileges } from '../../../common/model'; import { Actions } from '../actions'; import { featurePrivilegeBuilderFactory } from './feature_privilege_builder'; +import { FeaturesService } from '../../plugin'; export interface PrivilegesService { get(): RawKibanaPrivileges; } -export function privilegesFactory(actions: Actions, xpackMainPlugin: XPackMainPlugin) { +export function privilegesFactory(actions: Actions, featuresService: FeaturesService) { const featurePrivilegeBuilder = featurePrivilegeBuilderFactory(actions); return { get() { - const features = xpackMainPlugin.getFeatures(); + const features = featuresService.getFeatures(); const basePrivilegeFeatures = features.filter(feature => !feature.excludeFromBasePrivileges); const allActions = uniq( diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privileges_serializer.test.ts b/x-pack/plugins/security/server/authorization/privileges_serializer.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/privileges_serializer.test.ts rename to x-pack/plugins/security/server/authorization/privileges_serializer.test.ts diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privileges_serializer.ts b/x-pack/plugins/security/server/authorization/privileges_serializer.ts similarity index 97% rename from x-pack/legacy/plugins/security/server/lib/authorization/privileges_serializer.ts rename to x-pack/plugins/security/server/authorization/privileges_serializer.ts index ade90b5c52f909..3a101324ec196c 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/privileges_serializer.ts +++ b/x-pack/plugins/security/server/authorization/privileges_serializer.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RawKibanaPrivileges } from '../../../common/model'; +import { RawKibanaPrivileges } from '../../common/model'; import { PrivilegeSerializer } from './privilege_serializer'; interface SerializedPrivilege { diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/register_privileges_with_cluster.test.js b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts similarity index 74% rename from x-pack/legacy/plugins/security/server/lib/authorization/register_privileges_with_cluster.test.js rename to x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts index 23a7a0f0d01ab6..888565cd7e0ff9 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/register_privileges_with_cluster.test.js +++ b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts @@ -4,215 +4,165 @@ * you may not use this file except in compliance with the Elastic License. */ +import { IClusterClient, Logger } from '../../../../../target/types/core/server'; +import { RawKibanaPrivileges } from '../../common/model'; import { registerPrivilegesWithCluster } from './register_privileges_with_cluster'; -import { getClient } from '../../../../../server/lib/get_client_shield'; -import { buildRawKibanaPrivileges } from './privileges'; -jest.mock('../../../../../server/lib/get_client_shield', () => ({ - getClient: jest.fn(), -})); -jest.mock('./privileges', () => ({ - buildRawKibanaPrivileges: jest.fn(), -})); -const application = 'default-application'; - -const registerPrivilegesWithClusterTest = (description, { - settings = {}, - savedObjectTypes, - privilegeMap, - existingPrivileges, - throwErrorWhenDeletingPrivileges, - errorDeletingPrivilegeName, - throwErrorWhenGettingPrivileges, - throwErrorWhenPuttingPrivileges, - assert -}) => { - const registerMockCallWithInternalUser = () => { - const callWithInternalUser = jest.fn(); - getClient.mockReturnValue({ - callWithInternalUser, - }); - return callWithInternalUser; - }; - - const defaultVersion = 'default-version'; - - const createMockServer = ({ privilegeMap }) => { - const mockServer = { - config: jest.fn().mockReturnValue({ - get: jest.fn(), - }), - log: jest.fn(), - plugins: { - security: { - authorization: { - actions: Symbol(), - application, - privileges: { - get: () => privilegeMap - } - } - } - } - }; - - const defaultSettings = { - 'pkg.version': defaultVersion, - }; - - mockServer.config().get.mockImplementation(key => { - return key in settings ? settings[key] : defaultSettings[key]; - }); - - mockServer.savedObjects = { - types: savedObjectTypes - }; +import { elasticsearchServiceMock, loggingServiceMock } from '../../../../../src/core/server/mocks'; - return mockServer; - }; - - const createExpectUpdatedPrivileges = (mockServer, mockCallWithInternalUser, error) => { - return (postPrivilegesBody, deletedPrivileges = []) => { +const application = 'default-application'; +const registerPrivilegesWithClusterTest = ( + description: string, + { + privilegeMap, + existingPrivileges, + throwErrorWhenGettingPrivileges, + throwErrorWhenPuttingPrivileges, + assert, + }: { + privilegeMap: RawKibanaPrivileges; + existingPrivileges?: Record> | null; + throwErrorWhenGettingPrivileges?: Error; + throwErrorWhenPuttingPrivileges?: Error; + assert: (arg: { + expectUpdatedPrivileges: (postPrivilegesBody: any, deletedPrivileges?: string[]) => void; + expectDidntUpdatePrivileges: () => void; + expectErrorThrown: (expectedErrorMessage: string) => void; + }) => void; + } +) => { + const createExpectUpdatedPrivileges = ( + mockClusterClient: jest.Mocked, + mockLogger: jest.Mocked, + error: Error + ) => { + return (postPrivilegesBody: any, deletedPrivileges: string[] = []) => { expect(error).toBeUndefined(); - expect(mockCallWithInternalUser).toHaveBeenCalledTimes(2 + deletedPrivileges.length); - expect(mockCallWithInternalUser).toHaveBeenCalledWith('shield.getPrivilege', { + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes( + 2 + deletedPrivileges.length + ); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('shield.getPrivilege', { privilege: application, }); - expect(mockCallWithInternalUser).toHaveBeenCalledWith( - 'shield.postPrivileges', - { - body: postPrivilegesBody, - } - ); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('shield.postPrivileges', { + body: postPrivilegesBody, + }); for (const deletedPrivilege of deletedPrivileges) { - expect(mockServer.log).toHaveBeenCalledWith( - ['security', 'debug'], + expect(mockLogger.debug).toHaveBeenCalledWith( `Deleting Kibana Privilege ${deletedPrivilege} from Elasticearch for ${application}` ); - expect(mockCallWithInternalUser).toHaveBeenCalledWith( + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( 'shield.deletePrivilege', - { - application, - privilege: deletedPrivilege - } + { application, privilege: deletedPrivilege } ); } - expect(mockServer.log).toHaveBeenCalledWith( - ['security', 'debug'], + + expect(mockLogger.debug).toHaveBeenCalledWith( `Registering Kibana Privileges with Elasticsearch for ${application}` ); - expect(mockServer.log).toHaveBeenCalledWith( - ['security', 'debug'], - `Updated Kibana Privileges with Elasticearch for ${application}` + expect(mockLogger.debug).toHaveBeenCalledWith( + `Updated Kibana Privileges with Elasticsearch for ${application}` ); }; }; - const createExpectDidntUpdatePrivileges = (mockServer, mockCallWithInternalUser, error) => { + const createExpectDidntUpdatePrivileges = ( + mockClusterClient: jest.Mocked, + mockLogger: Logger, + error: Error + ) => { return () => { expect(error).toBeUndefined(); - expect(mockCallWithInternalUser).toHaveBeenCalledTimes(1); - expect(mockCallWithInternalUser).toHaveBeenLastCalledWith('shield.getPrivilege', { - privilege: application + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockClusterClient.callAsInternalUser).toHaveBeenLastCalledWith('shield.getPrivilege', { + privilege: application, }); - expect(mockServer.log).toHaveBeenCalledWith( - ['security', 'debug'], + expect(mockLogger.debug).toHaveBeenCalledWith( `Registering Kibana Privileges with Elasticsearch for ${application}` ); - expect(mockServer.log).toHaveBeenCalledWith( - ['security', 'debug'], + expect(mockLogger.debug).toHaveBeenCalledWith( `Kibana Privileges already registered with Elasticearch for ${application}` ); }; }; - const createExpectErrorThrown = (mockServer, actualError) => { - return (expectedErrorMessage) => { + const createExpectErrorThrown = (mockLogger: Logger, actualError: Error) => { + return (expectedErrorMessage: string) => { expect(actualError).toBeDefined(); expect(actualError).toBeInstanceOf(Error); expect(actualError.message).toEqual(expectedErrorMessage); - if (throwErrorWhenDeletingPrivileges) { - expect(mockServer.log).toHaveBeenCalledWith( - ['security', 'error'], - `Error deleting Kibana Privilege ${errorDeletingPrivilegeName}` - ); - } - - expect(mockServer.log).toHaveBeenCalledWith( - ['security', 'error'], + expect(mockLogger.error).toHaveBeenCalledWith( `Error registering Kibana Privileges with Elasticsearch for ${application}: ${expectedErrorMessage}` ); }; }; test(description, async () => { - const mockServer = createMockServer({ - privilegeMap - }); - const mockCallWithInternalUser = registerMockCallWithInternalUser() - .mockImplementation((api) => { - switch(api) { - case 'shield.getPrivilege': { - if (throwErrorWhenGettingPrivileges) { - throw throwErrorWhenGettingPrivileges; - } - - // ES returns an empty object if we don't have any privileges - if (!existingPrivileges) { - return {}; - } - - return existingPrivileges; + const mockClusterClient = elasticsearchServiceMock.createClusterClient(); + mockClusterClient.callAsInternalUser.mockImplementation(async api => { + switch (api) { + case 'shield.getPrivilege': { + if (throwErrorWhenGettingPrivileges) { + throw throwErrorWhenGettingPrivileges; } - case 'shield.deletePrivilege': { - if (throwErrorWhenDeletingPrivileges) { - throw throwErrorWhenDeletingPrivileges; - } - break; + // ES returns an empty object if we don't have any privileges + if (!existingPrivileges) { + return {}; } - case 'shield.postPrivileges': { - if (throwErrorWhenPuttingPrivileges) { - throw throwErrorWhenPuttingPrivileges; - } - return; - } - default: { - expect(true).toBe(false); + return existingPrivileges; + } + case 'shield.deletePrivilege': { + break; + } + case 'shield.postPrivileges': { + if (throwErrorWhenPuttingPrivileges) { + throw throwErrorWhenPuttingPrivileges; } + + return; } - }); + default: { + expect(true).toBe(false); + } + } + }); + const mockLogger = loggingServiceMock.create().get() as jest.Mocked; let error; try { - await registerPrivilegesWithCluster(mockServer); + await registerPrivilegesWithCluster( + mockLogger, + { get: jest.fn().mockReturnValue(privilegeMap) }, + application, + mockClusterClient + ); } catch (err) { error = err; } assert({ - expectUpdatedPrivileges: createExpectUpdatedPrivileges(mockServer, mockCallWithInternalUser, error), - expectDidntUpdatePrivileges: createExpectDidntUpdatePrivileges(mockServer, mockCallWithInternalUser, error), - expectErrorThrown: createExpectErrorThrown(mockServer, error), - mocks: { - buildRawKibanaPrivileges, - server: mockServer, - } + expectUpdatedPrivileges: createExpectUpdatedPrivileges(mockClusterClient, mockLogger, error), + expectDidntUpdatePrivileges: createExpectDidntUpdatePrivileges( + mockClusterClient, + mockLogger, + error + ), + expectErrorThrown: createExpectErrorThrown(mockLogger, error), }); }); }; registerPrivilegesWithClusterTest(`inserts privileges when we don't have any existing privileges`, { privilegeMap: { - features: {}, global: { - all: ['action:all'] + all: ['action:all'], }, space: { - read: ['action:read'] + read: ['action:read'], }, features: { foo: { @@ -220,11 +170,11 @@ registerPrivilegesWithClusterTest(`inserts privileges when we don't have any exi }, bar: { read: ['action:bar_read'], - } + }, }, reserved: { - customApplication: ['action:customApplication'] - } + customApplication: ['action:customApplication'], + }, }, existingPrivileges: null, assert: ({ expectUpdatedPrivileges }) => { @@ -259,10 +209,10 @@ registerPrivilegesWithClusterTest(`inserts privileges when we don't have any exi name: 'reserved_customApplication', actions: ['action:customApplication'], metadata: {}, - } - } + }, + }, }); - } + }, }); registerPrivilegesWithClusterTest(`deletes no-longer specified privileges`, { @@ -272,7 +222,7 @@ registerPrivilegesWithClusterTest(`deletes no-longer specified privileges`, { all: ['action:foo'], }, space: { - read: ['action:bar'] + read: ['action:bar'], }, reserved: {}, }, @@ -307,49 +257,51 @@ registerPrivilegesWithClusterTest(`deletes no-longer specified privileges`, { name: 'reserved_customApplication', actions: ['action:customApplication'], metadata: {}, - } - } + }, + }, }, assert: ({ expectUpdatedPrivileges }) => { - expectUpdatedPrivileges({ - [application]: { - all: { - application, - name: 'all', - actions: ['action:foo'], - metadata: {}, + expectUpdatedPrivileges( + { + [application]: { + all: { + application, + name: 'all', + actions: ['action:foo'], + metadata: {}, + }, + space_read: { + application, + name: 'space_read', + actions: ['action:bar'], + metadata: {}, + }, }, - space_read: { - application, - name: 'space_read', - actions: ['action:bar'], - metadata: {}, - } - } - }, ['read', 'space_baz', 'reserved_customApplication']); - } + }, + ['read', 'space_baz', 'reserved_customApplication'] + ); + }, }); registerPrivilegesWithClusterTest(`updates privileges when global actions don't match`, { privilegeMap: { - features: {}, global: { - all: ['action:foo'] + all: ['action:foo'], }, space: { - read: ['action:bar'] + read: ['action:bar'], }, features: { foo: { - all: ['action:baz'] + all: ['action:baz'], }, bar: { - read: ['action:quz'] - } + read: ['action:quz'], + }, }, reserved: { - customApplication: ['action:customApplication'] - } + customApplication: ['action:customApplication'], + }, }, existingPrivileges: { [application]: { @@ -380,8 +332,8 @@ registerPrivilegesWithClusterTest(`updates privileges when global actions don't name: 'reserved_customApplication', actions: ['action:customApplication'], metadata: {}, - } - } + }, + }, }, assert: ({ expectUpdatedPrivileges }) => { expectUpdatedPrivileges({ @@ -415,32 +367,31 @@ registerPrivilegesWithClusterTest(`updates privileges when global actions don't name: 'reserved_customApplication', actions: ['action:customApplication'], metadata: {}, - } - } + }, + }, }); - } + }, }); registerPrivilegesWithClusterTest(`updates privileges when space actions don't match`, { privilegeMap: { - features: {}, global: { - all: ['action:foo'] + all: ['action:foo'], }, space: { - read: ['action:bar'] + read: ['action:bar'], }, features: { foo: { - all: ['action:baz'] + all: ['action:baz'], }, bar: { - read: ['action:quz'] - } + read: ['action:quz'], + }, }, reserved: { - customApplication: ['action:customApplication'] - } + customApplication: ['action:customApplication'], + }, }, existingPrivileges: { [application]: { @@ -471,8 +422,8 @@ registerPrivilegesWithClusterTest(`updates privileges when space actions don't m name: 'reserved_customApplication', actions: ['action:customApplication'], metadata: {}, - } - } + }, + }, }, assert: ({ expectUpdatedPrivileges }) => { expectUpdatedPrivileges({ @@ -506,32 +457,31 @@ registerPrivilegesWithClusterTest(`updates privileges when space actions don't m name: 'reserved_customApplication', actions: ['action:customApplication'], metadata: {}, - } - } + }, + }, }); - } + }, }); registerPrivilegesWithClusterTest(`updates privileges when feature actions don't match`, { privilegeMap: { - features: {}, global: { - all: ['action:foo'] + all: ['action:foo'], }, space: { - read: ['action:bar'] + read: ['action:bar'], }, features: { foo: { - all: ['action:baz'] + all: ['action:baz'], }, bar: { - read: ['action:quz'] - } + read: ['action:quz'], + }, }, reserved: { - customApplication: ['action:customApplication'] - } + customApplication: ['action:customApplication'], + }, }, existingPrivileges: { [application]: { @@ -562,8 +512,8 @@ registerPrivilegesWithClusterTest(`updates privileges when feature actions don't name: 'reserved_customApplication', actions: ['action:customApplication'], metadata: {}, - } - } + }, + }, }, assert: ({ expectUpdatedPrivileges }) => { expectUpdatedPrivileges({ @@ -597,29 +547,28 @@ registerPrivilegesWithClusterTest(`updates privileges when feature actions don't name: 'reserved_customApplication', actions: ['action:customApplication'], metadata: {}, - } - } + }, + }, }); - } + }, }); registerPrivilegesWithClusterTest(`updates privileges when reserved actions don't match`, { privilegeMap: { - features: {}, global: { - all: ['action:foo'] + all: ['action:foo'], }, space: { - read: ['action:bar'] + read: ['action:bar'], }, features: { foo: { - all: ['action:baz'] - } + all: ['action:baz'], + }, }, reserved: { - customApplication: ['action:customApplication'] - } + customApplication: ['action:customApplication'], + }, }, existingPrivileges: { [application]: { @@ -645,8 +594,8 @@ registerPrivilegesWithClusterTest(`updates privileges when reserved actions don' name: 'reserved_customApplication', actions: ['action:not-customApplication'], metadata: {}, - } - } + }, + }, }, assert: ({ expectUpdatedPrivileges }) => { expectUpdatedPrivileges({ @@ -674,29 +623,29 @@ registerPrivilegesWithClusterTest(`updates privileges when reserved actions don' name: 'reserved_customApplication', actions: ['action:customApplication'], metadata: {}, - } - } + }, + }, }); - } + }, }); registerPrivilegesWithClusterTest(`updates privileges when global privilege added`, { privilegeMap: { global: { all: ['action:foo'], - read: ['action:quz'] + read: ['action:quz'], }, space: { - read: ['action:bar'] + read: ['action:bar'], }, features: { foo: { - all: ['action:foo-all'] - } + all: ['action:foo-all'], + }, }, reserved: { - customApplication: ['action:customApplication'] - } + customApplication: ['action:customApplication'], + }, }, existingPrivileges: { [application]: { @@ -723,8 +672,8 @@ registerPrivilegesWithClusterTest(`updates privileges when global privilege adde name: 'reserved_customApplication', actions: ['action:customApplication'], metadata: {}, - } - } + }, + }, }, assert: ({ expectUpdatedPrivileges }) => { expectUpdatedPrivileges({ @@ -758,10 +707,10 @@ registerPrivilegesWithClusterTest(`updates privileges when global privilege adde name: 'reserved_customApplication', actions: ['action:customApplication'], metadata: {}, - } - } + }, + }, }); - } + }, }); registerPrivilegesWithClusterTest(`updates privileges when space privilege added`, { @@ -771,16 +720,16 @@ registerPrivilegesWithClusterTest(`updates privileges when space privilege added }, space: { all: ['action:bar'], - read: ['action:quz'] + read: ['action:quz'], }, features: { foo: { - all: ['action:foo-all'] - } + all: ['action:foo-all'], + }, }, reserved: { - customApplication: ['action:customApplication'] - } + customApplication: ['action:customApplication'], + }, }, existingPrivileges: { [application]: { @@ -807,8 +756,8 @@ registerPrivilegesWithClusterTest(`updates privileges when space privilege added name: 'reserved_customApplication', actions: ['action:customApplication'], metadata: {}, - } - } + }, + }, }, assert: ({ expectUpdatedPrivileges }) => { expectUpdatedPrivileges({ @@ -842,15 +791,14 @@ registerPrivilegesWithClusterTest(`updates privileges when space privilege added name: 'reserved_customApplication', actions: ['action:customApplication'], metadata: {}, - } - } + }, + }, }); - } + }, }); registerPrivilegesWithClusterTest(`updates privileges when feature privilege added`, { privilegeMap: { - features: {}, global: { all: ['action:foo'], }, @@ -860,12 +808,12 @@ registerPrivilegesWithClusterTest(`updates privileges when feature privilege add features: { foo: { all: ['action:foo-all'], - read: ['action:foo-read'] - } + read: ['action:foo-read'], + }, }, reserved: { - customApplication: ['action:customApplication'] - } + customApplication: ['action:customApplication'], + }, }, existingPrivileges: { [application]: { @@ -892,8 +840,8 @@ registerPrivilegesWithClusterTest(`updates privileges when feature privilege add name: 'reserved_customApplication', actions: ['action:customApplication'], metadata: {}, - } - } + }, + }, }, assert: ({ expectUpdatedPrivileges }) => { expectUpdatedPrivileges({ @@ -927,15 +875,14 @@ registerPrivilegesWithClusterTest(`updates privileges when feature privilege add name: 'reserved_customApplication', actions: ['action:customApplication'], metadata: {}, - } - } + }, + }, }); - } + }, }); registerPrivilegesWithClusterTest(`updates privileges when reserved privilege added`, { privilegeMap: { - features: {}, global: { all: ['action:foo'], }, @@ -945,12 +892,12 @@ registerPrivilegesWithClusterTest(`updates privileges when reserved privilege ad features: { foo: { all: ['action:foo-all'], - } + }, }, reserved: { customApplication1: ['action:customApplication1'], - customApplication2: ['action:customApplication2'] - } + customApplication2: ['action:customApplication2'], + }, }, existingPrivileges: { [application]: { @@ -977,8 +924,8 @@ registerPrivilegesWithClusterTest(`updates privileges when reserved privilege ad name: 'reserved_customApplication1', actions: ['action:customApplication1'], metadata: {}, - } - } + }, + }, }, assert: ({ expectUpdatedPrivileges }) => { expectUpdatedPrivileges({ @@ -1012,28 +959,28 @@ registerPrivilegesWithClusterTest(`updates privileges when reserved privilege ad name: 'reserved_customApplication2', actions: ['action:customApplication2'], metadata: {}, - } - } + }, + }, }); - } + }, }); registerPrivilegesWithClusterTest(`doesn't update privileges when order of actions differ`, { privilegeMap: { global: { - all: ['action:foo', 'action:quz'] + all: ['action:foo', 'action:quz'], }, space: { - read: ['action:bar', 'action:quz'] + read: ['action:bar', 'action:quz'], }, features: { foo: { - all: ['action:foo-all', 'action:bar-all'] - } + all: ['action:foo-all', 'action:bar-all'], + }, }, reserved: { - customApplication: ['action:customApplication1', 'action:customApplication2'] - } + customApplication: ['action:customApplication1', 'action:customApplication2'], + }, }, existingPrivileges: { [application]: { @@ -1060,12 +1007,12 @@ registerPrivilegesWithClusterTest(`doesn't update privileges when order of actio name: 'reserved_customApplication', actions: ['action:customApplication2', 'action:customApplication1'], metadata: {}, - } - } + }, + }, }, assert: ({ expectDidntUpdatePrivileges }) => { expectDidntUpdatePrivileges(); - } + }, }); registerPrivilegesWithClusterTest(`throws and logs error when errors getting privileges`, { @@ -1078,17 +1025,17 @@ registerPrivilegesWithClusterTest(`throws and logs error when errors getting pri throwErrorWhenGettingPrivileges: new Error('Error getting privileges'), assert: ({ expectErrorThrown }) => { expectErrorThrown('Error getting privileges'); - } + }, }); registerPrivilegesWithClusterTest(`throws and logs error when errors putting privileges`, { privilegeMap: { features: {}, global: { - all: [] + all: [], }, space: { - read: [] + read: [], }, reserved: {}, }, @@ -1096,5 +1043,5 @@ registerPrivilegesWithClusterTest(`throws and logs error when errors putting pri throwErrorWhenPuttingPrivileges: new Error('Error putting privileges'), assert: ({ expectErrorThrown }) => { expectErrorThrown('Error putting privileges'); - } + }, }); diff --git a/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.ts b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.ts new file mode 100644 index 00000000000000..22e7830d20e28e --- /dev/null +++ b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.ts @@ -0,0 +1,92 @@ +/* + * 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 { isEqual, difference } from 'lodash'; +import { IClusterClient, Logger } from '../../../../../src/core/server'; + +import { serializePrivileges } from './privileges_serializer'; +import { PrivilegesService } from './privileges'; + +export async function registerPrivilegesWithCluster( + logger: Logger, + privileges: PrivilegesService, + application: string, + clusterClient: IClusterClient +) { + const arePrivilegesEqual = ( + existingPrivileges: Record, + expectedPrivileges: Record + ) => { + // when comparing privileges, the order of the actions doesn't matter, lodash's isEqual + // doesn't know how to compare Sets + return isEqual(existingPrivileges, expectedPrivileges, (value, other, key) => { + if (key === 'actions' && Array.isArray(value) && Array.isArray(other)) { + // Array.sort() is in-place, and we don't want to be modifying the actual order + // of the arrays permanently, and there's potential they're frozen, so we're copying + // before comparing. + return isEqual([...value].sort(), [...other].sort()); + } + + // Lodash types aren't correct, `undefined` should be supported as a return value here and it + // has special meaning. + return undefined as any; + }); + }; + + const getPrivilegesToDelete = ( + existingPrivileges: Record, + expectedPrivileges: Record + ) => { + if (Object.keys(existingPrivileges).length === 0) { + return []; + } + + return difference( + Object.keys(existingPrivileges[application]), + Object.keys(expectedPrivileges[application]) + ); + }; + + const expectedPrivileges = serializePrivileges(application, privileges.get()); + + logger.debug(`Registering Kibana Privileges with Elasticsearch for ${application}`); + + try { + // we only want to post the privileges when they're going to change as Elasticsearch has + // to clear the role cache to get these changes reflected in the _has_privileges API + const existingPrivileges = await clusterClient.callAsInternalUser('shield.getPrivilege', { + privilege: application, + }); + if (arePrivilegesEqual(existingPrivileges, expectedPrivileges)) { + logger.debug(`Kibana Privileges already registered with Elasticearch for ${application}`); + return; + } + + const privilegesToDelete = getPrivilegesToDelete(existingPrivileges, expectedPrivileges); + for (const privilegeToDelete of privilegesToDelete) { + logger.debug( + `Deleting Kibana Privilege ${privilegeToDelete} from Elasticearch for ${application}` + ); + try { + await clusterClient.callAsInternalUser('shield.deletePrivilege', { + application, + privilege: privilegeToDelete, + }); + } catch (err) { + logger.error(`Error deleting Kibana Privilege ${privilegeToDelete}`); + throw err; + } + } + + await clusterClient.callAsInternalUser('shield.postPrivileges', { body: expectedPrivileges }); + logger.debug(`Updated Kibana Privileges with Elasticsearch for ${application}`); + } catch (err) { + logger.error( + `Error registering Kibana Privileges with Elasticsearch for ${application}: ${err.message}` + ); + throw err; + } +} diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/resource_serializer.test.ts b/x-pack/plugins/security/server/authorization/resource_serializer.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/resource_serializer.test.ts rename to x-pack/plugins/security/server/authorization/resource_serializer.test.ts diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/resource_serializer.ts b/x-pack/plugins/security/server/authorization/resource_serializer.ts similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/resource_serializer.ts rename to x-pack/plugins/security/server/authorization/resource_serializer.ts diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/service.test.mocks.ts b/x-pack/plugins/security/server/authorization/service.test.mocks.ts similarity index 81% rename from x-pack/legacy/plugins/security/server/lib/authorization/service.test.mocks.ts rename to x-pack/plugins/security/server/authorization/service.test.mocks.ts index a766b60894d996..5cd2eac20094dd 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/service.test.mocks.ts +++ b/x-pack/plugins/security/server/authorization/service.test.mocks.ts @@ -19,16 +19,6 @@ jest.mock('./check_saved_objects_privileges', () => ({ checkSavedObjectsPrivilegesWithRequestFactory: mockCheckSavedObjectsPrivilegesWithRequestFactory, })); -export const mockGetClient = jest.fn(); -jest.mock('../../../../../server/lib/get_client_shield', () => ({ - getClient: mockGetClient, -})); - -export const mockActionsFactory = jest.fn(); -jest.mock('./actions', () => ({ - actionsFactory: mockActionsFactory, -})); - export const mockPrivilegesFactory = jest.fn(); jest.mock('./privileges', () => ({ privilegesFactory: mockPrivilegesFactory, diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/types.ts b/x-pack/plugins/security/server/authorization/types.ts similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/types.ts rename to x-pack/plugins/security/server/authorization/types.ts diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/validate_es_response.test.ts b/x-pack/plugins/security/server/authorization/validate_es_response.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/validate_es_response.test.ts rename to x-pack/plugins/security/server/authorization/validate_es_response.test.ts diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/validate_es_response.ts b/x-pack/plugins/security/server/authorization/validate_es_response.ts similarity index 100% rename from x-pack/legacy/plugins/security/server/lib/authorization/validate_es_response.ts rename to x-pack/plugins/security/server/authorization/validate_es_response.ts diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/validate_feature_privileges.test.ts b/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts similarity index 84% rename from x-pack/legacy/plugins/security/server/lib/authorization/validate_feature_privileges.test.ts rename to x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts index 6745a00091ceef..3dc3ae03b18cbf 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/validate_feature_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts @@ -4,20 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../../../../plugins/features/server'; -import { actionsFactory } from './actions'; +import { Feature } from '../../../features/server'; +import { Actions } from './actions'; import { validateFeaturePrivileges } from './validate_feature_privileges'; -const mockConfig = { - get: (key: string) => { - if (key === 'pkg.version') { - return `1.0.0-zeta1`; - } - - throw new Error(`Mock config doesn't know about key ${key}`); - }, -}; -const actions = actionsFactory(mockConfig); +const actions = new Actions('1.0.0-zeta1'); it(`doesn't allow read to grant privileges which aren't also included in all`, () => { const feature: Feature = { diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/validate_feature_privileges.ts b/x-pack/plugins/security/server/authorization/validate_feature_privileges.ts similarity index 87% rename from x-pack/legacy/plugins/security/server/lib/authorization/validate_feature_privileges.ts rename to x-pack/plugins/security/server/authorization/validate_feature_privileges.ts index 0e40ae36c4f729..7998c816ae1c72 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/validate_feature_privileges.ts +++ b/x-pack/plugins/security/server/authorization/validate_feature_privileges.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../../../../plugins/features/server'; -import { areActionsFullyCovered } from '../../../common/privilege_calculator_utils'; +import { Feature } from '../../../features/server'; +import { areActionsFullyCovered } from '../../common/privilege_calculator_utils'; import { Actions } from './actions'; import { featurePrivilegeBuilderFactory } from './privileges/feature_privilege_builder'; diff --git a/x-pack/plugins/security/server/licensing/index.mock.ts b/x-pack/plugins/security/server/licensing/index.mock.ts new file mode 100644 index 00000000000000..b38f031c4ee7dd --- /dev/null +++ b/x-pack/plugins/security/server/licensing/index.mock.ts @@ -0,0 +1,14 @@ +/* + * 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 { SecurityLicense } from '.'; + +export const licenseMock = { + create: (): jest.Mocked => ({ + isEnabled: jest.fn().mockReturnValue(true), + getFeatures: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/server/licensing/index.ts b/x-pack/plugins/security/server/licensing/index.ts new file mode 100644 index 00000000000000..9ddbe86167367f --- /dev/null +++ b/x-pack/plugins/security/server/licensing/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { SecurityLicenseService, SecurityLicense } from './license_service'; diff --git a/x-pack/plugins/security/server/licensing/license_features.ts b/x-pack/plugins/security/server/licensing/license_features.ts new file mode 100644 index 00000000000000..6b6c86d48c21e4 --- /dev/null +++ b/x-pack/plugins/security/server/licensing/license_features.ts @@ -0,0 +1,50 @@ +/* + * 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. + */ + +/** + * Describes Security plugin features that depend on license. + */ +export interface SecurityLicenseFeatures { + /** + * Indicates whether we show login page or skip it. + */ + readonly showLogin: boolean; + + /** + * Indicates whether we allow login or disable it on the login page. + */ + readonly allowLogin: boolean; + + /** + * Indicates whether we show security links throughout the kibana app. + */ + readonly showLinks: boolean; + + /** + * Indicates whether we allow users to define document level security in roles. + */ + readonly allowRoleDocumentLevelSecurity: boolean; + + /** + * Indicates whether we allow users to define field level security in roles. + */ + readonly allowRoleFieldLevelSecurity: boolean; + + /** + * Indicates whether we allow Role-based access control (RBAC). + */ + readonly allowRbac: boolean; + + /** + * Describes the layout of the login form if it's displayed. + */ + readonly layout?: string; + + /** + * Message to show when security links are clicked throughout the kibana app. + */ + readonly linksMessage?: string; +} diff --git a/x-pack/plugins/security/server/licensing/license_service.test.ts b/x-pack/plugins/security/server/licensing/license_service.test.ts new file mode 100644 index 00000000000000..16d7599ca4b1a4 --- /dev/null +++ b/x-pack/plugins/security/server/licensing/license_service.test.ts @@ -0,0 +1,104 @@ +/* + * 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 { ILicense } from '../../../licensing/server'; +import { SecurityLicenseService } from './license_service'; + +function getMockRawLicense({ isAvailable = false } = {}) { + return ({ + isAvailable, + isOneOf: jest.fn(), + getFeature: jest.fn(), + } as unknown) as jest.Mocked; +} + +describe('license features', function() { + it('should display error when ES is unavailable', () => { + const serviceSetup = new SecurityLicenseService().setup(); + expect(serviceSetup.license.getFeatures()).toEqual({ + showLogin: true, + allowLogin: false, + showLinks: false, + allowRoleDocumentLevelSecurity: false, + allowRoleFieldLevelSecurity: false, + layout: 'error-es-unavailable', + allowRbac: false, + }); + }); + + it('should display error when X-Pack is unavailable', () => { + const serviceSetup = new SecurityLicenseService().setup(); + serviceSetup.update(getMockRawLicense({ isAvailable: false })); + expect(serviceSetup.license.getFeatures()).toEqual({ + showLogin: true, + allowLogin: false, + showLinks: false, + allowRoleDocumentLevelSecurity: false, + allowRoleFieldLevelSecurity: false, + layout: 'error-xpack-unavailable', + allowRbac: false, + }); + }); + + it('should show login page and other security elements, allow RBAC but forbid document level security if license is not platinum or trial.', () => { + const mockRawLicense = getMockRawLicense({ isAvailable: true }); + mockRawLicense.isOneOf.mockImplementation(licenses => + Array.isArray(licenses) ? licenses.includes('basic') : licenses === 'basic' + ); + mockRawLicense.getFeature.mockReturnValue({ isEnabled: true, isAvailable: true } as any); + + const serviceSetup = new SecurityLicenseService().setup(); + serviceSetup.update(mockRawLicense); + expect(serviceSetup.license.getFeatures()).toEqual({ + showLogin: true, + allowLogin: true, + showLinks: true, + allowRoleDocumentLevelSecurity: false, + allowRoleFieldLevelSecurity: false, + allowRbac: true, + }); + expect(mockRawLicense.getFeature).toHaveBeenCalledTimes(1); + expect(mockRawLicense.getFeature).toHaveBeenCalledWith('security'); + }); + + it('should not show login page or other security elements if security is disabled in Elasticsearch.', () => { + const mockRawLicense = getMockRawLicense({ isAvailable: true }); + mockRawLicense.isOneOf.mockReturnValue(false); + mockRawLicense.getFeature.mockReturnValue({ isEnabled: false, isAvailable: true } as any); + + const serviceSetup = new SecurityLicenseService().setup(); + serviceSetup.update(mockRawLicense); + expect(serviceSetup.license.getFeatures()).toEqual({ + showLogin: false, + allowLogin: false, + showLinks: false, + allowRoleDocumentLevelSecurity: false, + allowRoleFieldLevelSecurity: false, + allowRbac: false, + linksMessage: 'Access is denied because Security is disabled in Elasticsearch.', + }); + }); + + it('should allow to login, allow RBAC and document level security if license is platinum or trial.', () => { + const mockRawLicense = getMockRawLicense({ isAvailable: true }); + mockRawLicense.isOneOf.mockImplementation(licenses => { + const licenseArray = [licenses].flat(); + return licenseArray.includes('trial') || licenseArray.includes('platinum'); + }); + mockRawLicense.getFeature.mockReturnValue({ isEnabled: true, isAvailable: true } as any); + + const serviceSetup = new SecurityLicenseService().setup(); + serviceSetup.update(mockRawLicense); + expect(serviceSetup.license.getFeatures()).toEqual({ + showLogin: true, + allowLogin: true, + showLinks: true, + allowRoleDocumentLevelSecurity: true, + allowRoleFieldLevelSecurity: true, + allowRbac: true, + }); + }); +}); diff --git a/x-pack/plugins/security/server/licensing/license_service.ts b/x-pack/plugins/security/server/licensing/license_service.ts new file mode 100644 index 00000000000000..58c445de9319de --- /dev/null +++ b/x-pack/plugins/security/server/licensing/license_service.ts @@ -0,0 +1,86 @@ +/* + * 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 { deepFreeze } from '../../../../../src/core/utils'; +import { ILicense } from '../../../licensing/server'; +import { SecurityLicenseFeatures } from './license_features'; + +export interface SecurityLicense { + isEnabled(): boolean; + getFeatures(): SecurityLicenseFeatures; +} + +export class SecurityLicenseService { + public setup() { + let rawLicense: Readonly | undefined; + + return { + update(newRawLicense: Readonly) { + rawLicense = newRawLicense; + }, + + license: deepFreeze({ + isEnabled() { + if (!rawLicense) { + return false; + } + + const securityFeature = rawLicense.getFeature('security'); + return ( + securityFeature !== undefined && + securityFeature.isAvailable && + securityFeature.isEnabled + ); + }, + + /** + * Returns up-do-date Security related features based on the last known license. + */ + getFeatures(): SecurityLicenseFeatures { + // If, for some reason, we cannot get license information from Elasticsearch, + // assume worst-case and lock user at login screen. + if (rawLicense === undefined || !rawLicense.isAvailable) { + return { + showLogin: true, + allowLogin: false, + showLinks: false, + allowRoleDocumentLevelSecurity: false, + allowRoleFieldLevelSecurity: false, + allowRbac: false, + layout: + rawLicense !== undefined && !rawLicense.isAvailable + ? 'error-xpack-unavailable' + : 'error-es-unavailable', + }; + } + + if (!this.isEnabled()) { + return { + showLogin: false, + allowLogin: false, + showLinks: false, + allowRoleDocumentLevelSecurity: false, + allowRoleFieldLevelSecurity: false, + allowRbac: false, + linksMessage: 'Access is denied because Security is disabled in Elasticsearch.', + }; + } + + const isLicensePlatinumOrTrial = rawLicense.isOneOf(['platinum', 'trial']); + return { + showLogin: true, + allowLogin: true, + showLinks: true, + // Only platinum and trial licenses are compliant with field- and document-level security. + allowRoleDocumentLevelSecurity: isLicensePlatinumOrTrial, + allowRoleFieldLevelSecurity: isLicensePlatinumOrTrial, + allowRbac: true, + }; + }, + }), + }; + } +} diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts new file mode 100644 index 00000000000000..d5c08d5ab1ab94 --- /dev/null +++ b/x-pack/plugins/security/server/mocks.ts @@ -0,0 +1,28 @@ +/* + * 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 { PluginSetupContract } from './plugin'; + +import { authenticationMock } from './authentication/index.mock'; +import { authorizationMock } from './authorization/index.mock'; + +function createSetupMock() { + const mockAuthz = authorizationMock.create(); + return { + authc: authenticationMock.create(), + authz: { + actions: mockAuthz.actions, + checkPrivilegesWithRequest: mockAuthz.checkPrivilegesWithRequest, + mode: mockAuthz.mode, + }, + registerSpacesService: jest.fn(), + __legacyCompat: {} as PluginSetupContract['__legacyCompat'], + }; +} + +export const securityMock = { + createSetup: createSetupMock, +}; diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 7fa8f20476f90e..b0e2ae71768348 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -4,16 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { coreMock, elasticsearchServiceMock } from '../../../../src/core/server/mocks'; - +import { of } from 'rxjs'; import { ByteSizeValue } from '@kbn/config-schema'; -import { Plugin } from './plugin'; import { IClusterClient, CoreSetup } from '../../../../src/core/server'; +import { Plugin, PluginSetupDependencies } from './plugin'; + +import { coreMock, elasticsearchServiceMock } from '../../../../src/core/server/mocks'; describe('Security Plugin', () => { let plugin: Plugin; let mockCoreSetup: MockedKeys; let mockClusterClient: jest.Mocked; + let mockDependencies: PluginSetupDependencies; beforeEach(() => { plugin = new Plugin( coreMock.createPluginInitializerContext({ @@ -33,12 +35,33 @@ describe('Security Plugin', () => { mockCoreSetup.elasticsearch.createClient.mockReturnValue( (mockClusterClient as unknown) as jest.Mocked ); + + mockDependencies = { licensing: { license$: of({}) } } as PluginSetupDependencies; }); describe('setup()', () => { it('exposes proper contract', async () => { - await expect(plugin.setup(mockCoreSetup)).resolves.toMatchInlineSnapshot(` + await expect(plugin.setup(mockCoreSetup, mockDependencies)).resolves.toMatchInlineSnapshot(` Object { + "__legacyCompat": Object { + "config": Object { + "authc": Object { + "providers": Array [ + "saml", + "token", + ], + }, + "cookieName": "sid", + "secureCookies": true, + "sessionTimeout": 1500, + }, + "license": Object { + "getFeatures": [Function], + "isEnabled": [Function], + }, + "registerLegacyAPI": [Function], + "registerPrivilegesWithCluster": [Function], + }, "authc": Object { "createAPIKey": [Function], "getCurrentUser": [Function], @@ -47,24 +70,40 @@ describe('Security Plugin', () => { "login": [Function], "logout": [Function], }, - "config": Object { - "authc": Object { - "providers": Array [ - "saml", - "token", - ], + "authz": Object { + "actions": Actions { + "allHack": "allHack:", + "api": ApiActions { + "prefix": "api:version:", + }, + "app": AppActions { + "prefix": "app:version:", + }, + "login": "login:", + "savedObject": SavedObjectActions { + "prefix": "saved_object:version:", + }, + "space": SpaceActions { + "prefix": "space:version:", + }, + "ui": UIActions { + "prefix": "ui:version:", + }, + "version": "version:version", + "versionNumber": "version", + }, + "checkPrivilegesWithRequest": [Function], + "mode": Object { + "useRbacForRequest": [Function], }, - "cookieName": "sid", - "secureCookies": true, - "sessionTimeout": 1500, }, - "registerLegacyAPI": [Function], + "registerSpacesService": [Function], } `); }); it('properly creates cluster client instance', async () => { - await plugin.setup(mockCoreSetup); + await plugin.setup(mockCoreSetup, mockDependencies); expect(mockCoreSetup.elasticsearch.createClient).toHaveBeenCalledTimes(1); expect(mockCoreSetup.elasticsearch.createClient).toHaveBeenCalledWith('security', { @@ -74,7 +113,7 @@ describe('Security Plugin', () => { }); describe('stop()', () => { - beforeEach(async () => await plugin.setup(mockCoreSetup)); + beforeEach(async () => await plugin.setup(mockCoreSetup, mockDependencies)); it('properly closes cluster client instance', async () => { expect(mockClusterClient.close).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 18717f3e132b9f..4b3997fe74f1b1 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Subscription } from 'rxjs'; import { first } from 'rxjs/operators'; import { IClusterClient, @@ -12,21 +13,43 @@ import { Logger, PluginInitializerContext, RecursiveReadonly, + SavedObjectsLegacyService, + LegacyRequest, } from '../../../../src/core/server'; import { deepFreeze } from '../../../../src/core/utils'; -import { XPackInfo } from '../../../legacy/plugins/xpack_main/server/lib/xpack_info'; -import { setupAuthentication, Authentication } from './authentication'; +import { SpacesPluginSetup } from '../../spaces/server'; +import { PluginSetupContract as FeaturesSetupContract } from '../../features/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { CapabilitiesModifier } from '../../../../src/legacy/server/capabilities'; + +import { Authentication, setupAuthentication } from './authentication'; +import { Authorization, setupAuthorization } from './authorization'; import { createConfig$ } from './config'; import { defineRoutes } from './routes'; +import { SecurityLicenseService, SecurityLicense } from './licensing'; +import { setupSavedObjects } from './saved_objects'; +import { SecurityAuditLogger } from './audit'; + +export type SpacesService = Pick< + SpacesPluginSetup['spacesService'], + 'getSpaceId' | 'namespaceToSpaceId' +>; + +export type FeaturesService = Pick; /** * Describes a set of APIs that is available in the legacy platform only and required by this plugin * to function properly. */ export interface LegacyAPI { - xpackInfo: Pick; isSystemAPIRequest: (request: KibanaRequest) => boolean; + capabilities: { registerCapabilitiesModifier: (provider: CapabilitiesModifier) => void }; + kibanaIndexName: string; cspRules: string; + savedObjects: SavedObjectsLegacyService; + auditLogger: { + log: (eventType: string, message: string, data?: Record) => void; + }; } /** @@ -34,14 +57,33 @@ export interface LegacyAPI { */ export interface PluginSetupContract { authc: Authentication; + authz: Pick; + + /** + * If Spaces plugin is available it's supposed to register its SpacesService with Security plugin + * so that Security can get space ID from the URL or namespace. We can't declare optional dependency + * to Spaces since it'd result into circular dependency between these two plugins and circular + * dependencies aren't supported by the Core. In the future we have to get rid of this implicit + * dependency. + * @param service Spaces service exposed by the Spaces plugin. + */ + registerSpacesService: (service: SpacesService) => void; + + __legacyCompat: { + registerLegacyAPI: (legacyAPI: LegacyAPI) => void; + registerPrivilegesWithCluster: () => void; + license: SecurityLicense; + config: RecursiveReadonly<{ + sessionTimeout: number | null; + secureCookies: boolean; + authc: { providers: string[] }; + }>; + }; +} - config: RecursiveReadonly<{ - sessionTimeout: number | null; - secureCookies: boolean; - authc: { providers: string[] }; - }>; - - registerLegacyAPI: (legacyAPI: LegacyAPI) => void; +export interface PluginSetupDependencies { + features: FeaturesService; + licensing: LicensingPluginSetup; } /** @@ -50,6 +92,8 @@ export interface PluginSetupContract { export class Plugin { private readonly logger: Logger; private clusterClient?: IClusterClient; + private spacesService?: SpacesService | symbol = Symbol('not accessed'); + private licenseSubscription?: Subscription; private legacyAPI?: LegacyAPI; private readonly getLegacyAPI = () => { @@ -59,11 +103,23 @@ export class Plugin { return this.legacyAPI; }; + private readonly getSpacesService = () => { + // Changing property value from Symbol to undefined denotes the fact that property was accessed. + if (!this.wasSpacesServiceAccessed()) { + this.spacesService = undefined; + } + + return this.spacesService as SpacesService | undefined; + }; + constructor(private readonly initializerContext: PluginInitializerContext) { this.logger = this.initializerContext.logger.get(); } - public async setup(core: CoreSetup): Promise> { + public async setup( + core: CoreSetup, + { features, licensing }: PluginSetupDependencies + ): Promise> { const config = await createConfig$(this.initializerContext, core.http.isTlsEnabled) .pipe(first()) .toPromise(); @@ -72,34 +128,88 @@ export class Plugin { plugins: [require('../../../legacy/server/lib/esjs_shield_plugin')], }); + const { license, update: updateLicense } = new SecurityLicenseService().setup(); + this.licenseSubscription = licensing.license$.subscribe(rawLicense => + updateLicense(rawLicense) + ); + const authc = await setupAuthentication({ - core, + http: core.http, + clusterClient: this.clusterClient, config, + license, + loggers: this.initializerContext.logger, + getLegacyAPI: this.getLegacyAPI, + }); + + const authz = await setupAuthorization({ + http: core.http, clusterClient: this.clusterClient, + license, loggers: this.initializerContext.logger, getLegacyAPI: this.getLegacyAPI, + packageVersion: this.initializerContext.env.packageInfo.version, + getSpacesService: this.getSpacesService, + featuresService: features, }); defineRoutes({ router: core.http.createRouter(), basePath: core.http.basePath, logger: this.initializerContext.logger.get('routes'), + clusterClient: this.clusterClient, config, authc, + authz, getLegacyAPI: this.getLegacyAPI, }); + const adminClient = await core.elasticsearch.adminClient$.pipe(first()).toPromise(); return deepFreeze({ - registerLegacyAPI: (legacyAPI: LegacyAPI) => (this.legacyAPI = legacyAPI), authc, - // We should stop exposing this config as soon as only new platform plugin consumes it. The only - // exception may be `sessionTimeout` as other parts of the app may want to know it. - config: { - sessionTimeout: config.sessionTimeout, - secureCookies: config.secureCookies, - cookieName: config.cookieName, - authc: { providers: config.authc.providers }, + authz: { + actions: authz.actions, + checkPrivilegesWithRequest: authz.checkPrivilegesWithRequest, + mode: authz.mode, + }, + + registerSpacesService: service => { + if (this.wasSpacesServiceAccessed()) { + throw new Error('Spaces service has been accessed before registration.'); + } + + this.spacesService = service; + }, + + __legacyCompat: { + registerLegacyAPI: (legacyAPI: LegacyAPI) => { + this.legacyAPI = legacyAPI; + + setupSavedObjects({ + auditLogger: new SecurityAuditLogger(legacyAPI.auditLogger), + adminClusterClient: adminClient, + authz, + legacyAPI, + }); + + legacyAPI.capabilities.registerCapabilitiesModifier((request, capabilities) => + authz.disableUnauthorizedCapabilities(KibanaRequest.from(request), capabilities) + ); + }, + + registerPrivilegesWithCluster: async () => await authz.registerPrivilegesWithCluster(), + + license, + + // We should stop exposing this config as soon as only new platform plugin consumes it. The only + // exception may be `sessionTimeout` as other parts of the app may want to know it. + config: { + sessionTimeout: config.sessionTimeout, + secureCookies: config.secureCookies, + cookieName: config.cookieName, + authc: { providers: config.authc.providers }, + }, }, }); } @@ -115,5 +225,14 @@ export class Plugin { this.clusterClient.close(); this.clusterClient = undefined; } + + if (this.licenseSubscription) { + this.licenseSubscription.unsubscribe(); + this.licenseSubscription = undefined; + } + } + + private wasSpacesServiceAccessed() { + return typeof this.spacesService !== 'symbol'; } } diff --git a/x-pack/plugins/security/server/routes/authentication/index.test.ts b/x-pack/plugins/security/server/routes/authentication/index.test.ts new file mode 100644 index 00000000000000..cad370b7837e1e --- /dev/null +++ b/x-pack/plugins/security/server/routes/authentication/index.test.ts @@ -0,0 +1,40 @@ +/* + * 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 { defineAuthenticationRoutes } from '.'; +import { ConfigType } from '../../config'; + +import { + elasticsearchServiceMock, + httpServiceMock, + loggingServiceMock, +} from '../../../../../../src/core/server/mocks'; +import { authenticationMock } from '../../authentication/index.mock'; +import { authorizationMock } from '../../authorization/index.mock'; + +describe('Authentication routes', () => { + it('does not register any SAML related routes if SAML auth provider is not enabled', () => { + const router = httpServiceMock.createRouter(); + + defineAuthenticationRoutes({ + router, + clusterClient: elasticsearchServiceMock.createClusterClient(), + basePath: httpServiceMock.createBasePath(), + logger: loggingServiceMock.create().get(), + config: { authc: { providers: ['basic'] } } as ConfigType, + authc: authenticationMock.create(), + authz: authorizationMock.create(), + getLegacyAPI: () => ({ cspRules: 'test-csp-rule' }), + }); + + const samlRoutePathPredicate = ([{ path }]: [{ path: string }, any]) => + path.startsWith('/api/security/saml/'); + expect(router.get.mock.calls.find(samlRoutePathPredicate)).toBeUndefined(); + expect(router.post.mock.calls.find(samlRoutePathPredicate)).toBeUndefined(); + expect(router.put.mock.calls.find(samlRoutePathPredicate)).toBeUndefined(); + expect(router.delete.mock.calls.find(samlRoutePathPredicate)).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/security/server/routes/authentication/index.ts b/x-pack/plugins/security/server/routes/authentication/index.ts new file mode 100644 index 00000000000000..0e3f03255dcb90 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authentication/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { defineSAMLRoutes } from './saml'; +import { RouteDefinitionParams } from '..'; + +export function defineAuthenticationRoutes(params: RouteDefinitionParams) { + if (params.config.authc.providers.includes('saml')) { + defineSAMLRoutes(params); + } +} diff --git a/x-pack/plugins/security/server/routes/authentication.test.ts b/x-pack/plugins/security/server/routes/authentication/saml.test.ts similarity index 82% rename from x-pack/plugins/security/server/routes/authentication.test.ts rename to x-pack/plugins/security/server/routes/authentication/saml.test.ts index ac677519cd9375..cdef1826ddaa88 100644 --- a/x-pack/plugins/security/server/routes/authentication.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.test.ts @@ -5,18 +5,21 @@ */ import { Type } from '@kbn/config-schema'; -import { Authentication, AuthenticationResult, SAMLLoginStep } from '../authentication'; -import { defineAuthenticationRoutes } from './authentication'; +import { Authentication, AuthenticationResult, SAMLLoginStep } from '../../authentication'; +import { defineSAMLRoutes } from './saml'; +import { ConfigType } from '../../config'; +import { IRouter, RequestHandler, RouteConfig } from '../../../../../../src/core/server'; +import { LegacyAPI } from '../../plugin'; + import { + elasticsearchServiceMock, httpServerMock, httpServiceMock, loggingServiceMock, -} from '../../../../../src/core/server/mocks'; -import { ConfigType } from '../config'; -import { IRouter, RequestHandler, RouteConfig } from '../../../../../src/core/server'; -import { LegacyAPI } from '../plugin'; -import { authenticationMock } from '../authentication/index.mock'; -import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock'; +} from '../../../../../../src/core/server/mocks'; +import { authenticationMock } from '../../authentication/index.mock'; +import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; +import { authorizationMock } from '../../authorization/index.mock'; describe('SAML authentication routes', () => { let router: jest.Mocked; @@ -25,35 +28,18 @@ describe('SAML authentication routes', () => { router = httpServiceMock.createRouter(); authc = authenticationMock.create(); - defineAuthenticationRoutes({ + defineSAMLRoutes({ router, + clusterClient: elasticsearchServiceMock.createClusterClient(), basePath: httpServiceMock.createBasePath(), logger: loggingServiceMock.create().get(), config: { authc: { providers: ['saml'] } } as ConfigType, authc, + authz: authorizationMock.create(), getLegacyAPI: () => ({ cspRules: 'test-csp-rule' } as LegacyAPI), }); }); - it('does not register any SAML related routes if SAML auth provider is not enabled', () => { - const testRouter = httpServiceMock.createRouter(); - defineAuthenticationRoutes({ - router: testRouter, - basePath: httpServiceMock.createBasePath(), - logger: loggingServiceMock.create().get(), - config: { authc: { providers: ['basic'] } } as ConfigType, - authc: authenticationMock.create(), - getLegacyAPI: () => ({ cspRules: 'test-csp-rule' } as LegacyAPI), - }); - - const samlRoutePathPredicate = ([{ path }]: [{ path: string }, any]) => - path.startsWith('/api/security/saml/'); - expect(testRouter.get.mock.calls.find(samlRoutePathPredicate)).toBeUndefined(); - expect(testRouter.post.mock.calls.find(samlRoutePathPredicate)).toBeUndefined(); - expect(testRouter.put.mock.calls.find(samlRoutePathPredicate)).toBeUndefined(); - expect(testRouter.delete.mock.calls.find(samlRoutePathPredicate)).toBeUndefined(); - }); - describe('Assertion consumer service endpoint', () => { let routeHandler: RequestHandler; let routeConfig: RouteConfig; diff --git a/x-pack/plugins/security/server/routes/authentication.ts b/x-pack/plugins/security/server/routes/authentication/saml.ts similarity index 92% rename from x-pack/plugins/security/server/routes/authentication.ts rename to x-pack/plugins/security/server/routes/authentication/saml.ts index e0c83602afffb6..61f40e583d24ef 100644 --- a/x-pack/plugins/security/server/routes/authentication.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.ts @@ -5,19 +5,13 @@ */ import { schema } from '@kbn/config-schema'; -import { RouteDefinitionParams } from '.'; -import { SAMLLoginStep } from '../authentication'; - -export function defineAuthenticationRoutes(params: RouteDefinitionParams) { - if (params.config.authc.providers.includes('saml')) { - defineSAMLRoutes(params); - } -} +import { SAMLLoginStep } from '../../authentication'; +import { RouteDefinitionParams } from '..'; /** * Defines routes required for SAML authentication. */ -function defineSAMLRoutes({ +export function defineSAMLRoutes({ router, logger, authc, diff --git a/x-pack/plugins/security/server/routes/authorization/index.ts b/x-pack/plugins/security/server/routes/authorization/index.ts new file mode 100644 index 00000000000000..19f2bcccb04a89 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { definePrivilegesRoutes } from './privileges'; +import { defineRolesRoutes } from './roles'; +import { RouteDefinitionParams } from '..'; + +export function defineAuthorizationRoutes(params: RouteDefinitionParams) { + defineRolesRoutes(params); + definePrivilegesRoutes(params); +} diff --git a/x-pack/plugins/security/server/routes/authorization/privileges/get.test.ts b/x-pack/plugins/security/server/routes/authorization/privileges/get.test.ts new file mode 100644 index 00000000000000..73adaba551875c --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/privileges/get.test.ts @@ -0,0 +1,107 @@ +/* + * 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 { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../../src/core/server'; +import { ILicenseCheck } from '../../../../../licensing/server'; +// TODO, require from licensing plugin root once https://github.com/elastic/kibana/pull/44922 is merged. +import { LICENSE_STATUS } from '../../../../../licensing/server/constants'; +import { RawKibanaPrivileges } from '../../../../common/model'; +import { defineGetPrivilegesRoutes } from './get'; + +import { httpServerMock } from '../../../../../../../src/core/server/mocks'; +import { routeDefinitionParamsMock } from '../../index.mock'; + +const createRawKibanaPrivileges: () => RawKibanaPrivileges = () => { + return { + features: { + feature1: { + all: ['action1'], + }, + feature2: { + all: ['action2'], + }, + }, + space: { + all: ['space*'], + read: ['space:read'], + }, + global: { + all: ['*'], + read: ['something:/read'], + }, + reserved: { + customApplication1: ['custom-action1'], + customApplication2: ['custom-action2'], + }, + }; +}; + +interface TestOptions { + licenseCheckResult?: ILicenseCheck; + includeActions?: boolean; + asserts: { statusCode: number; result: Record }; +} + +describe('GET privileges', () => { + const getPrivilegesTest = ( + description: string, + { licenseCheckResult = { check: LICENSE_STATUS.Valid }, includeActions, asserts }: TestOptions + ) => { + test(description, async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + mockRouteDefinitionParams.authz.privileges.get.mockImplementation(() => + createRawKibanaPrivileges() + ); + + defineGetPrivilegesRoutes(mockRouteDefinitionParams); + const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: `/api/security/privileges${includeActions ? '?includeActions=true' : ''}`, + query: includeActions ? { includeActions: 'true' } : undefined, + headers, + }); + const mockContext = ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); + }); + }; + + describe('failure', () => { + getPrivilegesTest(`returns result of routePreCheckLicense`, { + licenseCheckResult: { check: LICENSE_STATUS.Invalid, message: 'test forbidden message' }, + asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, + }); + }); + + describe('success', () => { + getPrivilegesTest(`returns registered application privileges with actions when requested`, { + includeActions: true, + asserts: { statusCode: 200, result: createRawKibanaPrivileges() }, + }); + + getPrivilegesTest(`returns registered application privileges without actions`, { + includeActions: false, + asserts: { + statusCode: 200, + result: { + global: ['all', 'read'], + space: ['all', 'read'], + features: { feature1: ['all'], feature2: ['all'] }, + reserved: ['customApplication1', 'customApplication2'], + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/authorization/privileges/get.ts b/x-pack/plugins/security/server/routes/authorization/privileges/get.ts new file mode 100644 index 00000000000000..81047c7faea965 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/privileges/get.ts @@ -0,0 +1,47 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { RouteDefinitionParams } from '../..'; +import { createLicensedRouteHandler } from '../../licensed_route_handler'; + +export function defineGetPrivilegesRoutes({ router, authz }: RouteDefinitionParams) { + router.get( + { + path: '/api/security/privileges', + validate: { + query: schema.object({ + // We don't use `schema.boolean` here, because all query string parameters are treated as + // strings and @kbn/config-schema doesn't coerce strings to booleans. + includeActions: schema.maybe( + schema.oneOf([schema.literal('true'), schema.literal('false')]) + ), + }), + }, + }, + createLicensedRouteHandler((context, request, response) => { + const privileges = authz.privileges.get(); + const includeActions = request.query.includeActions === 'true'; + const privilegesResponseBody = includeActions + ? privileges + : { + global: Object.keys(privileges.global), + space: Object.keys(privileges.space), + features: Object.entries(privileges.features).reduce( + (acc, [featureId, featurePrivileges]) => { + return { + ...acc, + [featureId]: Object.keys(featurePrivileges), + }; + }, + {} + ), + reserved: Object.keys(privileges.reserved), + }; + + return response.ok({ body: privilegesResponseBody }); + }) + ); +} diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/builtin_privileges.ts b/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts similarity index 52% rename from x-pack/legacy/plugins/security/server/routes/api/v1/builtin_privileges.ts rename to x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts index 991b57b11a8f8a..c9e963f0b8fc73 100644 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/builtin_privileges.ts +++ b/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts @@ -4,26 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; import { BuiltinESPrivileges } from '../../../../common/model'; -import { getClient } from '../../../../../../server/lib/get_client_shield'; +import { RouteDefinitionParams } from '../..'; -export function initGetBuiltinPrivilegesApi(server: Legacy.Server) { - server.route({ - method: 'GET', - path: '/api/security/v1/esPrivileges/builtin', - async handler(req: Legacy.Request) { - const callWithRequest = getClient(server).callWithRequest; - const privileges = await callWithRequest( - req, - 'shield.getBuiltinPrivileges' - ); +export function defineGetBuiltinPrivilegesRoutes({ router, clusterClient }: RouteDefinitionParams) { + router.get( + { path: '/internal/security/esPrivileges/builtin', validate: false }, + async (context, request, response) => { + const privileges: BuiltinESPrivileges = await clusterClient + .asScoped(request) + .callAsCurrentUser('shield.getBuiltinPrivileges'); // Exclude the `none` privilege, as it doesn't make sense as an option within the Kibana UI privileges.cluster = privileges.cluster.filter(privilege => privilege !== 'none'); privileges.index = privileges.index.filter(privilege => privilege !== 'none'); - return privileges; - }, - }); + return response.ok({ body: privileges }); + } + ); } diff --git a/x-pack/plugins/security/server/routes/authorization/privileges/index.ts b/x-pack/plugins/security/server/routes/authorization/privileges/index.ts new file mode 100644 index 00000000000000..7c7ff402fcee2c --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/privileges/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { RouteDefinitionParams } from '../..'; +import { defineGetPrivilegesRoutes } from './get'; +import { defineGetBuiltinPrivilegesRoutes } from './get_builtin'; + +export function definePrivilegesRoutes(params: RouteDefinitionParams) { + defineGetPrivilegesRoutes(params); + defineGetBuiltinPrivilegesRoutes(params); +} diff --git a/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts new file mode 100644 index 00000000000000..5699b100e3ffd6 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts @@ -0,0 +1,98 @@ +/* + * 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 Boom from 'boom'; +import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../../src/core/server'; +import { ILicenseCheck } from '../../../../../licensing/server'; +import { LICENSE_STATUS } from '../../../../../licensing/server/constants'; +import { defineDeleteRolesRoutes } from './delete'; + +import { + elasticsearchServiceMock, + httpServerMock, +} from '../../../../../../../src/core/server/mocks'; +import { routeDefinitionParamsMock } from '../../index.mock'; + +interface TestOptions { + licenseCheckResult?: ILicenseCheck; + name: string; + apiResponse?: () => Promise; + asserts: { statusCode: number; result?: Record }; +} + +describe('DELETE role', () => { + const deleteRoleTest = ( + description: string, + { + name, + licenseCheckResult = { check: LICENSE_STATUS.Valid }, + apiResponse, + asserts, + }: TestOptions + ) => { + test(description, async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + if (apiResponse) { + mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); + } + + defineDeleteRolesRoutes(mockRouteDefinitionParams); + const [[, handler]] = mockRouteDefinitionParams.router.delete.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `/api/security/role/${name}`, + params: { name }, + headers, + }); + const mockContext = ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + if (apiResponse) { + expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( + 'shield.deleteRole', + { name } + ); + } else { + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); + }); + }; + + describe('failure', () => { + deleteRoleTest(`returns result of license checker`, { + name: 'foo-role', + licenseCheckResult: { check: LICENSE_STATUS.Invalid, message: 'test forbidden message' }, + asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, + }); + + const error = Boom.notFound('test not found message'); + deleteRoleTest(`returns error from cluster client`, { + name: 'foo-role', + apiResponse: () => Promise.reject(error), + asserts: { statusCode: 404, result: error }, + }); + }); + + describe('success', () => { + deleteRoleTest(`deletes role`, { + name: 'foo-role', + apiResponse: async () => {}, + asserts: { statusCode: 204, result: undefined }, + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/delete.ts b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts new file mode 100644 index 00000000000000..aab815fbe449ff --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts @@ -0,0 +1,34 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { RouteDefinitionParams } from '../../index'; +import { createLicensedRouteHandler } from '../../licensed_route_handler'; +import { wrapError } from '../../../errors'; + +export function defineDeleteRolesRoutes({ router, clusterClient }: RouteDefinitionParams) { + router.delete( + { + path: '/api/security/role/{name}', + validate: { params: schema.object({ name: schema.string({ minLength: 1 }) }) }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + await clusterClient.asScoped(request).callAsCurrentUser('shield.deleteRole', { + name: request.params.name, + }); + + return response.noContent(); + } catch (error) { + const wrappedError = wrapError(error); + return response.customError({ + body: wrappedError, + statusCode: wrappedError.output.statusCode, + }); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts new file mode 100644 index 00000000000000..619e6e67f683bc --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts @@ -0,0 +1,1157 @@ +/* + * 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 Boom from 'boom'; +import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../../src/core/server'; +import { ILicenseCheck } from '../../../../../licensing/server'; +import { LICENSE_STATUS } from '../../../../../licensing/server/constants'; +import { defineGetRolesRoutes } from './get'; + +import { + elasticsearchServiceMock, + httpServerMock, +} from '../../../../../../../src/core/server/mocks'; +import { routeDefinitionParamsMock } from '../../index.mock'; + +const application = 'kibana-.kibana'; +const reservedPrivilegesApplicationWildcard = 'kibana-*'; + +interface TestOptions { + name?: string; + licenseCheckResult?: ILicenseCheck; + apiResponse?: () => Promise; + asserts: { statusCode: number; result?: Record }; +} + +describe('GET role', () => { + const getRoleTest = ( + description: string, + { + name, + licenseCheckResult = { check: LICENSE_STATUS.Valid }, + apiResponse, + asserts, + }: TestOptions + ) => { + test(description, async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + mockRouteDefinitionParams.authz.getApplicationName.mockReturnValue(application); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + if (apiResponse) { + mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); + } + + defineGetRolesRoutes(mockRouteDefinitionParams); + const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `/api/security/role/${name}`, + params: { name }, + headers, + }); + const mockContext = ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + if (apiResponse) { + expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.getRole', { + name, + }); + } else { + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); + }); + }; + + describe('failure', () => { + getRoleTest(`returns result of license check`, { + licenseCheckResult: { check: LICENSE_STATUS.Invalid, message: 'test forbidden message' }, + asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, + }); + + const error = Boom.notAcceptable('test not acceptable message'); + getRoleTest(`returns error from cluster client`, { + name: 'first_role', + apiResponse: () => Promise.reject(error), + asserts: { statusCode: 406, result: error }, + }); + + getRoleTest(`return error if we have empty resources`, { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['read'], + resources: [], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 500, + result: new Error("ES returned an application entry without resources, can't process this"), + }, + }); + }); + + describe('success', () => { + getRoleTest(`transforms elasticsearch privileges`, { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: ['manage_watcher'], + indices: [ + { + names: ['.kibana*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + applications: [], + run_as: ['other_user'], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: ['manage_watcher'], + indices: [ + { + names: ['.kibana*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + run_as: ['other_user'], + }, + kibana: [], + _transform_error: [], + _unrecognized_applications: [], + }, + }, + }); + + describe('global', () => { + getRoleTest( + `transforms matching applications with * resource to kibana global base privileges`, + { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['all', 'read'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: ['all', 'read'], + feature: {}, + spaces: ['*'], + }, + ], + _transform_error: [], + _unrecognized_applications: [], + }, + }, + } + ); + + getRoleTest( + `transforms matching applications with * resource to kibana global feature privileges`, + { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: [ + 'feature_foo.foo-privilege-1', + 'feature_foo.foo-privilege-2', + 'feature_bar.bar-privilege-1', + ], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: [], + feature: { + foo: ['foo-privilege-1', 'foo-privilege-2'], + bar: ['bar-privilege-1'], + }, + spaces: ['*'], + }, + ], + _transform_error: [], + _unrecognized_applications: [], + }, + }, + } + ); + + getRoleTest( + `transforms matching applications with * resource to kibana _reserved privileges`, + { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['reserved_customApplication1', 'reserved_customApplication2'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + _reserved: ['customApplication1', 'customApplication2'], + base: [], + feature: {}, + spaces: ['*'], + }, + ], + _transform_error: [], + _unrecognized_applications: [], + }, + }, + } + ); + + getRoleTest( + `transforms applications with wildcard and * resource to kibana _reserved privileges`, + { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application: reservedPrivilegesApplicationWildcard, + privileges: ['reserved_customApplication1', 'reserved_customApplication2'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + _reserved: ['customApplication1', 'customApplication2'], + base: [], + feature: {}, + spaces: ['*'], + }, + ], + _transform_error: [], + _unrecognized_applications: [], + }, + }, + } + ); + }); + + describe('space', () => { + getRoleTest( + `transforms matching applications with space resources to kibana space base privileges`, + { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['space_all', 'space_read'], + resources: ['space:marketing', 'space:sales'], + }, + { + application, + privileges: ['space_read'], + resources: ['space:engineering'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: ['all', 'read'], + feature: {}, + spaces: ['marketing', 'sales'], + }, + { + base: ['read'], + feature: {}, + spaces: ['engineering'], + }, + ], + _transform_error: [], + _unrecognized_applications: [], + }, + }, + } + ); + + getRoleTest( + `transforms matching applications with space resources to kibana space feature privileges`, + { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: [ + 'feature_foo.foo-privilege-1', + 'feature_foo.foo-privilege-2', + 'feature_bar.bar-privilege-1', + ], + resources: ['space:marketing', 'space:sales'], + }, + { + application, + privileges: ['feature_foo.foo-privilege-1'], + resources: ['space:engineering'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: [], + feature: { + foo: ['foo-privilege-1', 'foo-privilege-2'], + bar: ['bar-privilege-1'], + }, + spaces: ['marketing', 'sales'], + }, + { + base: [], + feature: { + foo: ['foo-privilege-1'], + }, + spaces: ['engineering'], + }, + ], + _transform_error: [], + _unrecognized_applications: [], + }, + }, + } + ); + }); + + getRoleTest( + `resource not * without space: prefix returns empty kibana section with _transform_error set to ['kibana']`, + { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['read'], + resources: ['default'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + } + ); + + getRoleTest( + `* and a space in the same entry returns empty kibana section with _transform_error set to ['kibana']`, + { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['read'], + resources: ['default'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + } + ); + + getRoleTest( + `* appearing in multiple entries returns empty kibana section with _transform_error set to ['kibana']`, + { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['space_all'], + resources: ['space:engineering'], + }, + { + application, + privileges: ['space_read'], + resources: ['space:engineering'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + } + ); + + getRoleTest( + `space privilege assigned globally returns empty kibana section with _transform_error set to ['kibana']`, + { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['space_all'], + resources: ['*'], + }, + { + application, + privileges: ['space_read'], + resources: ['space:engineering'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + } + ); + + getRoleTest( + `space privilege with application wildcard returns empty kibana section with _transform_error set to ['kibana']`, + { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application: reservedPrivilegesApplicationWildcard, + privileges: ['space_read'], + resources: ['space:engineering'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + } + ); + + getRoleTest( + `global base privilege assigned at a space returns empty kibana section with _transform_error set to ['kibana']`, + { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['all'], + resources: ['space:marketing'], + }, + { + application, + privileges: ['space_read'], + resources: ['space:engineering'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + } + ); + + getRoleTest( + `global base privilege with application wildcard returns empty kibana section with _transform_error set to ['kibana']`, + { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application: reservedPrivilegesApplicationWildcard, + privileges: ['all'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + } + ); + + getRoleTest( + `reserved privilege assigned at a space returns empty kibana section with _transform_error set to ['kibana']`, + { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['reserved_foo'], + resources: ['space:marketing'], + }, + { + application, + privileges: ['space_read'], + resources: ['space:engineering'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + } + ); + + getRoleTest( + `reserved privilege assigned with a base privilege returns empty kibana section with _transform_error set to ['kibana']`, + { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['reserved_foo', 'read'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + } + ); + + getRoleTest( + `reserved privilege assigned with a feature privilege returns empty kibana section with _transform_error set to ['kibana']`, + { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['reserved_foo', 'feature_foo.foo-privilege-1'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + } + ); + + getRoleTest( + `global base privilege assigned with a feature privilege returns empty kibana section with _transform_error set to ['kibana']`, + { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['all', 'feature_foo.foo-privilege-1'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + } + ); + + getRoleTest( + `space base privilege assigned with a feature privilege returns empty kibana section with _transform_error set to ['kibana']`, + { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['space_all', 'feature_foo.foo-privilege-1'], + resources: ['space:space_1'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + } + ); + + getRoleTest(`transforms unrecognized applications`, { + name: 'first_role', + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application: 'kibana-.another-kibana', + privileges: ['read'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: [], + _unrecognized_applications: ['kibana-.another-kibana'], + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.ts new file mode 100644 index 00000000000000..be69e222dd0936 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.ts @@ -0,0 +1,46 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { RouteDefinitionParams } from '../..'; +import { createLicensedRouteHandler } from '../../licensed_route_handler'; +import { wrapError } from '../../../errors'; +import { transformElasticsearchRoleToRole } from './model'; + +export function defineGetRolesRoutes({ router, authz, clusterClient }: RouteDefinitionParams) { + router.get( + { + path: '/api/security/role/{name}', + validate: { params: schema.object({ name: schema.string({ minLength: 1 }) }) }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + const elasticsearchRoles = await clusterClient + .asScoped(request) + .callAsCurrentUser('shield.getRole', { name: request.params.name }); + + const elasticsearchRole = elasticsearchRoles[request.params.name]; + if (elasticsearchRole) { + return response.ok({ + body: transformElasticsearchRoleToRole( + elasticsearchRole, + request.params.name, + authz.getApplicationName() + ), + }); + } + + return response.notFound(); + } catch (error) { + const wrappedError = wrapError(error); + return response.customError({ + body: wrappedError, + statusCode: wrappedError.output.statusCode, + }); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts new file mode 100644 index 00000000000000..d04513592f0279 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts @@ -0,0 +1,1335 @@ +/* + * 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 Boom from 'boom'; +import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../../src/core/server'; +import { ILicenseCheck } from '../../../../../licensing/server'; +import { LICENSE_STATUS } from '../../../../../licensing/server/constants'; +import { defineGetAllRolesRoutes } from './get_all'; + +import { + elasticsearchServiceMock, + httpServerMock, +} from '../../../../../../../src/core/server/mocks'; +import { routeDefinitionParamsMock } from '../../index.mock'; + +const application = 'kibana-.kibana'; +const reservedPrivilegesApplicationWildcard = 'kibana-*'; + +interface TestOptions { + name?: string; + licenseCheckResult?: ILicenseCheck; + apiResponse?: () => Promise; + asserts: { statusCode: number; result?: Record }; +} + +describe('GET all roles', () => { + const getRolesTest = ( + description: string, + { licenseCheckResult = { check: LICENSE_STATUS.Valid }, apiResponse, asserts }: TestOptions + ) => { + test(description, async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + mockRouteDefinitionParams.authz.getApplicationName.mockReturnValue(application); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + if (apiResponse) { + mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); + } + + defineGetAllRolesRoutes(mockRouteDefinitionParams); + const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'delete', + path: '/api/security/role', + headers, + }); + const mockContext = ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + if (apiResponse) { + expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.getRole'); + } else { + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); + }); + }; + + describe('failure', () => { + getRolesTest(`returns result of license check`, { + licenseCheckResult: { check: LICENSE_STATUS.Invalid, message: 'test forbidden message' }, + asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, + }); + + const error = Boom.notAcceptable('test not acceptable message'); + getRolesTest(`returns error from cluster client`, { + apiResponse: () => Promise.reject(error), + asserts: { statusCode: 406, result: error }, + }); + + getRolesTest(`return error if we have empty resources`, { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['read'], + resources: [], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 500, + result: new Error("ES returned an application entry without resources, can't process this"), + }, + }); + }); + + describe('success', () => { + getRolesTest(`transforms elasticsearch privileges`, { + apiResponse: async () => ({ + first_role: { + cluster: ['manage_watcher'], + indices: [ + { + names: ['.kibana*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + applications: [], + run_as: ['other_user'], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: ['manage_watcher'], + indices: [ + { + names: ['.kibana*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + run_as: ['other_user'], + }, + kibana: [], + _transform_error: [], + _unrecognized_applications: [], + }, + ], + }, + }); + + describe('global', () => { + getRolesTest( + `transforms matching applications with * resource to kibana global base privileges`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['all', 'read'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: ['all', 'read'], + feature: {}, + spaces: ['*'], + }, + ], + _transform_error: [], + _unrecognized_applications: [], + }, + ], + }, + } + ); + + getRolesTest( + `transforms matching applications with * resource to kibana global feature privileges`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: [ + 'feature_foo.foo-privilege-1', + 'feature_foo.foo-privilege-2', + 'feature_bar.bar-privilege-1', + ], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: [], + feature: { + foo: ['foo-privilege-1', 'foo-privilege-2'], + bar: ['bar-privilege-1'], + }, + spaces: ['*'], + }, + ], + _transform_error: [], + _unrecognized_applications: [], + }, + ], + }, + } + ); + + getRolesTest( + `transforms matching applications with * resource to kibana _reserved privileges`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['reserved_customApplication1', 'reserved_customApplication2'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + _reserved: ['customApplication1', 'customApplication2'], + base: [], + feature: {}, + spaces: ['*'], + }, + ], + _transform_error: [], + _unrecognized_applications: [], + }, + ], + }, + } + ); + + getRolesTest( + `transforms applications with wildcard and * resource to kibana _reserved privileges`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application: reservedPrivilegesApplicationWildcard, + privileges: ['reserved_customApplication1', 'reserved_customApplication2'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + _reserved: ['customApplication1', 'customApplication2'], + base: [], + feature: {}, + spaces: ['*'], + }, + ], + _transform_error: [], + _unrecognized_applications: [], + }, + ], + }, + } + ); + }); + + describe('space', () => { + getRolesTest( + `transforms matching applications with space resources to kibana space base privileges`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['space_all', 'space_read'], + resources: ['space:marketing', 'space:sales'], + }, + { + application, + privileges: ['space_read'], + resources: ['space:engineering'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: ['all', 'read'], + feature: {}, + spaces: ['marketing', 'sales'], + }, + { + base: ['read'], + feature: {}, + spaces: ['engineering'], + }, + ], + _transform_error: [], + _unrecognized_applications: [], + }, + ], + }, + } + ); + + getRolesTest( + `transforms matching applications with space resources to kibana space feature privileges`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: [ + 'feature_foo.foo-privilege-1', + 'feature_foo.foo-privilege-2', + 'feature_bar.bar-privilege-1', + ], + resources: ['space:marketing', 'space:sales'], + }, + { + application, + privileges: ['feature_foo.foo-privilege-1'], + resources: ['space:engineering'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: [], + feature: { + foo: ['foo-privilege-1', 'foo-privilege-2'], + bar: ['bar-privilege-1'], + }, + spaces: ['marketing', 'sales'], + }, + { + base: [], + feature: { + foo: ['foo-privilege-1'], + }, + spaces: ['engineering'], + }, + ], + _transform_error: [], + _unrecognized_applications: [], + }, + ], + }, + } + ); + }); + + getRolesTest( + `resource not * without space: prefix returns empty kibana section with _transform_error set to ['kibana']`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['read'], + resources: ['default'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + ], + }, + } + ); + + getRolesTest( + `* and a space in the same entry returns empty kibana section with _transform_error set to ['kibana']`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['all'], + resources: ['*', 'space:engineering'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + ], + }, + } + ); + + getRolesTest( + `* appearing in multiple entries returns empty kibana section with _transform_error set to ['kibana']`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['all'], + resources: ['*'], + }, + { + application, + privileges: ['read'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + ], + }, + } + ); + + getRolesTest( + `space appearing in multiple entries returns empty kibana section with _transform_error set to ['kibana']`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['space_all'], + resources: ['space:engineering'], + }, + { + application, + privileges: ['space_read'], + resources: ['space:engineering'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + ], + }, + } + ); + + getRolesTest( + `space privilege assigned globally returns empty kibana section with _transform_error set to ['kibana']`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['space_all'], + resources: ['*'], + }, + { + application, + privileges: ['space_read'], + resources: ['space:engineering'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + ], + }, + } + ); + + getRolesTest( + `space privilege with application wildcard returns empty kibana section with _transform_error set to ['kibana']`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application: reservedPrivilegesApplicationWildcard, + privileges: ['space_read'], + resources: ['space:engineering'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + ], + }, + } + ); + + getRolesTest( + `global base privilege assigned at a space returns empty kibana section with _transform_error set to ['kibana']`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['all'], + resources: ['space:marketing'], + }, + { + application, + privileges: ['space_read'], + resources: ['space:engineering'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + ], + }, + } + ); + + getRolesTest( + `global base privilege with application wildcard returns empty kibana section with _transform_error set to ['kibana']`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application: reservedPrivilegesApplicationWildcard, + privileges: ['all'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + ], + }, + } + ); + + getRolesTest( + `reserved privilege assigned at a space returns empty kibana section with _transform_error set to ['kibana']`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['reserved_foo'], + resources: ['space:marketing'], + }, + { + application, + privileges: ['space_read'], + resources: ['space:engineering'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + ], + }, + } + ); + + getRolesTest( + `reserved privilege assigned with a base privilege returns empty kibana section with _transform_error set to ['kibana']`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['reserved_foo', 'read'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + ], + }, + } + ); + + getRolesTest( + `reserved privilege assigned with a feature privilege returns empty kibana section with _transform_error set to ['kibana']`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['reserved_foo', 'feature_foo.foo-privilege-1'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + ], + }, + } + ); + + getRolesTest( + `global base privilege assigned with a feature privilege returns empty kibana section with _transform_error set to ['kibana']`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['all', 'feature_foo.foo-privilege-1'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + ], + }, + } + ); + + getRolesTest( + `space base privilege assigned with a feature privilege returns empty kibana section with _transform_error set to ['kibana']`, + { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['space_all', 'feature_foo.foo-privilege-1'], + resources: ['space:space_1'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + ], + }, + } + ); + + getRolesTest(`transforms unrecognized applications`, { + apiResponse: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application: 'kibana-.another-kibana', + privileges: ['read'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: [], + _unrecognized_applications: ['kibana-.another-kibana'], + }, + ], + }, + }); + + getRolesTest(`returns a sorted list of roles`, { + apiResponse: async () => ({ + z_role: { + cluster: [], + indices: [], + applications: [ + { + application: 'kibana-.another-kibana', + privileges: ['read'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + a_role: { + cluster: [], + indices: [], + applications: [ + { + application: 'kibana-.another-kibana', + privileges: ['read'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + b_role: { + cluster: [], + indices: [], + applications: [ + { + application: 'kibana-.another-kibana', + privileges: ['read'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'a_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: [], + _unrecognized_applications: ['kibana-.another-kibana'], + }, + { + name: 'b_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: [], + _unrecognized_applications: ['kibana-.another-kibana'], + }, + { + name: 'z_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: [], + _unrecognized_applications: ['kibana-.another-kibana'], + }, + ], + }, + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts new file mode 100644 index 00000000000000..f5d2d51280fc42 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts @@ -0,0 +1,52 @@ +/* + * 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 { RouteDefinitionParams } from '../..'; +import { createLicensedRouteHandler } from '../../licensed_route_handler'; +import { wrapError } from '../../../errors'; +import { ElasticsearchRole, transformElasticsearchRoleToRole } from './model'; + +export function defineGetAllRolesRoutes({ router, authz, clusterClient }: RouteDefinitionParams) { + router.get( + { path: '/api/security/role', validate: false }, + createLicensedRouteHandler(async (context, request, response) => { + try { + const elasticsearchRoles = (await clusterClient + .asScoped(request) + .callAsCurrentUser('shield.getRole')) as Record; + + // Transform elasticsearch roles into Kibana roles and return in a list sorted by the role name. + return response.ok({ + body: Object.entries(elasticsearchRoles) + .map(([roleName, elasticsearchRole]) => + transformElasticsearchRoleToRole( + elasticsearchRole, + roleName, + authz.getApplicationName() + ) + ) + .sort((roleA, roleB) => { + if (roleA.name < roleB.name) { + return -1; + } + + if (roleA.name > roleB.name) { + return 1; + } + + return 0; + }), + }); + } catch (error) { + const wrappedError = wrapError(error); + return response.customError({ + body: wrappedError, + statusCode: wrappedError.output.statusCode, + }); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/authorization/roles/index.ts b/x-pack/plugins/security/server/routes/authorization/roles/index.ts new file mode 100644 index 00000000000000..39cb31b9d20f3d --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/roles/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { RouteDefinitionParams } from '../..'; +import { defineGetRolesRoutes } from './get'; +import { defineGetAllRolesRoutes } from './get_all'; +import { defineDeleteRolesRoutes } from './delete'; +import { definePutRolesRoutes } from './put'; + +export function defineRolesRoutes(params: RouteDefinitionParams) { + defineGetRolesRoutes(params); + defineGetAllRolesRoutes(params); + defineDeleteRolesRoutes(params); + definePutRolesRoutes(params); +} diff --git a/x-pack/plugins/security/server/routes/authorization/roles/model/elasticsearch_role.ts b/x-pack/plugins/security/server/routes/authorization/roles/model/elasticsearch_role.ts new file mode 100644 index 00000000000000..c590c24923a8c0 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/roles/model/elasticsearch_role.ts @@ -0,0 +1,274 @@ +/* + * 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 { Role, RoleKibanaPrivilege } from '../../../../../common/model'; +import { + GLOBAL_RESOURCE, + RESERVED_PRIVILEGES_APPLICATION_WILDCARD, +} from '../../../../../common/constants'; +import { PrivilegeSerializer } from '../../../../authorization/privilege_serializer'; +import { ResourceSerializer } from '../../../../authorization/resource_serializer'; + +export type ElasticsearchRole = Pick & { + applications: Array<{ + application: string; + privileges: string[]; + resources: string[]; + }>; + cluster: Role['elasticsearch']['cluster']; + indices: Role['elasticsearch']['indices']; + run_as: Role['elasticsearch']['run_as']; +}; + +export function transformElasticsearchRoleToRole( + elasticsearchRole: ElasticsearchRole, + name: string, + application: string +): Role { + const kibanaTransformResult = transformRoleApplicationsToKibanaPrivileges( + elasticsearchRole.applications, + application + ); + + return { + name, + metadata: elasticsearchRole.metadata, + transient_metadata: elasticsearchRole.transient_metadata, + elasticsearch: { + cluster: elasticsearchRole.cluster, + indices: elasticsearchRole.indices, + run_as: elasticsearchRole.run_as, + }, + kibana: kibanaTransformResult.success ? (kibanaTransformResult.value as Role['kibana']) : [], + _transform_error: [...(kibanaTransformResult.success ? [] : ['kibana'])], + _unrecognized_applications: extractUnrecognizedApplicationNames( + elasticsearchRole.applications, + application + ), + }; +} + +function transformRoleApplicationsToKibanaPrivileges( + roleApplications: ElasticsearchRole['applications'], + application: string +) { + const roleKibanaApplications = roleApplications.filter( + roleApplication => + roleApplication.application === application || + roleApplication.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD + ); + + // if any application entry contains an empty resource, we throw an error + if (roleKibanaApplications.some(entry => entry.resources.length === 0)) { + throw new Error(`ES returned an application entry without resources, can't process this`); + } + + // if there is an entry with the reserved privileges application wildcard + // and there are privileges which aren't reserved, we won't transform these + if ( + roleKibanaApplications.some( + entry => + entry.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD && + !entry.privileges.every(privilege => + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ) + ) + ) { + return { + success: false, + }; + } + + // if space privilege assigned globally, we can't transform these + if ( + roleKibanaApplications.some( + entry => + entry.resources.includes(GLOBAL_RESOURCE) && + entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege) + ) + ) + ) { + return { + success: false, + }; + } + + // if global base or reserved privilege assigned at a space, we can't transform these + if ( + roleKibanaApplications.some( + entry => + !entry.resources.includes(GLOBAL_RESOURCE) && + entry.privileges.some( + privilege => + PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) || + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ) + ) + ) { + return { + success: false, + }; + } + + // if reserved privilege assigned with feature or base privileges, we won't transform these + if ( + roleKibanaApplications.some( + entry => + entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ) && + entry.privileges.some( + privilege => !PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ) + ) + ) { + return { + success: false, + }; + } + + // if base privilege assigned with feature privileges, we won't transform these + if ( + roleKibanaApplications.some( + entry => + entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedFeaturePrivilege(privilege) + ) && + (entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) + ) || + entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege) + )) + ) + ) { + return { + success: false, + }; + } + + // if any application entry contains the '*' resource in addition to another resource, we can't transform these + if ( + roleKibanaApplications.some( + entry => entry.resources.includes(GLOBAL_RESOURCE) && entry.resources.length > 1 + ) + ) { + return { + success: false, + }; + } + + const allResources = roleKibanaApplications.map(entry => entry.resources).flat(); + // if we have improperly formatted resource entries, we can't transform these + if ( + allResources.some( + resource => + resource !== GLOBAL_RESOURCE && !ResourceSerializer.isSerializedSpaceResource(resource) + ) + ) { + return { + success: false, + }; + } + + // if we have resources duplicated in entries, we won't transform these + if (allResources.length !== getUniqueList(allResources).length) { + return { + success: false, + }; + } + + return { + success: true, + value: roleKibanaApplications.map(({ resources, privileges }) => { + // if we're dealing with a global entry, which we've ensured above is only possible if it's the only item in the array + if (resources.length === 1 && resources[0] === GLOBAL_RESOURCE) { + const reservedPrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + ); + const basePrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) + ); + const featurePrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedFeaturePrivilege(privilege) + ); + + return { + ...(reservedPrivileges.length + ? { + _reserved: reservedPrivileges.map(privilege => + PrivilegeSerializer.deserializeReservedPrivilege(privilege) + ), + } + : {}), + base: basePrivileges.map(privilege => + PrivilegeSerializer.serializeGlobalBasePrivilege(privilege) + ), + feature: featurePrivileges.reduce( + (acc, privilege) => { + const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); + return { + ...acc, + [featurePrivilege.featureId]: getUniqueList([ + ...(acc[featurePrivilege.featureId] || []), + featurePrivilege.privilege, + ]), + }; + }, + {} as RoleKibanaPrivilege['feature'] + ), + spaces: ['*'], + }; + } + + const basePrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege) + ); + const featurePrivileges = privileges.filter(privilege => + PrivilegeSerializer.isSerializedFeaturePrivilege(privilege) + ); + return { + base: basePrivileges.map(privilege => + PrivilegeSerializer.deserializeSpaceBasePrivilege(privilege) + ), + feature: featurePrivileges.reduce( + (acc, privilege) => { + const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); + return { + ...acc, + [featurePrivilege.featureId]: getUniqueList([ + ...(acc[featurePrivilege.featureId] || []), + featurePrivilege.privilege, + ]), + }; + }, + {} as RoleKibanaPrivilege['feature'] + ), + spaces: resources.map(resource => ResourceSerializer.deserializeSpaceResource(resource)), + }; + }), + }; +} + +const extractUnrecognizedApplicationNames = ( + roleApplications: ElasticsearchRole['applications'], + application: string +) => { + return getUniqueList( + roleApplications + .filter( + roleApplication => + roleApplication.application !== application && + roleApplication.application !== RESERVED_PRIVILEGES_APPLICATION_WILDCARD + ) + .map(roleApplication => roleApplication.application) + ); +}; + +function getUniqueList(list: T[]) { + return Array.from(new Set(list)); +} diff --git a/x-pack/plugins/security/server/routes/authorization/roles/model/index.ts b/x-pack/plugins/security/server/routes/authorization/roles/model/index.ts new file mode 100644 index 00000000000000..8cf4956c2ac171 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/roles/model/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { ElasticsearchRole, transformElasticsearchRoleToRole } from './elasticsearch_role'; +export { getPutPayloadSchema, transformPutPayloadToElasticsearchRole } from './put_payload'; diff --git a/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.test.ts new file mode 100644 index 00000000000000..e9ba5c41c3988f --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.test.ts @@ -0,0 +1,346 @@ +/* + * 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 { getPutPayloadSchema } from './put_payload'; + +const basePrivilegeNamesMap = { + global: ['all', 'read'], + space: ['all', 'read'], +}; + +describe('Put payload schema', () => { + test('only allows features that match the pattern', () => { + expect(() => + getPutPayloadSchema(() => basePrivilegeNamesMap).validate({ + kibana: [{ feature: { '!foo': ['foo'] } }], + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[kibana.0.feature.key(\\"!foo\\")]: only a-z, A-Z, 0-9, '_', and '-' are allowed"` + ); + }); + + test('only allows feature privileges that match the pattern', () => { + expect(() => + getPutPayloadSchema(() => basePrivilegeNamesMap).validate({ + kibana: [{ feature: { foo: ['!foo'] } }], + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[kibana.0.feature.foo]: only a-z, A-Z, 0-9, '_', and '-' are allowed"` + ); + }); + + test('requires either base or feature', () => { + for (const kibanaPrivilege of [ + {}, + { base: [] }, + { feature: {} }, + { feature: { foo: [], bar: [] } }, + { base: [], feature: {} }, + { base: [], feature: { foo: [], bar: [] } }, + ]) { + expect(() => + getPutPayloadSchema(() => basePrivilegeNamesMap).validate({ + kibana: [kibanaPrivilege], + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[kibana.0]: either [base] or [feature] is expected, but none of them specified"` + ); + } + }); + + test(`doesn't allow both base and feature in the same entry`, () => { + expect(() => + getPutPayloadSchema(() => basePrivilegeNamesMap).validate({ + kibana: [{ base: ['all'], feature: { foo: ['foo'] } }], + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[kibana.0]: definition of [feature] isn't allowed when non-empty [base] is defined."` + ); + }); + + describe('global', () => { + test(`only allows known Kibana global base privileges`, () => { + expect(() => + getPutPayloadSchema(() => basePrivilegeNamesMap).validate({ + kibana: [{ base: ['foo'], spaces: ['*'] }], + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[kibana.0.base.0]: unknown global privilege \\"foo\\", must be one of [all,read]"` + ); + }); + + test(`doesn't allow Kibana reserved privileges`, () => { + expect(() => + getPutPayloadSchema(() => basePrivilegeNamesMap).validate({ + kibana: [{ _reserved: ['customApplication1'], spaces: ['*'] }], + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[kibana.0._reserved]: definition for this key is missing"` + ); + }); + + test(`only allows one global entry`, () => { + expect(() => + getPutPayloadSchema(() => basePrivilegeNamesMap).validate({ + kibana: [ + { feature: { foo: ['foo-privilege-1'] }, spaces: ['*'] }, + { feature: { bar: ['bar-privilege-1'] }, spaces: ['*'] }, + ], + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[kibana]: more than one privilege is applied to the following spaces: [*]"` + ); + }); + }); + + describe('space', () => { + test(`doesn't allow * in a space ID`, () => { + expect(() => + getPutPayloadSchema(() => basePrivilegeNamesMap).validate({ + kibana: [{ spaces: ['foo-*'] }], + }) + ).toThrowErrorMatchingInlineSnapshot(` +"[kibana.0.spaces]: types that failed validation: +- [kibana.0.spaces.0.0]: expected value to equal [*] but got [foo-*] +- [kibana.0.spaces.1.0]: must be lower case, a-z, 0-9, '_', and '-' are allowed" +`); + }); + + test(`can't assign space and global in same entry`, () => { + expect(() => + getPutPayloadSchema(() => basePrivilegeNamesMap).validate({ + kibana: [{ spaces: ['*', 'foo-space'] }], + }) + ).toThrowErrorMatchingInlineSnapshot(` +"[kibana.0.spaces]: types that failed validation: +- [kibana.0.spaces.0.1]: expected value to equal [*] but got [foo-space] +- [kibana.0.spaces.1.0]: must be lower case, a-z, 0-9, '_', and '-' are allowed" +`); + }); + + test(`only allows known Kibana space base privileges`, () => { + expect(() => + getPutPayloadSchema(() => basePrivilegeNamesMap).validate({ + kibana: [{ base: ['foo'], spaces: ['foo-space'] }], + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[kibana.0.base.0]: unknown space privilege \\"foo\\", must be one of [all,read]"` + ); + }); + + test(`only allows space to be in one entry`, () => { + expect(() => + getPutPayloadSchema(() => basePrivilegeNamesMap).validate({ + kibana: [ + { feature: { foo: ['foo-privilege-1'] }, spaces: ['marketing'] }, + { feature: { bar: ['bar-privilege-1'] }, spaces: ['sales', 'marketing'] }, + ], + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[kibana]: more than one privilege is applied to the following spaces: [marketing]"` + ); + }); + + test(`doesn't allow Kibana reserved privileges`, () => { + expect(() => + getPutPayloadSchema(() => basePrivilegeNamesMap).validate({ + kibana: [{ _reserved: ['customApplication1'], spaces: ['marketing'] }], + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[kibana.0._reserved]: definition for this key is missing"` + ); + }); + }); + + test('allows empty role', () => { + expect(getPutPayloadSchema(() => basePrivilegeNamesMap).validate({})).toMatchInlineSnapshot(` + Object { + "elasticsearch": Object {}, + } + `); + }); + + test('if spaces is not specified, defaults to global', () => { + expect( + getPutPayloadSchema(() => basePrivilegeNamesMap).validate({ + kibana: [{ base: ['all'] }], + }) + ).toMatchInlineSnapshot(` + Object { + "elasticsearch": Object {}, + "kibana": Array [ + Object { + "base": Array [ + "all", + ], + "spaces": Array [ + "*", + ], + }, + ], + } + `); + }); + + test('allows base with empty array and feature in the same entry', () => { + expect( + getPutPayloadSchema(() => basePrivilegeNamesMap).validate({ + kibana: [{ base: [], feature: { foo: ['foo'] } }], + }) + ).toMatchInlineSnapshot(` + Object { + "elasticsearch": Object {}, + "kibana": Array [ + Object { + "base": Array [], + "feature": Object { + "foo": Array [ + "foo", + ], + }, + "spaces": Array [ + "*", + ], + }, + ], + } + `); + }); + + test('allows base and feature with empty object in the same entry', () => { + expect( + getPutPayloadSchema(() => basePrivilegeNamesMap).validate({ + kibana: [{ base: ['all'], feature: {} }], + }) + ).toMatchInlineSnapshot(` + Object { + "elasticsearch": Object {}, + "kibana": Array [ + Object { + "base": Array [ + "all", + ], + "feature": Object {}, + "spaces": Array [ + "*", + ], + }, + ], + } + `); + }); + + test('allows full set of fields', () => { + expect( + getPutPayloadSchema(() => basePrivilegeNamesMap).validate({ + metadata: { + foo: 'test-metadata', + }, + elasticsearch: { + cluster: ['test-cluster-privilege'], + indices: [ + { + field_security: { + grant: ['test-field-security-grant-1', 'test-field-security-grant-2'], + except: ['test-field-security-except-1', 'test-field-security-except-2'], + }, + names: ['test-index-name-1', 'test-index-name-2'], + privileges: ['test-index-privilege-1', 'test-index-privilege-2'], + query: `{ "match": { "title": "foo" } }`, + }, + ], + run_as: ['test-run-as-1', 'test-run-as-2'], + }, + kibana: [ + { + base: ['all', 'read'], + spaces: ['*'], + }, + { + base: ['all', 'read'], + spaces: ['test-space-1', 'test-space-2'], + }, + { + feature: { + foo: ['foo-privilege-1', 'foo-privilege-2'], + }, + spaces: ['test-space-3'], + }, + ], + }) + ).toMatchInlineSnapshot(` + Object { + "elasticsearch": Object { + "cluster": Array [ + "test-cluster-privilege", + ], + "indices": Array [ + Object { + "field_security": Object { + "except": Array [ + "test-field-security-except-1", + "test-field-security-except-2", + ], + "grant": Array [ + "test-field-security-grant-1", + "test-field-security-grant-2", + ], + }, + "names": Array [ + "test-index-name-1", + "test-index-name-2", + ], + "privileges": Array [ + "test-index-privilege-1", + "test-index-privilege-2", + ], + "query": "{ \\"match\\": { \\"title\\": \\"foo\\" } }", + }, + ], + "run_as": Array [ + "test-run-as-1", + "test-run-as-2", + ], + }, + "kibana": Array [ + Object { + "base": Array [ + "all", + "read", + ], + "spaces": Array [ + "*", + ], + }, + Object { + "base": Array [ + "all", + "read", + ], + "spaces": Array [ + "test-space-1", + "test-space-2", + ], + }, + Object { + "feature": Object { + "foo": Array [ + "foo-privilege-1", + "foo-privilege-2", + ], + }, + "spaces": Array [ + "test-space-3", + ], + }, + ], + "metadata": Object { + "foo": "test-metadata", + }, + } + `); + }); +}); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts b/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts new file mode 100644 index 00000000000000..a5f6b2fd9fcc1f --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts @@ -0,0 +1,300 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; +import { GLOBAL_RESOURCE } from '../../../../../common/constants'; +import { PrivilegeSerializer } from '../../../../authorization/privilege_serializer'; +import { ResourceSerializer } from '../../../../authorization/resource_serializer'; +import { ElasticsearchRole } from './elasticsearch_role'; + +/** + * Elasticsearch specific portion of the role definition. + * See more details at https://www.elastic.co/guide/en/elasticsearch/reference/master/security-api.html#security-role-apis. + */ +const elasticsearchRoleSchema = schema.object({ + /** + * An optional list of cluster privileges. These privileges define the cluster level actions that + * users with this role are able to execute + */ + cluster: schema.maybe(schema.arrayOf(schema.string())), + + /** + * An optional list of indices permissions entries. + */ + indices: schema.maybe( + schema.arrayOf( + schema.object({ + /** + * Required list of indices (or index name patterns) to which the permissions in this + * entry apply. + */ + names: schema.arrayOf(schema.string(), { minSize: 1 }), + + /** + * An optional set of the document fields that the owners of the role have read access to. + */ + field_security: schema.maybe( + schema.recordOf( + schema.oneOf([schema.literal('grant'), schema.literal('except')]), + schema.arrayOf(schema.string()) + ) + ), + + /** + * Required list of the index level privileges that the owners of the role have on the + * specified indices. + */ + privileges: schema.arrayOf(schema.string(), { minSize: 1 }), + + /** + * An optional search query that defines the documents the owners of the role have read access + * to. A document within the specified indices must match this query in order for it to be + * accessible by the owners of the role. + */ + query: schema.maybe(schema.string()), + + /** + * An optional flag used to indicate if index pattern wildcards or regexps should cover + * restricted indices. + */ + allow_restricted_indices: schema.maybe(schema.boolean()), + }) + ) + ), + + /** + * An optional list of users that the owners of this role can impersonate. + */ + run_as: schema.maybe(schema.arrayOf(schema.string())), +}); + +const allSpacesSchema = schema.arrayOf(schema.literal(GLOBAL_RESOURCE), { + minSize: 1, + maxSize: 1, +}); + +/** + * Schema for the list of space IDs used within Kibana specific role definition. + */ +const spacesSchema = schema.oneOf( + [ + allSpacesSchema, + schema.arrayOf( + schema.string({ + validate(value) { + if (!/^[a-z0-9_-]+$/.test(value)) { + return `must be lower case, a-z, 0-9, '_', and '-' are allowed`; + } + }, + }) + ), + ], + { defaultValue: [GLOBAL_RESOURCE] } +); + +const FEATURE_NAME_VALUE_REGEX = /^[a-zA-Z0-9_-]+$/; + +type PutPayloadSchemaType = TypeOf>; +export function getPutPayloadSchema( + getBasePrivilegeNames: () => { global: string[]; space: string[] } +) { + return schema.object({ + /** + * An optional meta-data dictionary. Within the metadata, keys that begin with _ are reserved + * for system usage. + */ + metadata: schema.maybe(schema.recordOf(schema.string(), schema.any())), + + /** + * Elasticsearch specific portion of the role definition. + */ + elasticsearch: elasticsearchRoleSchema, + + /** + * Kibana specific portion of the role definition. It's represented as a list of base and/or + * feature Kibana privileges. None of the entries should apply to the same spaces. + */ + kibana: schema.maybe( + schema.arrayOf( + schema.object( + { + /** + * An optional list of space IDs to which the permissions in this entry apply. If not + * specified it defaults to special "global" space ID (all spaces). + */ + spaces: spacesSchema, + + /** + * An optional list of Kibana base privileges. If this entry applies to special "global" + * space (all spaces) then specified base privileges should be within known base "global" + * privilege list, otherwise - within known "space" privilege list. Base privileges + * definition isn't allowed when feature privileges are defined and required otherwise. + */ + base: schema.maybe( + schema.conditional( + schema.siblingRef('spaces'), + allSpacesSchema, + schema.arrayOf( + schema.string({ + validate(value) { + const globalPrivileges = getBasePrivilegeNames().global; + if (!globalPrivileges.some(privilege => privilege === value)) { + return `unknown global privilege "${value}", must be one of [${globalPrivileges}]`; + } + }, + }) + ), + schema.arrayOf( + schema.string({ + validate(value) { + const spacePrivileges = getBasePrivilegeNames().space; + if (!spacePrivileges.some(privilege => privilege === value)) { + return `unknown space privilege "${value}", must be one of [${spacePrivileges}]`; + } + }, + }) + ) + ) + ), + + /** + * An optional dictionary of Kibana feature privileges where the key is the ID of the + * feature and the value is a list of feature specific privilege IDs. Both feature and + * privilege IDs should consist of allowed set of characters. Feature privileges + * definition isn't allowed when base privileges are defined and required otherwise. + */ + feature: schema.maybe( + schema.recordOf( + schema.string({ + validate(value) { + if (!FEATURE_NAME_VALUE_REGEX.test(value)) { + return `only a-z, A-Z, 0-9, '_', and '-' are allowed`; + } + }, + }), + schema.arrayOf( + schema.string({ + validate(value) { + if (!FEATURE_NAME_VALUE_REGEX.test(value)) { + return `only a-z, A-Z, 0-9, '_', and '-' are allowed`; + } + }, + }) + ) + ) + ), + }, + { + validate(value) { + if ( + (value.base === undefined || value.base.length === 0) && + (value.feature === undefined || Object.values(value.feature).flat().length === 0) + ) { + return 'either [base] or [feature] is expected, but none of them specified'; + } + + if ( + value.base !== undefined && + value.base.length > 0 && + value.feature !== undefined && + Object.keys(value.feature).length > 0 + ) { + return `definition of [feature] isn't allowed when non-empty [base] is defined.`; + } + }, + } + ), + { + validate(value) { + for (const [indexA, valueA] of value.entries()) { + for (const valueB of value.slice(indexA + 1)) { + const spaceIntersection = _.intersection(valueA.spaces, valueB.spaces); + if (spaceIntersection.length !== 0) { + return `more than one privilege is applied to the following spaces: [${spaceIntersection}]`; + } + } + } + }, + } + ) + ), + }); +} + +export const transformPutPayloadToElasticsearchRole = ( + rolePayload: PutPayloadSchemaType, + application: string, + allExistingApplications: ElasticsearchRole['applications'] = [] +) => { + const { + elasticsearch = { cluster: undefined, indices: undefined, run_as: undefined }, + kibana = [], + } = rolePayload; + const otherApplications = allExistingApplications.filter( + roleApplication => roleApplication.application !== application + ); + + return { + metadata: rolePayload.metadata, + cluster: elasticsearch.cluster || [], + indices: elasticsearch.indices || [], + run_as: elasticsearch.run_as || [], + applications: [ + ...transformPrivilegesToElasticsearchPrivileges(application, kibana), + ...otherApplications, + ], + } as Omit; +}; + +const transformPrivilegesToElasticsearchPrivileges = ( + application: string, + kibanaPrivileges: PutPayloadSchemaType['kibana'] = [] +) => { + return kibanaPrivileges.map(({ base, feature, spaces }) => { + if (spaces.length === 1 && spaces[0] === GLOBAL_RESOURCE) { + return { + privileges: [ + ...(base + ? base.map(privilege => PrivilegeSerializer.serializeGlobalBasePrivilege(privilege)) + : []), + ...(feature + ? Object.entries(feature) + .map(([featureName, featurePrivileges]) => + featurePrivileges.map(privilege => + PrivilegeSerializer.serializeFeaturePrivilege(featureName, privilege) + ) + ) + .flat() + : []), + ], + application, + resources: [GLOBAL_RESOURCE], + }; + } + + return { + privileges: [ + ...(base + ? base.map(privilege => PrivilegeSerializer.serializeSpaceBasePrivilege(privilege)) + : []), + ...(feature + ? Object.entries(feature) + .map(([featureName, featurePrivileges]) => + featurePrivileges.map(privilege => + PrivilegeSerializer.serializeFeaturePrivilege(featureName, privilege) + ) + ) + .flat() + : []), + ], + application, + resources: (spaces as string[]).map(resource => + ResourceSerializer.serializeSpaceResource(resource) + ), + }; + }); +}; diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts new file mode 100644 index 00000000000000..fa4f2350bb7dd9 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts @@ -0,0 +1,603 @@ +/* + * 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 { Type } from '@kbn/config-schema'; +import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../../src/core/server'; +import { ILicenseCheck } from '../../../../../licensing/server'; +import { LICENSE_STATUS } from '../../../../../licensing/server/constants'; +import { GLOBAL_RESOURCE } from '../../../../common/constants'; +import { definePutRolesRoutes } from './put'; + +import { + elasticsearchServiceMock, + httpServerMock, +} from '../../../../../../../src/core/server/mocks'; +import { routeDefinitionParamsMock } from '../../index.mock'; + +const application = 'kibana-.kibana'; +const privilegeMap = { + global: { + all: [], + read: [], + }, + space: { + all: [], + read: [], + }, + features: { + foo: { + 'foo-privilege-1': [], + 'foo-privilege-2': [], + }, + bar: { + 'bar-privilege-1': [], + 'bar-privilege-2': [], + }, + }, + reserved: { + customApplication1: [], + customApplication2: [], + }, +}; + +interface TestOptions { + name: string; + licenseCheckResult?: ILicenseCheck; + apiResponses?: Array<() => Promise>; + payload?: Record; + asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; +} + +const putRoleTest = ( + description: string, + { + name, + payload, + licenseCheckResult = { check: LICENSE_STATUS.Valid }, + apiResponses = [], + asserts, + }: TestOptions +) => { + test(description, async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + mockRouteDefinitionParams.authz.getApplicationName.mockReturnValue(application); + mockRouteDefinitionParams.authz.privileges.get.mockReturnValue(privilegeMap); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + for (const apiResponse of apiResponses) { + mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); + } + + definePutRolesRoutes(mockRouteDefinitionParams); + const [[{ validate }, handler]] = mockRouteDefinitionParams.router.put.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'put', + path: `/api/security/role/${name}`, + params: { name }, + body: payload !== undefined ? (validate as any).body.validate(payload) : undefined, + headers, + }); + const mockContext = ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + if (Array.isArray(asserts.apiArguments)) { + for (const apiArguments of asserts.apiArguments) { + expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); + } + } else { + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); + }); +}; + +describe('PUT role', () => { + describe('request validation', () => { + let requestParamsSchema: Type; + beforeEach(() => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + mockRouteDefinitionParams.authz.privileges.get.mockReturnValue(privilegeMap); + definePutRolesRoutes(mockRouteDefinitionParams); + + const [[{ validate }]] = mockRouteDefinitionParams.router.put.mock.calls; + requestParamsSchema = (validate as any).params; + }); + + test('requires name in params', () => { + expect(() => + requestParamsSchema.validate({}, {}, 'request params') + ).toThrowErrorMatchingInlineSnapshot( + `"[request params.name]: expected value of type [string] but got [undefined]"` + ); + + expect(() => + requestParamsSchema.validate({ name: '' }, {}, 'request params') + ).toThrowErrorMatchingInlineSnapshot( + `"[request params.name]: value is [] but it must have a minimum length of [1]."` + ); + }); + + test('requires name in params to not exceed 1024 characters', () => { + expect(() => + requestParamsSchema.validate({ name: 'a'.repeat(1025) }, {}, 'request params') + ).toThrowErrorMatchingInlineSnapshot( + `"[request params.name]: value is [aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] but it must have a maximum length of [1024]."` + ); + }); + }); + + describe('failure', () => { + putRoleTest(`returns result of license checker`, { + name: 'foo-role', + licenseCheckResult: { check: LICENSE_STATUS.Invalid, message: 'test forbidden message' }, + asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, + }); + }); + + describe('success', () => { + putRoleTest(`creates empty role`, { + name: 'foo-role', + payload: {}, + apiResponses: [async () => ({}), async () => {}], + asserts: { + apiArguments: [ + ['shield.getRole', { name: 'foo-role', ignore: [404] }], + [ + 'shield.putRole', + { + name: 'foo-role', + body: { + cluster: [], + indices: [], + run_as: [], + applications: [], + }, + }, + ], + ], + statusCode: 204, + result: undefined, + }, + }); + + putRoleTest(`if spaces isn't specified, defaults to global`, { + name: 'foo-role', + payload: { + kibana: [ + { + base: ['all'], + }, + ], + }, + apiResponses: [async () => ({}), async () => {}], + asserts: { + apiArguments: [ + ['shield.getRole', { name: 'foo-role', ignore: [404] }], + [ + 'shield.putRole', + { + name: 'foo-role', + body: { + cluster: [], + indices: [], + run_as: [], + applications: [ + { + application, + privileges: ['all'], + resources: [GLOBAL_RESOURCE], + }, + ], + }, + }, + ], + ], + statusCode: 204, + result: undefined, + }, + }); + + putRoleTest(`allows base with empty array and feature in the same entry`, { + name: 'foo-role', + payload: { + kibana: [ + { + base: [], + feature: { + foo: ['foo'], + }, + }, + ], + }, + apiResponses: [async () => ({}), async () => {}], + asserts: { + apiArguments: [ + ['shield.getRole', { name: 'foo-role', ignore: [404] }], + [ + 'shield.putRole', + { + name: 'foo-role', + body: { + cluster: [], + indices: [], + run_as: [], + applications: [ + { + application, + privileges: ['feature_foo.foo'], + resources: [GLOBAL_RESOURCE], + }, + ], + }, + }, + ], + ], + statusCode: 204, + result: undefined, + }, + }); + + putRoleTest(`allows base and feature with empty object in the same entry`, { + name: 'foo-role', + payload: { + kibana: [ + { + base: ['all'], + feature: {}, + }, + ], + }, + apiResponses: [async () => ({}), async () => {}], + asserts: { + apiArguments: [ + ['shield.getRole', { name: 'foo-role', ignore: [404] }], + [ + 'shield.putRole', + { + name: 'foo-role', + body: { + cluster: [], + indices: [], + run_as: [], + applications: [ + { + application, + privileges: ['all'], + resources: [GLOBAL_RESOURCE], + }, + ], + }, + }, + ], + ], + statusCode: 204, + result: undefined, + }, + }); + + putRoleTest(`creates role with everything`, { + name: 'foo-role', + payload: { + metadata: { + foo: 'test-metadata', + }, + elasticsearch: { + cluster: ['test-cluster-privilege'], + indices: [ + { + field_security: { + grant: ['test-field-security-grant-1', 'test-field-security-grant-2'], + except: ['test-field-security-except-1', 'test-field-security-except-2'], + }, + names: ['test-index-name-1', 'test-index-name-2'], + privileges: ['test-index-privilege-1', 'test-index-privilege-2'], + query: `{ "match": { "title": "foo" } }`, + }, + ], + run_as: ['test-run-as-1', 'test-run-as-2'], + }, + kibana: [ + { + base: ['all', 'read'], + spaces: ['*'], + }, + { + base: ['all', 'read'], + spaces: ['test-space-1', 'test-space-2'], + }, + { + feature: { + foo: ['foo-privilege-1', 'foo-privilege-2'], + }, + spaces: ['test-space-3'], + }, + ], + }, + apiResponses: [async () => ({}), async () => {}], + asserts: { + apiArguments: [ + ['shield.getRole', { name: 'foo-role', ignore: [404] }], + [ + 'shield.putRole', + { + name: 'foo-role', + body: { + applications: [ + { + application, + privileges: ['all', 'read'], + resources: [GLOBAL_RESOURCE], + }, + { + application, + privileges: ['space_all', 'space_read'], + resources: ['space:test-space-1', 'space:test-space-2'], + }, + { + application, + privileges: ['feature_foo.foo-privilege-1', 'feature_foo.foo-privilege-2'], + resources: ['space:test-space-3'], + }, + ], + cluster: ['test-cluster-privilege'], + indices: [ + { + field_security: { + grant: ['test-field-security-grant-1', 'test-field-security-grant-2'], + except: ['test-field-security-except-1', 'test-field-security-except-2'], + }, + names: ['test-index-name-1', 'test-index-name-2'], + privileges: ['test-index-privilege-1', 'test-index-privilege-2'], + query: `{ "match": { "title": "foo" } }`, + }, + ], + metadata: { foo: 'test-metadata' }, + run_as: ['test-run-as-1', 'test-run-as-2'], + }, + }, + ], + ], + statusCode: 204, + result: undefined, + }, + }); + + putRoleTest(`updates role which has existing kibana privileges`, { + name: 'foo-role', + payload: { + metadata: { + foo: 'test-metadata', + }, + elasticsearch: { + cluster: ['test-cluster-privilege'], + indices: [ + { + field_security: { + grant: ['test-field-security-grant-1', 'test-field-security-grant-2'], + except: ['test-field-security-except-1', 'test-field-security-except-2'], + }, + names: ['test-index-name-1', 'test-index-name-2'], + privileges: ['test-index-privilege-1', 'test-index-privilege-2'], + query: `{ "match": { "title": "foo" } }`, + }, + ], + run_as: ['test-run-as-1', 'test-run-as-2'], + }, + kibana: [ + { + feature: { + foo: ['foo-privilege-1'], + bar: ['bar-privilege-1'], + }, + spaces: ['*'], + }, + { + base: ['all'], + spaces: ['test-space-1', 'test-space-2'], + }, + { + feature: { + bar: ['bar-privilege-2'], + }, + spaces: ['test-space-3'], + }, + ], + }, + apiResponses: [ + async () => ({ + 'foo-role': { + metadata: { + bar: 'old-metadata', + }, + transient_metadata: { + enabled: true, + }, + cluster: ['old-cluster-privilege'], + indices: [ + { + field_security: { + grant: ['old-field-security-grant-1', 'old-field-security-grant-2'], + except: ['old-field-security-except-1', 'old-field-security-except-2'], + }, + names: ['old-index-name'], + privileges: ['old-privilege'], + query: `{ "match": { "old-title": "foo" } }`, + }, + ], + run_as: ['old-run-as'], + applications: [ + { + application, + privileges: ['old-kibana-privilege'], + resources: ['old-resource'], + }, + ], + }, + }), + async () => {}, + ], + asserts: { + apiArguments: [ + ['shield.getRole', { name: 'foo-role', ignore: [404] }], + [ + 'shield.putRole', + { + name: 'foo-role', + body: { + applications: [ + { + application, + privileges: ['feature_foo.foo-privilege-1', 'feature_bar.bar-privilege-1'], + resources: [GLOBAL_RESOURCE], + }, + { + application, + privileges: ['space_all'], + resources: ['space:test-space-1', 'space:test-space-2'], + }, + { + application, + privileges: ['feature_bar.bar-privilege-2'], + resources: ['space:test-space-3'], + }, + ], + cluster: ['test-cluster-privilege'], + indices: [ + { + field_security: { + grant: ['test-field-security-grant-1', 'test-field-security-grant-2'], + except: ['test-field-security-except-1', 'test-field-security-except-2'], + }, + names: ['test-index-name-1', 'test-index-name-2'], + privileges: ['test-index-privilege-1', 'test-index-privilege-2'], + query: `{ "match": { "title": "foo" } }`, + }, + ], + metadata: { foo: 'test-metadata' }, + run_as: ['test-run-as-1', 'test-run-as-2'], + }, + }, + ], + ], + statusCode: 204, + result: undefined, + }, + }); + + putRoleTest(`updates role which has existing other application privileges`, { + name: 'foo-role', + payload: { + metadata: { + foo: 'test-metadata', + }, + elasticsearch: { + cluster: ['test-cluster-privilege'], + indices: [ + { + names: ['test-index-name-1', 'test-index-name-2'], + privileges: ['test-index-privilege-1', 'test-index-privilege-2'], + }, + ], + run_as: ['test-run-as-1', 'test-run-as-2'], + }, + kibana: [ + { + base: ['all', 'read'], + spaces: ['*'], + }, + ], + }, + apiResponses: [ + async () => ({ + 'foo-role': { + metadata: { + bar: 'old-metadata', + }, + transient_metadata: { + enabled: true, + }, + cluster: ['old-cluster-privilege'], + indices: [ + { + names: ['old-index-name'], + privileges: ['old-privilege'], + }, + ], + run_as: ['old-run-as'], + applications: [ + { + application, + privileges: ['old-kibana-privilege'], + resources: ['old-resource'], + }, + { + application: 'logstash-foo', + privileges: ['logstash-privilege'], + resources: ['logstash-resource'], + }, + { + application: 'beats-foo', + privileges: ['beats-privilege'], + resources: ['beats-resource'], + }, + ], + }, + }), + async () => {}, + ], + asserts: { + apiArguments: [ + ['shield.getRole', { name: 'foo-role', ignore: [404] }], + [ + 'shield.putRole', + { + name: 'foo-role', + body: { + applications: [ + { + application, + privileges: ['all', 'read'], + resources: [GLOBAL_RESOURCE], + }, + { + application: 'logstash-foo', + privileges: ['logstash-privilege'], + resources: ['logstash-resource'], + }, + { + application: 'beats-foo', + privileges: ['beats-privilege'], + resources: ['beats-resource'], + }, + ], + cluster: ['test-cluster-privilege'], + indices: [ + { + names: ['test-index-name-1', 'test-index-name-2'], + privileges: ['test-index-privilege-1', 'test-index-privilege-2'], + }, + ], + metadata: { foo: 'test-metadata' }, + run_as: ['test-run-as-1', 'test-run-as-2'], + }, + }, + ], + ], + statusCode: 204, + result: undefined, + }, + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.ts new file mode 100644 index 00000000000000..92c940132e6605 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.ts @@ -0,0 +1,63 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { RouteDefinitionParams } from '../../index'; +import { createLicensedRouteHandler } from '../../licensed_route_handler'; +import { wrapError } from '../../../errors'; +import { + ElasticsearchRole, + getPutPayloadSchema, + transformPutPayloadToElasticsearchRole, +} from './model'; + +export function definePutRolesRoutes({ router, authz, clusterClient }: RouteDefinitionParams) { + router.put( + { + path: '/api/security/role/{name}', + validate: { + params: schema.object({ name: schema.string({ minLength: 1, maxLength: 1024 }) }), + body: getPutPayloadSchema(() => { + const privileges = authz.privileges.get(); + return { + global: Object.keys(privileges.global), + space: Object.keys(privileges.space), + }; + }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + const { name } = request.params; + + try { + const rawRoles: Record = await clusterClient + .asScoped(request) + .callAsCurrentUser('shield.getRole', { + name: request.params.name, + ignore: [404], + }); + + const body = transformPutPayloadToElasticsearchRole( + request.body, + authz.getApplicationName(), + rawRoles[name] ? rawRoles[name].applications : [] + ); + + await clusterClient + .asScoped(request) + .callAsCurrentUser('shield.putRole', { name: request.params.name, body }); + + return response.noContent(); + } catch (error) { + const wrappedError = wrapError(error); + return response.customError({ + body: wrappedError, + statusCode: wrappedError.output.statusCode, + }); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/index.mock.ts b/x-pack/plugins/security/server/routes/index.mock.ts new file mode 100644 index 00000000000000..2d3a3154e64990 --- /dev/null +++ b/x-pack/plugins/security/server/routes/index.mock.ts @@ -0,0 +1,27 @@ +/* + * 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 { + elasticsearchServiceMock, + httpServiceMock, + loggingServiceMock, +} from '../../../../../src/core/server/mocks'; +import { authenticationMock } from '../authentication/index.mock'; +import { authorizationMock } from '../authorization/index.mock'; +import { ConfigSchema } from '../config'; + +export const routeDefinitionParamsMock = { + create: () => ({ + router: httpServiceMock.createRouter(), + basePath: httpServiceMock.createBasePath(), + logger: loggingServiceMock.create().get(), + clusterClient: elasticsearchServiceMock.createClusterClient(), + config: { ...ConfigSchema.validate({}), encryptionKey: 'some-enc-key' }, + authc: authenticationMock.create(), + authz: authorizationMock.create(), + getLegacyAPI: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index 289f87d70b1dea..73e276832f4741 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -4,12 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, IRouter, Logger } from '../../../../../src/core/server'; +import { CoreSetup, IClusterClient, IRouter, Logger } from '../../../../../src/core/server'; import { Authentication } from '../authentication'; +import { Authorization } from '../authorization'; import { ConfigType } from '../config'; -import { defineAuthenticationRoutes } from './authentication'; import { LegacyAPI } from '../plugin'; +import { defineAuthenticationRoutes } from './authentication'; +import { defineAuthorizationRoutes } from './authorization'; + /** * Describes parameters used to define HTTP routes. */ @@ -17,11 +20,14 @@ export interface RouteDefinitionParams { router: IRouter; basePath: CoreSetup['http']['basePath']; logger: Logger; + clusterClient: IClusterClient; config: ConfigType; authc: Authentication; - getLegacyAPI: () => LegacyAPI; + authz: Authorization; + getLegacyAPI: () => Pick; } export function defineRoutes(params: RouteDefinitionParams) { defineAuthenticationRoutes(params); + defineAuthorizationRoutes(params); } diff --git a/x-pack/plugins/security/server/routes/licensed_route_handler.ts b/x-pack/plugins/security/server/routes/licensed_route_handler.ts new file mode 100644 index 00000000000000..de5b842c7d292c --- /dev/null +++ b/x-pack/plugins/security/server/routes/licensed_route_handler.ts @@ -0,0 +1,32 @@ +/* + * 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 { RequestHandler } from 'src/core/server'; +import { ObjectType } from '@kbn/config-schema'; +import { LICENSE_STATUS } from '../../../licensing/server/constants'; + +export const createLicensedRouteHandler = < + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType +>( + handler: RequestHandler +) => { + const licensedRouteHandler: RequestHandler = (context, request, responseToolkit) => { + const { license } = context.licensing; + const licenseCheck = license.check('security', 'basic'); + if ( + licenseCheck.check === LICENSE_STATUS.Unavailable || + licenseCheck.check === LICENSE_STATUS.Invalid + ) { + return responseToolkit.forbidden({ body: { message: licenseCheck.message! } }); + } + + return handler(context, request, responseToolkit); + }; + + return licensedRouteHandler; +}; diff --git a/x-pack/plugins/security/server/saved_objects/index.ts b/x-pack/plugins/security/server/saved_objects/index.ts new file mode 100644 index 00000000000000..2bd7440d3ee70d --- /dev/null +++ b/x-pack/plugins/security/server/saved_objects/index.ts @@ -0,0 +1,63 @@ +/* + * 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 { IClusterClient, KibanaRequest, LegacyRequest } from '../../../../../src/core/server'; +import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper'; +import { LegacyAPI } from '../plugin'; +import { Authorization } from '../authorization'; +import { SecurityAuditLogger } from '../audit'; + +interface SetupSavedObjectsParams { + adminClusterClient: IClusterClient; + auditLogger: SecurityAuditLogger; + authz: Pick; + legacyAPI: Pick; +} + +export function setupSavedObjects({ + adminClusterClient, + auditLogger, + authz, + legacyAPI: { savedObjects }, +}: SetupSavedObjectsParams) { + const getKibanaRequest = (request: KibanaRequest | LegacyRequest) => + request instanceof KibanaRequest ? request : KibanaRequest.from(request); + savedObjects.setScopedSavedObjectsClientFactory(({ request }) => { + const kibanaRequest = getKibanaRequest(request); + if (authz.mode.useRbacForRequest(kibanaRequest)) { + const internalRepository = savedObjects.getSavedObjectsRepository( + adminClusterClient.callAsInternalUser + ); + return new savedObjects.SavedObjectsClient(internalRepository); + } + + const callAsCurrentUserRepository = savedObjects.getSavedObjectsRepository( + adminClusterClient.asScoped(kibanaRequest).callAsCurrentUser + ); + return new savedObjects.SavedObjectsClient(callAsCurrentUserRepository); + }); + + savedObjects.addScopedSavedObjectsClientWrapperFactory( + Number.MAX_SAFE_INTEGER - 1, + 'security', + ({ client, request }) => { + const kibanaRequest = getKibanaRequest(request); + if (authz.mode.useRbacForRequest(kibanaRequest)) { + return new SecureSavedObjectsClientWrapper({ + actions: authz.actions, + auditLogger, + baseClient: client, + checkSavedObjectsPrivilegesAsCurrentUser: authz.checkSavedObjectsPrivilegesWithRequest( + kibanaRequest + ), + errors: savedObjects.SavedObjectsClient.errors, + }); + } + + return client; + } + ); +} diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts new file mode 100644 index 00000000000000..f802c011f207e2 --- /dev/null +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -0,0 +1,822 @@ +/* + * 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 { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper'; +import { Actions } from '../authorization'; +import { securityAuditLoggerMock } from '../audit/index.mock'; +import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import { SavedObjectsClientContract } from 'kibana/server'; + +const createSecureSavedObjectsClientWrapperOptions = () => { + const actions = new Actions('some-version'); + jest + .spyOn(actions.savedObject, 'get') + .mockImplementation((type: string, action: string) => `mock-saved_object:${type}/${action}`); + + const forbiddenError = new Error('Mock ForbiddenError'); + const generalError = new Error('Mock GeneralError'); + + const errors = ({ + decorateForbiddenError: jest.fn().mockReturnValue(forbiddenError), + decorateGeneralError: jest.fn().mockReturnValue(generalError), + } as unknown) as jest.Mocked; + + return { + actions, + baseClient: savedObjectsClientMock.create(), + checkSavedObjectsPrivilegesAsCurrentUser: jest.fn(), + errors, + auditLogger: securityAuditLoggerMock.create(), + forbiddenError, + generalError, + }; +}; + +describe('#errors', () => { + test(`assigns errors from constructor to .errors`, () => { + const options = createSecureSavedObjectsClientWrapperOptions(); + const client = new SecureSavedObjectsClientWrapper(options); + + expect(client.errors).toBe(options.errors); + }); +}); + +describe(`spaces disabled`, () => { + describe('#create', () => { + test(`throws decorated GeneralError when checkPrivileges.globally rejects promise`, async () => { + const type = 'foo'; + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( + new Error('An actual error would happen here') + ); + const client = new SecureSavedObjectsClientWrapper(options); + + await expect(client.create(type)).rejects.toThrowError(options.generalError); + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'create')], + undefined + ); + expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const type = 'foo'; + const username = Symbol(); + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ + hasAllRequested: false, + username, + privileges: { [options.actions.savedObject.get(type, 'create')]: false }, + }); + + const client = new SecureSavedObjectsClientWrapper(options); + + const attributes = { some_attr: 's' }; + const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); + await expect(client.create(type, attributes, apiCallOptions)).rejects.toThrowError( + options.forbiddenError + ); + + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'create')], + apiCallOptions.namespace + ); + expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'create', + [type], + [options.actions.savedObject.get(type, 'create')], + { type, attributes, options: apiCallOptions } + ); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.create when authorized`, async () => { + const type = 'foo'; + const username = Symbol(); + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ + hasAllRequested: true, + username, + privileges: { [options.actions.savedObject.get(type, 'create')]: true }, + }); + + const apiCallReturnValue = Symbol(); + options.baseClient.create.mockReturnValue(apiCallReturnValue as any); + + const client = new SecureSavedObjectsClientWrapper(options); + + const attributes = { some_attr: 's' }; + const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); + await expect(client.create(type, attributes, apiCallOptions)).resolves.toBe( + apiCallReturnValue + ); + + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'create')], + apiCallOptions.namespace + ); + expect(options.baseClient.create).toHaveBeenCalledWith(type, attributes, apiCallOptions); + expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( + username, + 'create', + [type], + { type, attributes, options: apiCallOptions } + ); + }); + }); + + describe('#bulkCreate', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( + new Error('An actual error would happen here') + ); + const client = new SecureSavedObjectsClientWrapper(options); + + const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); + await expect( + client.bulkCreate([{ type, attributes: {} }], apiCallOptions) + ).rejects.toThrowError(options.generalError); + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'bulk_create')], + apiCallOptions.namespace + ); + expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const type1 = 'foo'; + const type2 = 'bar'; + const username = Symbol(); + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ + hasAllRequested: false, + username, + privileges: { + [options.actions.savedObject.get(type1, 'bulk_create')]: false, + [options.actions.savedObject.get(type2, 'bulk_create')]: true, + }, + }); + + const client = new SecureSavedObjectsClientWrapper(options); + + const objects = [{ type: type1, attributes: {} }, { type: type2, attributes: {} }]; + const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); + await expect(client.bulkCreate(objects, apiCallOptions)).rejects.toThrowError( + options.forbiddenError + ); + + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [ + options.actions.savedObject.get(type1, 'bulk_create'), + options.actions.savedObject.get(type2, 'bulk_create'), + ], + apiCallOptions.namespace + ); + expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'bulk_create', + [type1, type2], + [options.actions.savedObject.get(type1, 'bulk_create')], + { objects, options: apiCallOptions } + ); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.bulkCreate when authorized`, async () => { + const type1 = 'foo'; + const type2 = 'bar'; + const username = Symbol(); + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ + hasAllRequested: true, + username, + privileges: { + [options.actions.savedObject.get(type1, 'bulk_create')]: true, + [options.actions.savedObject.get(type2, 'bulk_create')]: true, + }, + }); + + const apiCallReturnValue = Symbol(); + options.baseClient.bulkCreate.mockReturnValue(apiCallReturnValue as any); + + const client = new SecureSavedObjectsClientWrapper(options); + + const objects = [ + { type: type1, otherThing: 'sup', attributes: {} }, + { type: type2, otherThing: 'everyone', attributes: {} }, + ]; + const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); + await expect(client.bulkCreate(objects, apiCallOptions)).resolves.toBe(apiCallReturnValue); + + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [ + options.actions.savedObject.get(type1, 'bulk_create'), + options.actions.savedObject.get(type2, 'bulk_create'), + ], + apiCallOptions.namespace + ); + expect(options.baseClient.bulkCreate).toHaveBeenCalledWith(objects, apiCallOptions); + expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( + username, + 'bulk_create', + [type1, type2], + { objects, options: apiCallOptions } + ); + }); + }); + + describe('#delete', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( + new Error('An actual error would happen here') + ); + const client = new SecureSavedObjectsClientWrapper(options); + + await expect(client.delete(type, 'bar')).rejects.toThrowError(options.generalError); + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'delete')], + undefined + ); + expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const type = 'foo'; + const id = 'bar'; + const username = Symbol(); + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ + hasAllRequested: false, + username, + privileges: { + [options.actions.savedObject.get(type, 'delete')]: false, + }, + }); + + const client = new SecureSavedObjectsClientWrapper(options); + + const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); + await expect(client.delete(type, id, apiCallOptions)).rejects.toThrowError( + options.forbiddenError + ); + + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'delete')], + apiCallOptions.namespace + ); + expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'delete', + [type], + [options.actions.savedObject.get(type, 'delete')], + { type, id, options: apiCallOptions } + ); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of internalRepository.delete when authorized`, async () => { + const type = 'foo'; + const id = 'bar'; + const username = Symbol(); + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ + hasAllRequested: true, + username, + privileges: { [options.actions.savedObject.get(type, 'delete')]: true }, + }); + + const apiCallReturnValue = Symbol(); + options.baseClient.delete.mockReturnValue(apiCallReturnValue as any); + + const client = new SecureSavedObjectsClientWrapper(options); + + const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); + await expect(client.delete(type, id, apiCallOptions)).resolves.toBe(apiCallReturnValue); + + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'delete')], + apiCallOptions.namespace + ); + expect(options.baseClient.delete).toHaveBeenCalledWith(type, id, apiCallOptions); + expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( + username, + 'delete', + [type], + { type, id, options: apiCallOptions } + ); + }); + }); + + describe('#find', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( + new Error('An actual error would happen here') + ); + const client = new SecureSavedObjectsClientWrapper(options); + + await expect(client.find({ type })).rejects.toThrowError(options.generalError); + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'find')], + undefined + ); + expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when type's singular and unauthorized`, async () => { + const type = 'foo'; + const username = Symbol(); + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ + hasAllRequested: false, + username, + privileges: { [options.actions.savedObject.get(type, 'find')]: false }, + }); + + const client = new SecureSavedObjectsClientWrapper(options); + + const apiCallOptions = Object.freeze({ type, namespace: 'some-ns' }); + await expect(client.find(apiCallOptions)).rejects.toThrowError(options.forbiddenError); + + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'find')], + apiCallOptions.namespace + ); + expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'find', + [type], + [options.actions.savedObject.get(type, 'find')], + { options: apiCallOptions } + ); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when type's an array and unauthorized`, async () => { + const type1 = 'foo'; + const type2 = 'bar'; + const username = Symbol(); + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ + hasAllRequested: false, + username, + privileges: { + [options.actions.savedObject.get(type1, 'find')]: false, + [options.actions.savedObject.get(type2, 'find')]: true, + }, + }); + + const client = new SecureSavedObjectsClientWrapper(options); + + const apiCallOptions = Object.freeze({ type: [type1, type2], namespace: 'some-ns' }); + await expect(client.find(apiCallOptions)).rejects.toThrowError(options.forbiddenError); + + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [ + options.actions.savedObject.get(type1, 'find'), + options.actions.savedObject.get(type2, 'find'), + ], + apiCallOptions.namespace + ); + expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'find', + [type1, type2], + [options.actions.savedObject.get(type1, 'find')], + { options: apiCallOptions } + ); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.find when authorized`, async () => { + const type = 'foo'; + const username = Symbol(); + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ + hasAllRequested: true, + username, + privileges: { [options.actions.savedObject.get(type, 'find')]: true }, + }); + + const apiCallReturnValue = Symbol(); + options.baseClient.find.mockReturnValue(apiCallReturnValue as any); + + const client = new SecureSavedObjectsClientWrapper(options); + + const apiCallOptions = Object.freeze({ type, namespace: 'some-ns' }); + await expect(client.find(apiCallOptions)).resolves.toBe(apiCallReturnValue); + + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'find')], + apiCallOptions.namespace + ); + expect(options.baseClient.find).toHaveBeenCalledWith(apiCallOptions); + expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( + username, + 'find', + [type], + { options: apiCallOptions } + ); + }); + }); + + describe('#bulkGet', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( + new Error('An actual error would happen here') + ); + const client = new SecureSavedObjectsClientWrapper(options); + + await expect(client.bulkGet([{ id: 'bar', type }])).rejects.toThrowError( + options.generalError + ); + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'bulk_get')], + undefined + ); + expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const type1 = 'foo'; + const type2 = 'bar'; + const username = Symbol(); + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ + hasAllRequested: false, + username, + privileges: { + [options.actions.savedObject.get(type1, 'bulk_get')]: false, + [options.actions.savedObject.get(type2, 'bulk_get')]: true, + }, + }); + + const client = new SecureSavedObjectsClientWrapper(options); + + const objects = [{ type: type1, id: `bar-${type1}` }, { type: type2, id: `bar-${type2}` }]; + const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); + await expect(client.bulkGet(objects, apiCallOptions)).rejects.toThrowError( + options.forbiddenError + ); + + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [ + options.actions.savedObject.get(type1, 'bulk_get'), + options.actions.savedObject.get(type2, 'bulk_get'), + ], + apiCallOptions.namespace + ); + expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'bulk_get', + [type1, type2], + [options.actions.savedObject.get(type1, 'bulk_get')], + { objects, options: apiCallOptions } + ); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.bulkGet when authorized`, async () => { + const type1 = 'foo'; + const type2 = 'bar'; + const username = Symbol(); + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ + hasAllRequested: true, + username, + privileges: { + [options.actions.savedObject.get(type1, 'bulk_get')]: true, + [options.actions.savedObject.get(type2, 'bulk_get')]: true, + }, + }); + + const apiCallReturnValue = Symbol(); + options.baseClient.bulkGet.mockReturnValue(apiCallReturnValue as any); + + const client = new SecureSavedObjectsClientWrapper(options); + + const objects = [{ type: type1, id: `id-${type1}` }, { type: type2, id: `id-${type2}` }]; + const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); + await expect(client.bulkGet(objects, apiCallOptions)).resolves.toBe(apiCallReturnValue); + + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [ + options.actions.savedObject.get(type1, 'bulk_get'), + options.actions.savedObject.get(type2, 'bulk_get'), + ], + apiCallOptions.namespace + ); + expect(options.baseClient.bulkGet).toHaveBeenCalledWith(objects, apiCallOptions); + expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( + username, + 'bulk_get', + [type1, type2], + { objects, options: apiCallOptions } + ); + }); + }); + + describe('#get', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( + new Error('An actual error would happen here') + ); + const client = new SecureSavedObjectsClientWrapper(options); + + await expect(client.get(type, 'bar')).rejects.toThrowError(options.generalError); + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'get')], + undefined + ); + expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const type = 'foo'; + const id = 'bar'; + const username = Symbol(); + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ + hasAllRequested: false, + username, + privileges: { + [options.actions.savedObject.get(type, 'get')]: false, + }, + }); + + const client = new SecureSavedObjectsClientWrapper(options); + + const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); + await expect(client.get(type, id, apiCallOptions)).rejects.toThrowError( + options.forbiddenError + ); + + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'get')], + apiCallOptions.namespace + ); + expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'get', + [type], + [options.actions.savedObject.get(type, 'get')], + { type, id, options: apiCallOptions } + ); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.get when authorized`, async () => { + const type = 'foo'; + const id = 'bar'; + const username = Symbol(); + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ + hasAllRequested: true, + username, + privileges: { [options.actions.savedObject.get(type, 'get')]: true }, + }); + + const apiCallReturnValue = Symbol(); + options.baseClient.get.mockReturnValue(apiCallReturnValue as any); + + const client = new SecureSavedObjectsClientWrapper(options); + + const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); + await expect(client.get(type, id, apiCallOptions)).resolves.toBe(apiCallReturnValue); + + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'get')], + apiCallOptions.namespace + ); + expect(options.baseClient.get).toHaveBeenCalledWith(type, id, apiCallOptions); + expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( + username, + 'get', + [type], + { type, id, options: apiCallOptions } + ); + }); + }); + + describe('#update', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( + new Error('An actual error would happen here') + ); + const client = new SecureSavedObjectsClientWrapper(options); + + await expect(client.update(type, 'bar', {})).rejects.toThrowError(options.generalError); + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'update')], + undefined + ); + expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const type = 'foo'; + const id = 'bar'; + const username = Symbol(); + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ + hasAllRequested: false, + username, + privileges: { + [options.actions.savedObject.get(type, 'update')]: false, + }, + }); + + const client = new SecureSavedObjectsClientWrapper(options); + + const attributes = { some: 'attr' }; + const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); + await expect(client.update(type, id, attributes, apiCallOptions)).rejects.toThrowError( + options.forbiddenError + ); + + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'update')], + apiCallOptions.namespace + ); + expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'update', + [type], + [options.actions.savedObject.get(type, 'update')], + { type, id, attributes, options: apiCallOptions } + ); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.update when authorized`, async () => { + const type = 'foo'; + const id = 'bar'; + const username = Symbol(); + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ + hasAllRequested: true, + username, + privileges: { [options.actions.savedObject.get(type, 'update')]: true }, + }); + + const apiCallReturnValue = Symbol(); + options.baseClient.update.mockReturnValue(apiCallReturnValue as any); + + const client = new SecureSavedObjectsClientWrapper(options); + + const attributes = { some: 'attr' }; + const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); + await expect(client.update(type, id, attributes, apiCallOptions)).resolves.toBe( + apiCallReturnValue + ); + + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'update')], + apiCallOptions.namespace + ); + expect(options.baseClient.update).toHaveBeenCalledWith(type, id, attributes, apiCallOptions); + expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( + username, + 'update', + [type], + { type, id, attributes, options: apiCallOptions } + ); + }); + }); + + describe('#bulkUpdate', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( + new Error('An actual error would happen here') + ); + const client = new SecureSavedObjectsClientWrapper(options); + + await expect(client.bulkUpdate([{ id: 'bar', type, attributes: {} }])).rejects.toThrowError( + options.generalError + ); + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'bulk_update')], + undefined + ); + expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const type = 'foo'; + const username = Symbol(); + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ + hasAllRequested: false, + username, + privileges: { + [options.actions.savedObject.get(type, 'bulk_update')]: false, + }, + }); + + const client = new SecureSavedObjectsClientWrapper(options); + + const objects = [{ type, id: `bar-${type}`, attributes: {} }]; + const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); + await expect(client.bulkUpdate(objects, apiCallOptions)).rejects.toThrowError( + options.forbiddenError + ); + + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'bulk_update')], + apiCallOptions.namespace + ); + expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'bulk_update', + [type], + [options.actions.savedObject.get(type, 'bulk_update')], + { objects, options: apiCallOptions } + ); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.bulkUpdate when authorized`, async () => { + const type = 'foo'; + const username = Symbol(); + const options = createSecureSavedObjectsClientWrapperOptions(); + options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ + hasAllRequested: true, + username, + privileges: { + [options.actions.savedObject.get(type, 'bulk_update')]: true, + }, + }); + + const apiCallReturnValue = Symbol(); + options.baseClient.bulkUpdate.mockReturnValue(apiCallReturnValue as any); + + const client = new SecureSavedObjectsClientWrapper(options); + + const objects = [{ type, id: `id-${type}`, attributes: {} }]; + const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); + await expect(client.bulkUpdate(objects, apiCallOptions)).resolves.toBe(apiCallReturnValue); + + expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [options.actions.savedObject.get(type, 'bulk_update')], + apiCallOptions.namespace + ); + expect(options.baseClient.bulkUpdate).toHaveBeenCalledWith(objects, apiCallOptions); + expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( + username, + 'bulk_update', + [type], + { objects, options: apiCallOptions } + ); + }); + }); +}); diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts new file mode 100644 index 00000000000000..03b1d770fa7706 --- /dev/null +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -0,0 +1,183 @@ +/* + * 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 { + SavedObjectAttributes, + SavedObjectsBaseOptions, + SavedObjectsBulkCreateObject, + SavedObjectsBulkGetObject, + SavedObjectsBulkUpdateObject, + SavedObjectsClientContract, + SavedObjectsCreateOptions, + SavedObjectsFindOptions, + SavedObjectsUpdateOptions, +} from '../../../../../src/core/server'; +import { SecurityAuditLogger } from '../audit'; +import { Actions, CheckSavedObjectsPrivileges } from '../authorization'; + +interface SecureSavedObjectsClientWrapperOptions { + actions: Actions; + auditLogger: SecurityAuditLogger; + baseClient: SavedObjectsClientContract; + errors: SavedObjectsClientContract['errors']; + checkSavedObjectsPrivilegesAsCurrentUser: CheckSavedObjectsPrivileges; +} + +export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContract { + private readonly actions: Actions; + private readonly auditLogger: PublicMethodsOf; + private readonly baseClient: SavedObjectsClientContract; + private readonly checkSavedObjectsPrivilegesAsCurrentUser: CheckSavedObjectsPrivileges; + public readonly errors: SavedObjectsClientContract['errors']; + constructor({ + actions, + auditLogger, + baseClient, + checkSavedObjectsPrivilegesAsCurrentUser, + errors, + }: SecureSavedObjectsClientWrapperOptions) { + this.errors = errors; + this.actions = actions; + this.auditLogger = auditLogger; + this.baseClient = baseClient; + this.checkSavedObjectsPrivilegesAsCurrentUser = checkSavedObjectsPrivilegesAsCurrentUser; + } + + public async create( + type: string, + attributes: T = {} as T, + options: SavedObjectsCreateOptions = {} + ) { + await this.ensureAuthorized(type, 'create', options.namespace, { type, attributes, options }); + + return await this.baseClient.create(type, attributes, options); + } + + public async bulkCreate( + objects: SavedObjectsBulkCreateObject[], + options: SavedObjectsBaseOptions = {} + ) { + await this.ensureAuthorized( + this.getUniqueObjectTypes(objects), + 'bulk_create', + options.namespace, + { objects, options } + ); + + return await this.baseClient.bulkCreate(objects, options); + } + + public async delete(type: string, id: string, options: SavedObjectsBaseOptions = {}) { + await this.ensureAuthorized(type, 'delete', options.namespace, { type, id, options }); + + return await this.baseClient.delete(type, id, options); + } + + public async find(options: SavedObjectsFindOptions) { + await this.ensureAuthorized(options.type, 'find', options.namespace, { options }); + + return this.baseClient.find(options); + } + + public async bulkGet( + objects: SavedObjectsBulkGetObject[] = [], + options: SavedObjectsBaseOptions = {} + ) { + await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_get', options.namespace, { + objects, + options, + }); + + return await this.baseClient.bulkGet(objects, options); + } + + public async get(type: string, id: string, options: SavedObjectsBaseOptions = {}) { + await this.ensureAuthorized(type, 'get', options.namespace, { type, id, options }); + + return await this.baseClient.get(type, id, options); + } + + public async update( + type: string, + id: string, + attributes: Partial, + options: SavedObjectsUpdateOptions = {} + ) { + await this.ensureAuthorized(type, 'update', options.namespace, { + type, + id, + attributes, + options, + }); + + return await this.baseClient.update(type, id, attributes, options); + } + + public async bulkUpdate( + objects: SavedObjectsBulkUpdateObject[] = [], + options: SavedObjectsBaseOptions = {} + ) { + await this.ensureAuthorized( + this.getUniqueObjectTypes(objects), + 'bulk_update', + options && options.namespace, + { objects, options } + ); + + return await this.baseClient.bulkUpdate(objects, options); + } + + private async checkPrivileges(actions: string | string[], namespace?: string) { + try { + return await this.checkSavedObjectsPrivilegesAsCurrentUser(actions, namespace); + } catch (error) { + throw this.errors.decorateGeneralError(error, error.body && error.body.reason); + } + } + + private async ensureAuthorized( + typeOrTypes: string | string[], + action: string, + namespace?: string, + args?: Record + ) { + const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; + const actionsToTypesMap = new Map( + types.map(type => [this.actions.savedObject.get(type, action), type]) + ); + const actions = Array.from(actionsToTypesMap.keys()); + const { hasAllRequested, username, privileges } = await this.checkPrivileges( + actions, + namespace + ); + + if (hasAllRequested) { + this.auditLogger.savedObjectsAuthorizationSuccess(username, action, types, args); + } else { + const missingPrivileges = this.getMissingPrivileges(privileges); + this.auditLogger.savedObjectsAuthorizationFailure( + username, + action, + types, + missingPrivileges, + args + ); + const msg = `Unable to ${action} ${missingPrivileges + .map(privilege => actionsToTypesMap.get(privilege)) + .sort() + .join(',')}`; + throw this.errors.decorateForbiddenError(new Error(msg)); + } + } + + private getMissingPrivileges(privileges: Record) { + return Object.keys(privileges).filter(privilege => !privileges[privilege]); + } + + private getUniqueObjectTypes(objects: Array<{ type: string }>) { + return [...new Set(objects.map(o => o.type))]; + } +} diff --git a/x-pack/plugins/spaces/kibana.json b/x-pack/plugins/spaces/kibana.json index 15d900bf99e14b..ae121e299cc556 100644 --- a/x-pack/plugins/spaces/kibana.json +++ b/x-pack/plugins/spaces/kibana.json @@ -4,6 +4,7 @@ "kibanaVersion": "kibana", "configPath": ["xpack", "spaces"], "requiredPlugins": ["features", "licensing"], + "optionalPlugins": ["security"], "server": true, "ui": false } diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index 1f20fee46ba4c9..2b0cfd3687a24a 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -27,9 +27,8 @@ import { SpacesAuditLogger } from '../audit_logger'; import { convertSavedObjectToSpace } from '../../routes/lib'; import { initSpacesOnPostAuthRequestInterceptor } from './on_post_auth_interceptor'; import { Feature } from '../../../../features/server'; -import { OptionalPlugin } from '../../../../../legacy/server/lib/optional_plugin'; -import { SecurityPlugin } from '../../../../../legacy/plugins/security'; import { spacesConfig } from '../__fixtures__'; +import { securityMock } from '../../../../security/server/mocks'; describe('onPostAuthInterceptor', () => { let root: ReturnType; @@ -170,7 +169,7 @@ describe('onPostAuthInterceptor', () => { const spacesService = await service.setup({ http: (http as unknown) as CoreSetup['http'], elasticsearch: elasticsearchServiceMock.createSetupContract(), - getSecurity: () => ({} as OptionalPlugin), + authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), }); diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts index e62a3a0efa6019..24a994e836e87d 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PluginSetupContract as SecuritySetupContract } from '../../../../security/server'; import { SpacesClient } from './spaces_client'; -import { AuthorizationService } from '../../../../../legacy/plugins/security/server/lib/authorization/service'; -import { actionsFactory } from '../../../../../legacy/plugins/security/server/lib/authorization/actions'; import { ConfigType, ConfigSchema } from '../../config'; import { GetSpacePurpose } from '../../../common/model/types'; +import { securityMock } from '../../../../security/server/mocks'; + const createMockAuditLogger = () => { return { spacesAuthorizationFailure: jest.fn(), @@ -21,45 +22,17 @@ const createMockDebugLogger = () => { return jest.fn(); }; -interface MockedAuthorization extends AuthorizationService { - mode: { - useRbacForRequest: jest.Mock; - }; -} const createMockAuthorization = () => { const mockCheckPrivilegesAtSpace = jest.fn(); const mockCheckPrivilegesAtSpaces = jest.fn(); const mockCheckPrivilegesGlobally = jest.fn(); - // mocking base path - const mockConfig = { get: jest.fn().mockReturnValue('/') }; - const mockAuthorization: MockedAuthorization = { - actions: actionsFactory(mockConfig), - application: '', - checkPrivilegesDynamicallyWithRequest: jest.fn().mockImplementation(() => { - throw new Error( - 'checkPrivilegesDynamicallyWithRequest should not be called from this test suite' - ); - }), - checkSavedObjectsPrivilegesWithRequest: jest.fn().mockImplementation(() => { - throw new Error( - 'checkSavedObjectsPrivilegesWithRequest should not be called from this test suite' - ); - }), - privileges: { - get: jest.fn().mockImplementation(() => { - throw new Error('privileges.get() should not be called from this test suite'); - }), - }, - checkPrivilegesWithRequest: jest.fn(() => ({ - atSpaces: mockCheckPrivilegesAtSpaces, - atSpace: mockCheckPrivilegesAtSpace, - globally: mockCheckPrivilegesGlobally, - })), - mode: { - useRbacForRequest: jest.fn(), - }, - }; + const mockAuthorization = securityMock.createSetup().authz; + mockAuthorization.checkPrivilegesWithRequest.mockImplementation(() => ({ + atSpaces: mockCheckPrivilegesAtSpaces, + atSpace: mockCheckPrivilegesAtSpace, + globally: mockCheckPrivilegesGlobally, + })); return { mockCheckPrivilegesAtSpaces, @@ -251,17 +224,17 @@ describe('#getAll', () => { [ { purpose: undefined, - expectedPrivilege: (mockAuthorization: MockedAuthorization) => + expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) => mockAuthorization.actions.login, }, { purpose: 'any', - expectedPrivilege: (mockAuthorization: MockedAuthorization) => + expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) => mockAuthorization.actions.login, }, { purpose: 'copySavedObjectsIntoSpace', - expectedPrivilege: (mockAuthorization: MockedAuthorization) => + expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) => mockAuthorization.actions.ui.get('savedObjectsManagement', 'copyIntoSpace'), }, ].forEach(scenario => { diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts index 052534879e678a..f964ae7d7ac32d 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts @@ -5,22 +5,19 @@ */ import Boom from 'boom'; import { omit } from 'lodash'; -import { Legacy } from 'kibana'; import { KibanaRequest } from 'src/core/server'; -import { AuthorizationService } from '../../../../../legacy/plugins/security/server/lib/authorization/service'; +import { PluginSetupContract as SecurityPluginSetupContract } from '../../../../security/server'; import { isReservedSpace } from '../../../common/is_reserved_space'; import { Space } from '../../../common/model/space'; import { SpacesAuditLogger } from '../audit_logger'; import { ConfigType } from '../../config'; import { GetSpacePurpose } from '../../../common/model/types'; -type SpacesClientRequestFacade = Legacy.Request | KibanaRequest; - const SUPPORTED_GET_SPACE_PURPOSES: GetSpacePurpose[] = ['any', 'copySavedObjectsIntoSpace']; const PURPOSE_PRIVILEGE_MAP: Record< GetSpacePurpose, - (authorization: AuthorizationService) => string + (authorization: SecurityPluginSetupContract['authz']) => string > = { any: authorization => authorization.actions.login, copySavedObjectsIntoSpace: authorization => @@ -31,11 +28,11 @@ export class SpacesClient { constructor( private readonly auditLogger: SpacesAuditLogger, private readonly debugLogger: (message: string) => void, - private readonly authorization: AuthorizationService | null, + private readonly authorization: SecurityPluginSetupContract['authz'] | null, private readonly callWithRequestSavedObjectRepository: any, private readonly config: ConfigType, private readonly internalSavedObjectRepository: any, - private readonly request: SpacesClientRequestFacade + private readonly request: KibanaRequest ) {} public async canEnumerateSpaces(): Promise { @@ -220,10 +217,7 @@ export class SpacesClient { } private useRbac(): boolean { - // TODO: remove "as any" once Security is updated to NP conventions - return ( - this.authorization != null && this.authorization.mode.useRbacForRequest(this.request as any) - ); + return this.authorization != null && this.authorization.mode.useRbacForRequest(this.request); } private async ensureAuthorizedGlobally(action: string, method: string, forbiddenMessage: string) { diff --git a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts index 4fbc4df03d00e1..b000c767b53e8d 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts @@ -12,9 +12,9 @@ import { SavedObjectsLegacyService } from 'src/core/server'; import { SpacesAuditLogger } from './audit_logger'; import { elasticsearchServiceMock, coreMock } from '../../../../../src/core/server/mocks'; import { spacesServiceMock } from '../spaces_service/spaces_service.mock'; -import { createOptionalPlugin } from '../../../../legacy/server/lib/optional_plugin'; import { LegacyAPI } from '../plugin'; import { spacesConfig } from './__fixtures__'; +import { securityMock } from '../../../security/server/mocks'; const log = { log: jest.fn(), @@ -55,8 +55,7 @@ describe('createSpacesTutorialContextFactory', () => { const spacesService = await service.setup({ http: coreMock.createSetup().http, elasticsearch: elasticsearchServiceMock.createSetupContract(), - getSecurity: () => - createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'), + authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), }); diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index 4b071baaa7e2c5..aabdc5bcb97e85 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -14,10 +14,9 @@ import { Logger, PluginInitializerContext, } from '../../../../src/core/server'; -import { SecurityPlugin } from '../../../legacy/plugins/security'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { PluginSetupContract as SecurityPluginSetup } from '../../security/server'; import { LicensingPluginSetup } from '../../licensing/server'; -import { OptionalPlugin } from '../../../legacy/server/lib/optional_plugin'; import { XPackMainPlugin } from '../../../legacy/plugins/xpack_main/xpack_main'; import { createDefaultSpace } from './lib/create_default_space'; // @ts-ignore @@ -57,14 +56,12 @@ export interface LegacyAPI { kibanaIndex: string; }; xpackMain: XPackMainPlugin; - // TODO: Spaces has a circular dependency with Security right now. - // Security is not yet available when init runs, so this is wrapped in an optional plugin for the time being. - security: OptionalPlugin; } export interface PluginsSetup { features: FeaturesPluginSetup; licensing: LicensingPluginSetup; + security?: SecurityPluginSetup; } export interface SpacesPluginSetup { @@ -116,7 +113,7 @@ export class Plugin { const spacesService = await service.setup({ http: core.http, elasticsearch: core.elasticsearch, - getSecurity: () => this.getLegacyAPI().security, + authorization: plugins.security ? plugins.security.authz : null, getSpacesAuditLogger: this.getSpacesAuditLogger, config$: this.config$, }); @@ -137,6 +134,10 @@ export class Plugin { features: plugins.features, }); + if (plugins.security) { + plugins.security.registerSpacesService(spacesService); + } + return { spacesService, __legacyCompat: { diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts index 5f366871ba81eb..38a973c1203d58 100644 --- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts @@ -105,7 +105,6 @@ export const createLegacyAPI = ({ }, auditLogger: {} as any, capabilities: {} as any, - security: {} as any, tutorial: {} as any, usage: {} as any, xpackMain: {} as any, diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index 54d9654005f893..f25908147bfe5b 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -19,13 +19,13 @@ import { httpServerMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; -import { createOptionalPlugin } from '../../../../../../legacy/server/lib/optional_plugin'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; import { SpacesClient } from '../../../lib/spaces_client'; import { initCopyToSpacesApi } from './copy_to_space'; import { ObjectType } from '@kbn/config-schema'; import { RouteSchemas } from 'src/core/server/http/router/route'; import { spacesConfig } from '../../../lib/__fixtures__'; +import { securityMock } from '../../../../../security/server/mocks'; describe('copy to space', () => { const spacesSavedObjects = createSpaces(); @@ -45,8 +45,7 @@ describe('copy to space', () => { const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], elasticsearch: elasticsearchServiceMock.createSetupContract(), - getSecurity: () => - createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'), + authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts index e341bd3e4bcbbb..86da3023c515ef 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts @@ -20,13 +20,13 @@ import { httpServerMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; -import { createOptionalPlugin } from '../../../../../../legacy/server/lib/optional_plugin'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; import { SpacesClient } from '../../../lib/spaces_client'; import { initDeleteSpacesApi } from './delete'; import { RouteSchemas } from 'src/core/server/http/router/route'; import { ObjectType } from '@kbn/config-schema'; import { spacesConfig } from '../../../lib/__fixtures__'; +import { securityMock } from '../../../../../security/server/mocks'; describe('Spaces Public API', () => { const spacesSavedObjects = createSpaces(); @@ -46,8 +46,7 @@ describe('Spaces Public API', () => { const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], elasticsearch: elasticsearchServiceMock.createSetupContract(), - getSecurity: () => - createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'), + authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts index 69c4f16d4ca803..f9bd4494791f1f 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts @@ -20,10 +20,10 @@ import { httpServerMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; -import { createOptionalPlugin } from '../../../../../../legacy/server/lib/optional_plugin'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; import { SpacesClient } from '../../../lib/spaces_client'; import { spacesConfig } from '../../../lib/__fixtures__'; +import { securityMock } from '../../../../../security/server/mocks'; describe('GET space', () => { const spacesSavedObjects = createSpaces(); @@ -43,8 +43,7 @@ describe('GET space', () => { const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], elasticsearch: elasticsearchServiceMock.createSetupContract(), - getSecurity: () => - createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'), + authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts index fd31b7d084c0ec..02219db88a04c2 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts @@ -19,11 +19,11 @@ import { httpServerMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; -import { createOptionalPlugin } from '../../../../../../legacy/server/lib/optional_plugin'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; import { SpacesClient } from '../../../lib/spaces_client'; import { initGetAllSpacesApi } from './get_all'; import { spacesConfig } from '../../../lib/__fixtures__'; +import { securityMock } from '../../../../../security/server/mocks'; describe('GET /spaces/space', () => { const spacesSavedObjects = createSpaces(); @@ -43,8 +43,7 @@ describe('GET /spaces/space', () => { const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], elasticsearch: elasticsearchServiceMock.createSetupContract(), - getSecurity: () => - createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'), + authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts index f874f968333500..398b2e37191b6a 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts @@ -19,13 +19,13 @@ import { httpServiceMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; -import { createOptionalPlugin } from '../../../../../../legacy/server/lib/optional_plugin'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; import { SpacesClient } from '../../../lib/spaces_client'; import { initPostSpacesApi } from './post'; import { RouteSchemas } from 'src/core/server/http/router/route'; import { ObjectType } from '@kbn/config-schema'; import { spacesConfig } from '../../../lib/__fixtures__'; +import { securityMock } from '../../../../../security/server/mocks'; describe('Spaces Public API', () => { const spacesSavedObjects = createSpaces(); @@ -45,8 +45,7 @@ describe('Spaces Public API', () => { const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], elasticsearch: elasticsearchServiceMock.createSetupContract(), - getSecurity: () => - createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'), + authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts index b06bb41fe8b6b4..5c213b7f73f627 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts @@ -20,13 +20,13 @@ import { httpServerMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; -import { createOptionalPlugin } from '../../../../../../legacy/server/lib/optional_plugin'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; import { SpacesClient } from '../../../lib/spaces_client'; import { initPutSpacesApi } from './put'; import { RouteSchemas } from 'src/core/server/http/router/route'; import { ObjectType } from '@kbn/config-schema'; import { spacesConfig } from '../../../lib/__fixtures__'; +import { securityMock } from '../../../../../security/server/mocks'; describe('PUT /api/spaces/space', () => { const spacesSavedObjects = createSpaces(); @@ -46,8 +46,7 @@ describe('PUT /api/spaces/space', () => { const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], elasticsearch: elasticsearchServiceMock.createSetupContract(), - getSecurity: () => - createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'), + authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), }); diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts index d0910e00586eda..73791201185e81 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts @@ -5,7 +5,7 @@ */ import * as Rx from 'rxjs'; import { SpacesService } from './spaces_service'; -import { coreMock, elasticsearchServiceMock } from 'src/core/server/mocks'; +import { coreMock, elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; import { SpacesAuditLogger } from '../lib/audit_logger'; import { KibanaRequest, @@ -16,8 +16,8 @@ import { import { DEFAULT_SPACE_ID } from '../../common/constants'; import { getSpaceIdFromPath } from '../../common/lib/spaces_url_parser'; import { LegacyAPI } from '../plugin'; -import { createOptionalPlugin } from '../../../../legacy/server/lib/optional_plugin'; import { spacesConfig } from '../lib/__fixtures__'; +import { securityMock } from '../../../security/server/mocks'; const mockLogger = { trace: jest.fn(), @@ -79,7 +79,7 @@ const createService = async (serverBasePath: string = '') => { http: httpSetup, elasticsearch: elasticsearchServiceMock.createSetupContract(), config$: Rx.of(spacesConfig), - getSecurity: () => createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'), + authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => new SpacesAuditLogger({}), }); @@ -183,9 +183,7 @@ describe('SpacesService', () => { describe('#getActiveSpace', () => { it('returns the default space when in the default space', async () => { const spacesServiceSetup = await createService(); - const request = { - url: { path: 'app/kibana' }, - } as KibanaRequest; + const request = httpServerMock.createKibanaRequest({ path: 'app/kibana' }); const activeSpace = await spacesServiceSetup.getActiveSpace(request); expect(activeSpace).toEqual({ @@ -198,9 +196,7 @@ describe('SpacesService', () => { it('returns the space for the current (non-default) space', async () => { const spacesServiceSetup = await createService(); - const request = { - url: { path: '/s/foo/app/kibana' }, - } as KibanaRequest; + const request = httpServerMock.createKibanaRequest({ path: '/s/foo/app/kibana' }); const activeSpace = await spacesServiceSetup.getActiveSpace(request); expect(activeSpace).toEqual({ @@ -212,11 +208,11 @@ describe('SpacesService', () => { it('propagates errors from the repository', async () => { const spacesServiceSetup = await createService(); - const request = { - url: { path: '/s/unknown-space/app/kibana' }, - } as KibanaRequest; + const request = httpServerMock.createKibanaRequest({ path: '/s/unknown-space/app/kibana' }); - expect(spacesServiceSetup.getActiveSpace(request)).rejects.toThrowErrorMatchingInlineSnapshot( + await expect( + spacesServiceSetup.getActiveSpace(request) + ).rejects.toThrowErrorMatchingInlineSnapshot( `"Saved object [space/unknown-space] not found"` ); }); diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts index 83a62f91ade017..b8d0f910a42ea3 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts @@ -7,9 +7,8 @@ import { map, take } from 'rxjs/operators'; import { Observable, Subscription, combineLatest } from 'rxjs'; import { Legacy } from 'kibana'; -import { Logger, KibanaRequest, CoreSetup } from 'src/core/server'; -import { SecurityPlugin } from '../../../../legacy/plugins/security'; -import { OptionalPlugin } from '../../../../legacy/server/lib/optional_plugin'; +import { Logger, KibanaRequest, CoreSetup } from '../../../../../src/core/server'; +import { PluginSetupContract as SecurityPluginSetup } from '../../../security/server'; import { LegacyAPI } from '../plugin'; import { SpacesClient } from '../lib/spaces_client'; import { ConfigType } from '../config'; @@ -39,7 +38,7 @@ export interface SpacesServiceSetup { interface SpacesServiceDeps { http: CoreSetup['http']; elasticsearch: CoreSetup['elasticsearch']; - getSecurity: () => OptionalPlugin; + authorization: SecurityPluginSetup['authz'] | null; config$: Observable; getSpacesAuditLogger(): any; } @@ -52,7 +51,7 @@ export class SpacesService { public async setup({ http, elasticsearch, - getSecurity, + authorization, config$, getSpacesAuditLogger, }: SpacesServiceDeps): Promise { @@ -69,7 +68,7 @@ export class SpacesService { return spaceId; }; - const getScopedClient = async (request: RequestFacade) => { + const getScopedClient = async (request: KibanaRequest) => { return combineLatest(elasticsearch.adminClient$, config$) .pipe( map(([clusterClient, config]) => { @@ -85,10 +84,6 @@ export class SpacesService { ['space'] ); - const security = getSecurity(); - - const authorization = security.isEnabled ? security.authorization : null; - return new SpacesClient( getSpacesAuditLogger(), (message: string) => { @@ -124,7 +119,9 @@ export class SpacesService { scopedClient: getScopedClient, getActiveSpace: async (request: RequestFacade) => { const spaceId = getSpaceId(request); - const spacesClient = await getScopedClient(request); + const spacesClient = await getScopedClient( + request instanceof KibanaRequest ? request : KibanaRequest.from(request) + ); return spacesClient.get(spaceId); }, }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 057ae6e2ce9946..28db2197638ff2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -581,8 +581,6 @@ "core.ui.overlays.banner.closeButtonLabel": "閉じる", "core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel": "ホームページに移動", "core.ui.chrome.headerGlobalNav.helpMenuButtonAriaLabel": "ヘルプメニュー", - "core.ui.chrome.headerGlobalNav.helpMenuGoToDocumentation": "ドキュメントに移動", - "core.ui.chrome.headerGlobalNav.helpMenuHelpDescription": "不明な点、アップデートなどの情報はドキュメントをご覧ください。", "core.ui.chrome.headerGlobalNav.helpMenuTitle": "ヘルプ", "core.ui.chrome.headerGlobalNav.helpMenuVersion": "v {version}", "core.ui.chrome.sideGlobalNav.viewRecentItemsFlyoutTitle": "最近のアイテム", @@ -6191,10 +6189,8 @@ "xpack.ml.jobsList.multiJobsActions.startDatafeedsLabel": "{jobsCount, plural, one {データフィード} other {データフィード}}を開始", "xpack.ml.jobsList.multiJobsActions.stopDatafeedsLabel": "{jobsCount, plural, one {データフィード} other {データフィード}}を停止", "xpack.ml.jobsList.nodeAvailableWarning.linkToCloud.hereLinkText": "こちら", - "xpack.ml.jobsList.nodeAvailableWarning.linkToCloudDescription": "これはクラウド {hereCloudLink} で構成できます。", "xpack.ml.jobsList.nodeAvailableWarning.noMLNodesAvailableDescription": "利用可能な ML ノードがありません。", "xpack.ml.jobsList.nodeAvailableWarning.noMLNodesAvailableTitle": "利用可能な ML ノードがありません", - "xpack.ml.jobsList.nodeAvailableWarning.unavailableCreateOrRunJobsDescription": "ジョブの作成または実行はできません. {cloudConfigLink}", "xpack.ml.jobsList.noJobsFoundLabel": "ジョブが見つかりません", "xpack.ml.jobsList.processedRecordsLabel": "処理済みレコード", "xpack.ml.jobsList.refreshButtonLabel": "更新", @@ -10310,4 +10306,4 @@ "xpack.fileUpload.fileParser.errorReadingFile": "ファイルの読み込み中にエラーが発生しました", "xpack.fileUpload.fileParser.noFileProvided": "エラー、ファイルが提供されていません" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8a0e3d78139b18..6cf125aee8f963 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -582,8 +582,6 @@ "core.ui.overlays.banner.closeButtonLabel": "关闭", "core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel": "前往主页", "core.ui.chrome.headerGlobalNav.helpMenuButtonAriaLabel": "帮助菜单", - "core.ui.chrome.headerGlobalNav.helpMenuGoToDocumentation": "前往文档", - "core.ui.chrome.headerGlobalNav.helpMenuHelpDescription": "在我们的文档中获取更新、信息以及答案。", "core.ui.chrome.headerGlobalNav.helpMenuTitle": "帮助", "core.ui.chrome.headerGlobalNav.helpMenuVersion": "v {version}", "core.ui.chrome.sideGlobalNav.viewRecentItemsFlyoutTitle": "最近项", @@ -6192,10 +6190,8 @@ "xpack.ml.jobsList.multiJobsActions.startDatafeedsLabel": "开始 {jobsCount, plural, one { 个数据馈送} other { 个数据馈送}}", "xpack.ml.jobsList.multiJobsActions.stopDatafeedsLabel": "停止 {jobsCount, plural, one { 个数据馈送} other { 个数据馈送}}", "xpack.ml.jobsList.nodeAvailableWarning.linkToCloud.hereLinkText": "此处", - "xpack.ml.jobsList.nodeAvailableWarning.linkToCloudDescription": "这可以在云 {hereCloudLink} 中进行配置。", "xpack.ml.jobsList.nodeAvailableWarning.noMLNodesAvailableDescription": "没有可用的 ML 节点。", "xpack.ml.jobsList.nodeAvailableWarning.noMLNodesAvailableTitle": "没有可用的 ML 节点", - "xpack.ml.jobsList.nodeAvailableWarning.unavailableCreateOrRunJobsDescription": "您将无法创建或运行作业。{cloudConfigLink}", "xpack.ml.jobsList.noJobsFoundLabel": "找不到作业", "xpack.ml.jobsList.processedRecordsLabel": "已处理记录", "xpack.ml.jobsList.refreshButtonLabel": "刷新", @@ -10465,4 +10461,4 @@ "xpack.fileUpload.fileParser.errorReadingFile": "读取文件时出错", "xpack.fileUpload.fileParser.noFileProvided": "错误,未提供任何文件" } -} +} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/infra/log_entry_highlights.ts b/x-pack/test/api_integration/apis/infra/log_entry_highlights.ts index 53ece86b924835..88d7d8716d0f54 100644 --- a/x-pack/test/api_integration/apis/infra/log_entry_highlights.ts +++ b/x-pack/test/api_integration/apis/infra/log_entry_highlights.ts @@ -88,7 +88,8 @@ export default function({ getService }: FtrProviderContext) { } }); - it('should return log highlights in a field column', async () => { + // https://github.com/elastic/kibana/issues/49959 + it.skip('should return log highlights in a field column', async () => { const { data: { source: { logEntryHighlights }, diff --git a/x-pack/test/api_integration/apis/lens/telemetry.ts b/x-pack/test/api_integration/apis/lens/telemetry.ts index 5bba8848fe1d9c..6428ef9f478d4a 100644 --- a/x-pack/test/api_integration/apis/lens/telemetry.ts +++ b/x-pack/test/api_integration/apis/lens/telemetry.ts @@ -95,9 +95,10 @@ export default ({ getService }: FtrProviderContext) => { const olderDate = moment() .subtract(100, 'days') .valueOf(); + + // @ts-ignore optional type: string await es.index({ index: '.kibana', - type: '_doc', body: { type: 'lens-ui-telemetry', 'lens-ui-telemetry': { diff --git a/x-pack/test/api_integration/apis/management/index_management/settings.js b/x-pack/test/api_integration/apis/management/index_management/settings.js index 7d9e1732f19d65..bed71c0a621669 100644 --- a/x-pack/test/api_integration/apis/management/index_management/settings.js +++ b/x-pack/test/api_integration/apis/management/index_management/settings.js @@ -57,7 +57,6 @@ export default function ({ getService }) { 'priority', 'codec', 'max_rescore_window', - 'max_adjacency_matrix_filters', 'analyze', 'gc_deletes', 'max_ngram_diff', diff --git a/x-pack/test/api_integration/apis/security/builtin_es_privileges.ts b/x-pack/test/api_integration/apis/security/builtin_es_privileges.ts index cf22394a086163..efce016a16209c 100644 --- a/x-pack/test/api_integration/apis/security/builtin_es_privileges.ts +++ b/x-pack/test/api_integration/apis/security/builtin_es_privileges.ts @@ -11,10 +11,10 @@ export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); describe('Builtin ES Privileges', () => { - describe('GET /api/security/v1/esPrivileges/builtin', () => { + describe('GET /internal/security/esPrivileges/builtin', () => { it('should return a list of available builtin privileges', async () => { await supertest - .get('/api/security/v1/esPrivileges/builtin') + .get('/internal/security/esPrivileges/builtin') .set('kbn-xsrf', 'xxx') .send() .expect(200) diff --git a/x-pack/test/api_integration/apis/siem/timeline_details.ts b/x-pack/test/api_integration/apis/siem/timeline_details.ts index b702adfc1d35eb..5d1e645bcf80be 100644 --- a/x-pack/test/api_integration/apis/siem/timeline_details.ts +++ b/x-pack/test/api_integration/apis/siem/timeline_details.ts @@ -289,7 +289,6 @@ const EXPECTED_DATA: DetailItem[] = [ values: ['filebeat-7.0.0-iot-2019.06'], originalValue: 'filebeat-7.0.0-iot-2019.06', }, - { field: '_type', values: ['_doc'], originalValue: '_doc' }, { field: '_id', values: ['QRhG1WgBqd-n62SwZYDT'], diff --git a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js index 90d4da02e3ed7b..4aa8e1767818c4 100644 --- a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js +++ b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js @@ -31,7 +31,7 @@ export default function ({ getPageObjects, getService }) { await PageObjects.maps.loadSavedMap('document example'); const response = await getResponse(); const firstHit = response.hits.hits[0]; - expect(Object.keys(firstHit).join(',')).to.equal('_index,_type,_id,_score,fields'); + expect(Object.keys(firstHit).join(',')).to.equal('_index,_id,_score,fields'); expect(Object.keys(firstHit.fields).join(',')).to.equal('geo.coordinates'); }); @@ -39,7 +39,7 @@ export default function ({ getPageObjects, getService }) { await PageObjects.maps.loadSavedMap('document example with data driven styles'); const response = await getResponse(); const firstHit = response.hits.hits[0]; - expect(Object.keys(firstHit).join(',')).to.equal('_index,_type,_id,_score,fields'); + expect(Object.keys(firstHit).join(',')).to.equal('_index,_id,_score,fields'); expect(Object.keys(firstHit.fields).join(',')).to.equal('geo.coordinates,bytes,hour_of_day'); }); @@ -47,7 +47,7 @@ export default function ({ getPageObjects, getService }) { await PageObjects.maps.loadSavedMap('document example with data driven styles on date field'); const response = await getResponse(); const firstHit = response.hits.hits[0]; - expect(Object.keys(firstHit).join(',')).to.equal('_index,_type,_id,_score,fields'); + expect(Object.keys(firstHit).join(',')).to.equal('_index,_id,_score,fields'); expect(Object.keys(firstHit.fields).join(',')).to.equal('geo.coordinates,bytes,@timestamp'); expect(firstHit.fields['@timestamp']).to.be.an('array'); expect(firstHit.fields['@timestamp'][0]).to.equal('1442709321445'); diff --git a/x-pack/test/functional/apps/security/doc_level_security_roles.js b/x-pack/test/functional/apps/security/doc_level_security_roles.js index 0766a1e2b15f1d..9dac150b9a0e6b 100644 --- a/x-pack/test/functional/apps/security/doc_level_security_roles.js +++ b/x-pack/test/functional/apps/security/doc_level_security_roles.js @@ -74,7 +74,7 @@ export default function ({ getService, getPageObjects }) { expect(hitCount).to.be('1'); }); const rowData = await PageObjects.discover.getDocTableIndex(1); - expect(rowData).to.be('name:ABC Company region:EAST _id:doc1 _type:_doc _index:dlstest _score:0'); + expect(rowData).to.be('name:ABC Company region:EAST _id:doc1 _type: - _index:dlstest _score:0'); }); after('logout', async () => { await PageObjects.security.logout(); diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.js index 640289fd119b6f..2939805d708598 100644 --- a/x-pack/test/functional/apps/security/field_level_security.js +++ b/x-pack/test/functional/apps/security/field_level_security.js @@ -98,7 +98,7 @@ export default function ({ getService, getPageObjects }) { }); const rowData = await PageObjects.discover.getDocTableIndex(1); expect(rowData).to - .be('customer_ssn:444.555.6666 customer_name:ABC Company customer_region:WEST _id:2 _type:_doc _index:flstest _score:0'); + .be('customer_ssn:444.555.6666 customer_name:ABC Company customer_region:WEST _id:2 _type: - _index:flstest _score:0'); }); it('user customer2 should not see ssn', async function () { @@ -110,7 +110,7 @@ export default function ({ getService, getPageObjects }) { expect(hitCount).to.be('2'); }); const rowData = await PageObjects.discover.getDocTableIndex(1); - expect(rowData).to.be('customer_name:ABC Company customer_region:WEST _id:2 _type:_doc _index:flstest _score:0'); + expect(rowData).to.be('customer_name:ABC Company customer_region:WEST _id:2 _type: - _index:flstest _score:0'); }); after(async function () { diff --git a/x-pack/test/functional/services/machine_learning/navigation.ts b/x-pack/test/functional/services/machine_learning/navigation.ts index a55eae91224859..06ab99b3dcb9f2 100644 --- a/x-pack/test/functional/services/machine_learning/navigation.ts +++ b/x-pack/test/functional/services/machine_learning/navigation.ts @@ -32,12 +32,10 @@ export function MachineLearningNavigationProvider({ }, async navigateToArea(linkSubject: string, pageSubject: string) { - await retry.tryForTime(2 * 60 * 1000, async () => { - if ((await testSubjects.exists(`${linkSubject} selected`)) === false) { - await testSubjects.click(linkSubject); - await testSubjects.existOrFail(`${linkSubject} selected`, { timeout: 30 * 1000 }); - await testSubjects.existOrFail(pageSubject, { timeout: 30 * 1000 }); - } + await testSubjects.click(linkSubject); + await retry.tryForTime(60 * 1000, async () => { + await testSubjects.existOrFail(`${linkSubject} & ~selected`); + await testSubjects.existOrFail(pageSubject); }); }, @@ -51,11 +49,11 @@ export function MachineLearningNavigationProvider({ }, async navigateToOverview() { - await this.navigateToArea('mlMainTab overview', 'mlPageOverview'); + await this.navigateToArea('~mlMainTab & ~overview', 'mlPageOverview'); }, async navigateToAnomalyDetection() { - await this.navigateToArea('mlMainTab anomalyDetection', 'mlPageJobManagement'); + await this.navigateToArea('~mlMainTab & ~anomalyDetection', 'mlPageJobManagement'); await this.assertTabsExist('mlSubTab', [ 'jobManagement', 'anomalyExplorer', @@ -65,33 +63,33 @@ export function MachineLearningNavigationProvider({ }, async navigateToDataFrameAnalytics() { - await this.navigateToArea('mlMainTab dataFrameAnalytics', 'mlPageDataFrameAnalytics'); + await this.navigateToArea('~mlMainTab & ~dataFrameAnalytics', 'mlPageDataFrameAnalytics'); await this.assertTabsExist('mlSubTab', []); }, async navigateToDataVisualizer() { - await this.navigateToArea('mlMainTab dataVisualizer', 'mlPageDataVisualizerSelector'); + await this.navigateToArea('~mlMainTab & ~dataVisualizer', 'mlPageDataVisualizerSelector'); await this.assertTabsExist('mlSubTab', []); }, async navigateToJobManagement() { await this.navigateToAnomalyDetection(); - await this.navigateToArea('mlSubTab jobManagement', 'mlPageJobManagement'); + await this.navigateToArea('~mlSubTab & ~jobManagement', 'mlPageJobManagement'); }, async navigateToAnomalyExplorer() { await this.navigateToAnomalyDetection(); - await this.navigateToArea('mlSubTab anomalyExplorer', 'mlPageAnomalyExplorer'); + await this.navigateToArea('~mlSubTab & ~anomalyExplorer', 'mlPageAnomalyExplorer'); }, async navigateToSingleMetricViewer() { await this.navigateToAnomalyDetection(); - await this.navigateToArea('mlSubTab singleMetricViewer', 'mlPageSingleMetricViewer'); + await this.navigateToArea('~mlSubTab & ~singleMetricViewer', 'mlPageSingleMetricViewer'); }, async navigateToSettings() { await this.navigateToAnomalyDetection(); - await this.navigateToArea('mlSubTab settings', 'mlPageSettings'); + await this.navigateToArea('~mlSubTab & ~settings', 'mlPageSettings'); }, }; } diff --git a/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/encrypted_saved_objects_api.ts b/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/encrypted_saved_objects_api.ts index 7be9816f40de3f..1c7be6def614d1 100644 --- a/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/encrypted_saved_objects_api.ts +++ b/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/encrypted_saved_objects_api.ts @@ -21,7 +21,6 @@ export default function({ getService }: FtrProviderContext) { _source: { [SAVED_OBJECT_WITH_SECRET_TYPE]: savedObject }, } = await es.get({ id: generateRawID(id), - type: '_doc', index: '.kibana', }); diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts index e2ec45151418d8..d719543fa3807a 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts @@ -119,7 +119,6 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: // query ES directory to ensure namespace was or wasn't specified const { _source } = await es.get({ id: `${expectedSpacePrefix}${savedObject.type}:${savedObject.id}`, - type: '_doc', index: '.kibana', }); diff --git a/x-pack/test/saved_object_api_integration/common/suites/create.ts b/x-pack/test/saved_object_api_integration/common/suites/create.ts index a6af4b7ad24693..3ee0548b707bc4 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/create.ts @@ -82,7 +82,6 @@ export function createTestSuiteFactory(es: any, esArchiver: any, supertest: Supe // query ES directory to ensure namespace was or wasn't specified const { _source } = await es.get({ id: `${expectedSpacePrefix}${spaceAwareType}:${resp.body.id}`, - type: '_doc', index: '.kibana', }); @@ -121,7 +120,6 @@ export function createTestSuiteFactory(es: any, esArchiver: any, supertest: Supe // query ES directory to ensure namespace wasn't specified const { _source } = await es.get({ id: `${notSpaceAwareType}:${resp.body.id}`, - type: '_doc', index: '.kibana', }); diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts index 3ea00890aedeb5..6b15d1bff42097 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts @@ -66,9 +66,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { expect(uiCapabilities.value!.catalogue).to.eql(expected); break; } - // if we don't have access at the space itself, we're - // redirected to the space selector and the ui capabilities - // are lagely irrelevant because they won't be consumed + // if we don't have access at the space itself, security interceptor responds with 404. case 'no_kibana_privileges at everything_space': case 'no_kibana_privileges at nothing_space': case 'legacy_all at everything_space': @@ -78,9 +76,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { case 'nothing_space_all at everything_space': case 'nothing_space_read at everything_space': expect(uiCapabilities.success).to.be(false); - expect(uiCapabilities.failureReason).to.be( - GetUICapabilitiesFailureReason.RedirectedToSpaceSelector - ); + expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound); break; default: throw new UnreachableError(scenario); diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts index ef3162fe9ddd9e..ad4c3582d468fe 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts @@ -70,9 +70,7 @@ export default function fooTests({ getService }: FtrProviderContext) { show: false, }); break; - // if we don't have access at the space itself, we're - // redirected to the space selector and the ui capabilities - // are largely irrelevant because they won't be consumed + // if we don't have access at the space itself, security interceptor responds with 404. case 'no_kibana_privileges at everything_space': case 'no_kibana_privileges at nothing_space': case 'legacy_all at everything_space': @@ -82,9 +80,7 @@ export default function fooTests({ getService }: FtrProviderContext) { case 'nothing_space_all at everything_space': case 'nothing_space_read at everything_space': expect(uiCapabilities.success).to.be(false); - expect(uiCapabilities.failureReason).to.be( - GetUICapabilitiesFailureReason.RedirectedToSpaceSelector - ); + expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound); break; default: throw new UnreachableError(scenario); diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts index 1b9c1daf902828..e9d0cf28e96eca 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts @@ -62,6 +62,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) { expect(uiCapabilities.value).to.have.property('navLinks'); expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.only('management')); break; + // if we don't have access at the space itself, security interceptor responds with 404. case 'no_kibana_privileges at everything_space': case 'no_kibana_privileges at nothing_space': case 'legacy_all at everything_space': @@ -71,9 +72,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) { case 'nothing_space_all at everything_space': case 'nothing_space_read at everything_space': expect(uiCapabilities.success).to.be(false); - expect(uiCapabilities.failureReason).to.be( - GetUICapabilitiesFailureReason.RedirectedToSpaceSelector - ); + expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound); break; default: throw new UnreachableError(scenario); diff --git a/yarn.lock b/yarn.lock index 6526c90663b753..d1f0ee07fd05d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1064,10 +1064,10 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@elastic/charts@^13.5.12": - version "13.5.12" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-13.5.12.tgz#95bd92389ec5fb411debfa5979091b6da2e4b123" - integrity sha512-MMNuebZ5jmzXkUJZr/mSvmtWNIR0gWGBtbqpZBfq3T9WRQPvnEHeE/N1WmXw2BSvwN86fy1i0gr52izh/nfzjQ== +"@elastic/charts@^14.0.0": + version "14.0.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-14.0.0.tgz#410c87e9ae53df5848aae09a210fa7d08510b376" + integrity sha512-cskrD5Yq6yTTqGOKV2/7dw/eRON1GbWkIgSuWXPIBa/TQMUwiWqxFkxSMUJSbu9xXq07KMblDgXLf73yMc0AyQ== dependencies: "@types/d3-shape" "^1.3.1" classnames "^2.2.6"