From 0bfa7ca5c6412d277d165abb69fb8f211ff9329e Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 2 Oct 2019 12:05:02 -0400 Subject: [PATCH] Support space-specific default routes (#44678) --- config/kibana.yml | 4 - docs/setup/settings.asciidoc | 4 - .../advanced_settings.test.js.snap | 30 ++ .../field/__snapshots__/field.test.js.snap | 419 ++++++++++++++++++ .../settings/components/field/field.js | 15 +- .../settings/components/field/field.test.js | 30 ++ .../lib/__tests__/to_editable_config.test.js | 18 +- .../settings/lib/to_editable_config.js | 4 + .../kibana/ui_setting_defaults.js | 18 + src/legacy/server/config/schema.js | 2 +- .../server/config/transform_deprecations.js | 1 + .../config/transform_deprecations.test.js | 48 +- src/legacy/server/http/index.js | 9 +- .../default_route_provider.test.ts | 87 ++++ .../http/setup_default_route_provider.ts | 74 ++++ src/legacy/server/kbn_server.d.ts | 1 + .../privilege_space_table.tsx | 2 +- .../space_selector.tsx | 2 +- .../legacy/plugins/spaces/common/constants.ts | 5 + x-pack/legacy/plugins/spaces/common/index.ts | 2 +- .../spaces_url_parser.test.ts.snap | 0 .../lib/spaces_url_parser.test.ts | 2 +- .../lib/spaces_url_parser.ts | 2 +- x-pack/legacy/plugins/spaces/index.ts | 6 +- .../spaces/public/components/space_avatar.tsx | 4 +- .../legacy/plugins/spaces/public/lib/index.ts | 1 + .../lib}/space_attributes.test.ts | 0 .../lib}/space_attributes.ts | 4 +- .../spaces/public/lib/spaces_manager.ts | 35 +- .../customize_space_avatar.tsx | 2 +- .../spaces/public/views/management/index.tsx | 4 +- .../public/views/management/page_routes.tsx | 12 +- .../public/views/nav_control/nav_control.tsx | 4 +- .../public/views/space_selector/index.tsx | 4 +- .../spaces/server/lib/get_active_space.ts | 2 +- .../on_post_auth_interceptor.test.ts | 13 +- .../on_post_auth_interceptor.ts | 12 +- .../on_request_interceptor.ts | 2 +- .../spaces/server/new_platform/plugin.ts | 8 - .../spaces_service/spaces_service.test.ts | 2 +- .../spaces_service/spaces_service.ts | 2 +- .../api/__fixtures__/create_test_handler.ts | 6 +- .../spaces/server/routes/api/v1/index.ts | 36 -- .../server/routes/api/v1/spaces.test.ts | 93 ---- .../spaces/server/routes/api/v1/spaces.ts | 51 --- .../spaces/server/routes/views/enter_space.ts | 24 + .../spaces/server/routes/views/index.ts | 1 + .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../functional/apps/spaces/enter_space.ts | 60 +++ x-pack/test/functional/apps/spaces/index.ts | 1 + .../es_archives/spaces/enter_space/data.json | 83 ++++ .../spaces/enter_space/mappings.json | 287 ++++++++++++ .../page_objects/space_selector_page.js | 10 +- .../common/suites/select.ts | 125 ------ .../security_and_spaces/apis/index.ts | 1 - .../security_and_spaces/apis/select.ts | 341 -------------- .../spaces_only/apis/index.ts | 1 - .../spaces_only/apis/select.ts | 74 ---- 59 files changed, 1251 insertions(+), 843 deletions(-) create mode 100644 src/legacy/server/http/integration_tests/default_route_provider.test.ts create mode 100644 src/legacy/server/http/setup_default_route_provider.ts rename x-pack/legacy/plugins/spaces/{server => common}/lib/__snapshots__/spaces_url_parser.test.ts.snap (100%) rename x-pack/legacy/plugins/spaces/{server => common}/lib/spaces_url_parser.test.ts (97%) rename x-pack/legacy/plugins/spaces/{server => common}/lib/spaces_url_parser.ts (95%) rename x-pack/legacy/plugins/spaces/{common => public/lib}/space_attributes.test.ts (100%) rename x-pack/legacy/plugins/spaces/{common => public/lib}/space_attributes.ts (94%) delete mode 100644 x-pack/legacy/plugins/spaces/server/routes/api/v1/index.ts delete mode 100644 x-pack/legacy/plugins/spaces/server/routes/api/v1/spaces.test.ts delete mode 100644 x-pack/legacy/plugins/spaces/server/routes/api/v1/spaces.ts create mode 100644 x-pack/legacy/plugins/spaces/server/routes/views/enter_space.ts create mode 100644 x-pack/test/functional/apps/spaces/enter_space.ts create mode 100644 x-pack/test/functional/es_archives/spaces/enter_space/data.json create mode 100644 x-pack/test/functional/es_archives/spaces/enter_space/mappings.json delete mode 100644 x-pack/test/spaces_api_integration/common/suites/select.ts delete mode 100644 x-pack/test/spaces_api_integration/security_and_spaces/apis/select.ts delete mode 100644 x-pack/test/spaces_api_integration/spaces_only/apis/select.ts diff --git a/config/kibana.yml b/config/kibana.yml index 7d49fb37e0320..9525a6423d90a 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -18,10 +18,6 @@ # default to `true` starting in Kibana 7.0. #server.rewriteBasePath: false -# Specifies the default route when opening Kibana. You can use this setting to modify -# the landing page when opening Kibana. -#server.defaultRoute: /app/kibana - # The maximum payload size in bytes for incoming server requests. #server.maxPayloadBytes: 1048576 diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 7f9034c48e232..5b3db22a39ea6 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -256,10 +256,6 @@ deprecation warning at startup. This setting cannot end in a slash (`/`). `server.customResponseHeaders:`:: *Default: `{}`* Header names and values to send on all responses to the client from the Kibana server. -[[server-default]]`server.defaultRoute:`:: *Default: "/app/kibana"* This setting -specifies the default route when opening Kibana. You can use this setting to -modify the landing page when opening Kibana. Supported on {ece}. - `server.host:`:: *Default: "localhost"* This setting specifies the host of the back end server. diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap index a3af9fcc884e5..8c1db0c33e0b0 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap @@ -70,6 +70,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "array", + "validation": undefined, "value": undefined, }, Object { @@ -88,6 +89,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "boolean", + "validation": undefined, "value": undefined, }, ], @@ -108,6 +110,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, Object { @@ -126,6 +129,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "image", + "validation": undefined, "value": undefined, }, Object { @@ -146,6 +150,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "json", + "validation": undefined, "value": undefined, }, Object { @@ -164,6 +169,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "number", + "validation": undefined, "value": undefined, }, Object { @@ -186,6 +192,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "select", + "validation": undefined, "value": undefined, }, Object { @@ -204,6 +211,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, Object { @@ -222,6 +230,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "json", + "validation": undefined, "value": undefined, }, Object { @@ -240,6 +249,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "markdown", + "validation": undefined, "value": undefined, }, Object { @@ -258,6 +268,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "number", + "validation": undefined, "value": undefined, }, Object { @@ -280,6 +291,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "select", + "validation": undefined, "value": undefined, }, Object { @@ -298,6 +310,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, ], @@ -342,6 +355,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "array", + "validation": undefined, "value": undefined, }, Object { @@ -360,6 +374,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "boolean", + "validation": undefined, "value": undefined, }, ], @@ -380,6 +395,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, Object { @@ -398,6 +414,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "image", + "validation": undefined, "value": undefined, }, Object { @@ -418,6 +435,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "json", + "validation": undefined, "value": undefined, }, Object { @@ -436,6 +454,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "number", + "validation": undefined, "value": undefined, }, Object { @@ -458,6 +477,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "select", + "validation": undefined, "value": undefined, }, Object { @@ -476,6 +496,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, Object { @@ -494,6 +515,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "json", + "validation": undefined, "value": undefined, }, Object { @@ -512,6 +534,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "markdown", + "validation": undefined, "value": undefined, }, Object { @@ -530,6 +553,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "number", + "validation": undefined, "value": undefined, }, Object { @@ -552,6 +576,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "select", + "validation": undefined, "value": undefined, }, Object { @@ -570,6 +595,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, ], @@ -689,6 +715,7 @@ exports[`AdvancedSettings should render read-only when saving is disabled 1`] = "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, ], @@ -731,6 +758,7 @@ exports[`AdvancedSettings should render read-only when saving is disabled 1`] = "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, ], @@ -868,6 +896,7 @@ exports[`AdvancedSettings should render specific setting if given setting key 1` "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, ], @@ -910,6 +939,7 @@ exports[`AdvancedSettings should render specific setting if given setting key 1` "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, ], diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap index 1a5039bbb96f8..eb8454f64e7ba 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap @@ -3707,3 +3707,422 @@ exports[`Field for string setting should render user value if there is user valu /> `; + +exports[`Field for stringWithValidation setting should render as read only if saving is disabled 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="string:test-validation:setting-aria" + title={ +

+ String test validation setting + +

+ } + titleSize="xs" + > + + + + + + + +`; + +exports[`Field for stringWithValidation setting should render as read only with help text if overridden 1`] = ` + + + +
+ + + + + + foo-default + , + } + } + /> + + + + + } + fullWidth={false} + gutterSize="l" + idAria="string:test-validation:setting-aria" + title={ +

+ String test validation setting + +

+ } + titleSize="xs" + > + + + + } + isInvalid={false} + label="string:test-validation:setting" + labelType="label" + > + + + + + + +`; + +exports[`Field for stringWithValidation setting should render custom setting icon if it is custom 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="string:test-validation:setting-aria" + title={ +

+ String test validation setting + + } + type="asterisk" + /> +

+ } + titleSize="xs" + > + + + + + + + +`; + +exports[`Field for stringWithValidation setting should render default value if there is no user value set 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="string:test-validation:setting-aria" + title={ +

+ String test validation setting + +

+ } + titleSize="xs" + > + + + + + + + +`; + +exports[`Field for stringWithValidation setting should render user value if there is user value is set 1`] = ` + + + +
+ + + + + + foo-default + , + } + } + /> + + + + + } + fullWidth={false} + gutterSize="l" + idAria="string:test-validation:setting-aria" + title={ +

+ String test validation setting + +

+ } + titleSize="xs" + > + + + + + +     + + + } + isInvalid={false} + label="string:test-validation:setting" + labelType="label" + > + + + + + + +`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.js b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.js index f431a862fb4c8..c0b1188950126 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.js @@ -166,7 +166,7 @@ class FieldUI extends PureComponent { onFieldChange = (e) => { const value = e.target.value; - const { type } = this.props.setting; + const { type, validation } = this.props.setting; const { unsavedValue } = this.state; let newUnsavedValue = undefined; @@ -181,8 +181,21 @@ class FieldUI extends PureComponent { default: newUnsavedValue = value; } + + let isInvalid = false; + let error = undefined; + + if (validation && validation.regex) { + if (!validation.regex.test(newUnsavedValue)) { + error = validation.message; + isInvalid = true; + } + } + this.setState({ unsavedValue: newUnsavedValue, + isInvalid, + error }); } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.test.js b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.test.js index 81cda09eaf0da..0a2886d0d0287 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.test.js @@ -143,6 +143,22 @@ const settings = { isOverridden: false, options: null, }, + stringWithValidation: { + name: 'string:test-validation:setting', + ariaName: 'string test validation setting', + displayName: 'String test validation setting', + description: 'Description for String test validation setting', + type: 'string', + validation: { + regex: new RegExp('/^foo'), + message: 'must start with "foo"' + }, + value: undefined, + defVal: 'foo-default', + isCustom: false, + isOverridden: false, + options: null, + } }; const userValues = { array: ['user', 'value'], @@ -153,6 +169,10 @@ const userValues = { number: 10, select: 'banana', string: 'foo', + stringWithValidation: 'fooUserValue' +}; +const invalidUserValues = { + stringWithValidation: 'invalidUserValue' }; const save = jest.fn(() => Promise.resolve()); const clear = jest.fn(() => Promise.resolve()); @@ -392,6 +412,16 @@ describe('Field', () => { const userValue = userValues[type]; const fieldUserValue = type === 'array' ? userValue.join(', ') : userValue; + if (setting.validation) { + const invalidUserValue = invalidUserValues[type]; + it('should display an error when validation fails', async () => { + component.instance().onFieldChange({ target: { value: invalidUserValue } }); + component.update(); + const errorMessage = component.find('.euiFormErrorText').text(); + expect(errorMessage).toEqual(setting.validation.message); + }); + } + it('should be able to change value and cancel', async () => { component.instance().onFieldChange({ target: { value: fieldUserValue } }); component.update(); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/__tests__/to_editable_config.test.js b/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/__tests__/to_editable_config.test.js index ad1ba30ece4b1..555aab8c2b5ff 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/__tests__/to_editable_config.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/__tests__/to_editable_config.test.js @@ -43,7 +43,7 @@ describe('Settings', function () { def = { value: 'the original', description: 'the one and only', - options: 'all the options' + options: 'all the options', }; }); @@ -76,6 +76,18 @@ describe('Settings', function () { expect(invoke({ def }).type).to.equal('array'); }); }); + + describe('that contains a validation object', function () { + it('constructs a validation regex with message', function () { + def.validation = { + regexString: '^foo', + message: 'must start with "foo"' + }; + const result = invoke({ def }); + expect(result.validation.regex).to.be.a(RegExp); + expect(result.validation.message).to.equal('must start with "foo"'); + }); + }); }); describe('when not given a setting definition object', function () { @@ -94,6 +106,10 @@ describe('Settings', function () { it('sets options to undefined', function () { expect(invoke().options).to.be.undefined; }); + + it('sets validation to undefined', function () { + expect(invoke().validation).to.be.undefined; + }); }); }); }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.js b/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.js index b557c880c496b..4c3b87e512092 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.js @@ -43,6 +43,10 @@ export function toEditableConfig({ def, name, value, isCustom, isOverridden }) { defVal: def.value, type: getValType(def, value), description: def.description, + validation: def.validation ? { + regex: new RegExp(def.validation.regexString), + message: def.validation.message + } : undefined, options: def.options, optionLabels: def.optionLabels, requiresPageReload: !!def.requiresPageReload, diff --git a/src/legacy/core_plugins/kibana/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/ui_setting_defaults.js index 191ae7309f46f..7a15252633956 100644 --- a/src/legacy/core_plugins/kibana/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/ui_setting_defaults.js @@ -55,6 +55,24 @@ export function getUiSettingDefaults() { 'buildNum': { readonly: true }, + 'defaultRoute': { + name: i18n.translate('kbn.advancedSettings.defaultRoute.defaultRouteTitle', { + defaultMessage: 'Default route', + }), + value: '/app/kibana', + validation: { + regexString: '^\/', + message: i18n.translate('kbn.advancedSettings.defaultRoute.defaultRouteValidationMessage', { + defaultMessage: 'The route must start with a slash ("/")', + }), + }, + description: + i18n.translate('kbn.advancedSettings.defaultRoute.defaultRouteText', { + defaultMessage: 'This setting specifies the default route when opening Kibana. ' + + 'You can use this setting to modify the landing page when opening Kibana. ' + + 'The route must start with a slash ("/").', + }), + }, 'query:queryString:options': { name: i18n.translate('kbn.advancedSettings.query.queryStringOptionsTitle', { defaultMessage: 'Query string options', diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index 919653bc941f4..2b91eafd45caa 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -78,7 +78,7 @@ export default () => Joi.object({ server: Joi.object({ uuid: Joi.string().guid().default(), name: Joi.string().default(os.hostname()), - defaultRoute: Joi.string().default('/app/kibana').regex(/^\//, `start with a slash`), + defaultRoute: Joi.string().regex(/^\//, `start with a slash`), customResponseHeaders: Joi.object().unknown(true).default({}), xsrf: Joi.object({ disableProtection: Joi.boolean().default(false), diff --git a/src/legacy/server/config/transform_deprecations.js b/src/legacy/server/config/transform_deprecations.js index 7cac17a88fe64..8be880074f9fd 100644 --- a/src/legacy/server/config/transform_deprecations.js +++ b/src/legacy/server/config/transform_deprecations.js @@ -95,6 +95,7 @@ const cspRules = (settings, log) => { const deprecations = [ //server + rename('server.defaultRoute', 'uiSettings.overrides.defaultRoute'), unused('server.xsrf.token'), unused('uiSettings.enabled'), rename('optimize.lazy', 'optimize.watch'), diff --git a/src/legacy/server/config/transform_deprecations.test.js b/src/legacy/server/config/transform_deprecations.test.js index 38044357f230d..4094443ac0006 100644 --- a/src/legacy/server/config/transform_deprecations.test.js +++ b/src/legacy/server/config/transform_deprecations.test.js @@ -62,6 +62,24 @@ describe('server/config', function () { }); }); + describe('server.defaultRoute', () => { + it('renames to uiSettings.overrides.defaultRoute when specified', () => { + const settings = { + server: { + defaultRoute: '/app/foo', + }, + }; + + expect(transformDeprecations(settings)).toEqual({ + uiSettings: { + overrides: { + defaultRoute: '/app/foo' + } + } + }); + }); + }); + describe('csp.rules', () => { describe('with nonce source', () => { it('logs a warning', () => { @@ -74,20 +92,18 @@ describe('server/config', function () { const log = jest.fn(); transformDeprecations(settings, log); expect(log.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in script-src", - ], - ] - `); + Array [ + Array [ + "csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in script-src", + ], + ] + `); }); it('replaces a nonce', () => { expect( - transformDeprecations( - { csp: { rules: [`script-src 'nonce-{nonce}'`] } }, - jest.fn() - ).csp.rules + transformDeprecations({ csp: { rules: [`script-src 'nonce-{nonce}'`] } }, jest.fn()).csp + .rules ).toEqual([`script-src 'self'`]); expect( transformDeprecations( @@ -158,12 +174,12 @@ describe('server/config', function () { const log = jest.fn(); transformDeprecations({ csp: { rules: [`script-src 'unsafe-eval'`] } }, log); expect(log.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "csp.rules must contain the 'self' source. Automatically adding to script-src.", - ], - ] - `); + Array [ + Array [ + "csp.rules must contain the 'self' source. Automatically adding to script-src.", + ], + ] + `); }); it('adds self', () => { diff --git a/src/legacy/server/http/index.js b/src/legacy/server/http/index.js index 40ac2baa032d6..f8fbc6c4976ff 100644 --- a/src/legacy/server/http/index.js +++ b/src/legacy/server/http/index.js @@ -25,6 +25,7 @@ import Boom from 'boom'; import { setupVersionCheck } from './version_check'; import { registerHapiPlugins } from './register_hapi_plugins'; import { setupBasePathProvider } from './setup_base_path_provider'; +import { setupDefaultRouteProvider } from './setup_default_route_provider'; import { setupXsrf } from './xsrf'; export default async function (kbnServer, server, config) { @@ -33,6 +34,8 @@ export default async function (kbnServer, server, config) { setupBasePathProvider(kbnServer); + setupDefaultRouteProvider(server); + await registerHapiPlugins(server); // provide a simple way to expose static directories @@ -86,10 +89,8 @@ export default async function (kbnServer, server, config) { server.route({ path: '/', method: 'GET', - handler(req, h) { - const basePath = req.getBasePath(); - const defaultRoute = config.get('server.defaultRoute'); - return h.redirect(`${basePath}${defaultRoute}`); + async handler(req, h) { + return h.redirect(await req.getDefaultRoute()); } }); diff --git a/src/legacy/server/http/integration_tests/default_route_provider.test.ts b/src/legacy/server/http/integration_tests/default_route_provider.test.ts new file mode 100644 index 0000000000000..fe8c464965132 --- /dev/null +++ b/src/legacy/server/http/integration_tests/default_route_provider.test.ts @@ -0,0 +1,87 @@ +/* + * 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. + */ +jest.mock('../../../ui/ui_settings/ui_settings_mixin', () => { + return jest.fn(); +}); + +import * as kbnTestServer from '../../../../test_utils/kbn_server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { Root } from '../../../../core/server/root'; + +let mockDefaultRouteSetting: any = ''; + +describe('default route provider', () => { + let root: Root; + beforeAll(async () => { + root = kbnTestServer.createRoot(); + + await root.setup(); + await root.start(); + + const kbnServer = kbnTestServer.getKbnServer(root); + + kbnServer.server.decorate('request', 'getUiSettingsService', function() { + return { + get: (key: string) => { + if (key === 'defaultRoute') { + return Promise.resolve(mockDefaultRouteSetting); + } + throw Error(`unsupported ui setting: ${key}`); + }, + getDefaults: () => { + return Promise.resolve({ + defaultRoute: { + value: '/app/kibana', + }, + }); + }, + }; + }); + }, 30000); + + afterAll(async () => await root.shutdown()); + + it('redirects to the configured default route', async function() { + mockDefaultRouteSetting = '/app/some/default/route'; + + const { status, header } = await kbnTestServer.request.get(root, '/'); + expect(status).toEqual(302); + expect(header).toMatchObject({ + location: '/app/some/default/route', + }); + }); + + const invalidRoutes = [ + 'http://not-your-kibana.com', + '///example.com', + '//example.com', + ' //example.com', + ]; + for (const route of invalidRoutes) { + it(`falls back to /app/kibana when the configured route (${route}) is not a valid relative path`, async function() { + mockDefaultRouteSetting = route; + + const { status, header } = await kbnTestServer.request.get(root, '/'); + expect(status).toEqual(302); + expect(header).toMatchObject({ + location: '/app/kibana', + }); + }); + } +}); diff --git a/src/legacy/server/http/setup_default_route_provider.ts b/src/legacy/server/http/setup_default_route_provider.ts new file mode 100644 index 0000000000000..07ff61015a187 --- /dev/null +++ b/src/legacy/server/http/setup_default_route_provider.ts @@ -0,0 +1,74 @@ +/* + * 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 { Legacy } from 'kibana'; +import { parse } from 'url'; + +export function setupDefaultRouteProvider(server: Legacy.Server) { + server.decorate('request', 'getDefaultRoute', async function() { + // @ts-ignore + const request: Legacy.Request = this; + + const serverBasePath: string = server.config().get('server.basePath'); + + const uiSettings = request.getUiSettingsService(); + + const defaultRoute = await uiSettings.get('defaultRoute'); + const qualifiedDefaultRoute = `${request.getBasePath()}${defaultRoute}`; + + if (isRelativePath(qualifiedDefaultRoute, serverBasePath)) { + return qualifiedDefaultRoute; + } else { + server.log( + ['http', 'warn'], + `Ignoring configured default route of '${defaultRoute}', as it is malformed.` + ); + + const fallbackRoute = (await uiSettings.getDefaults()).defaultRoute.value; + + const qualifiedFallbackRoute = `${request.getBasePath()}${fallbackRoute}`; + return qualifiedFallbackRoute; + } + }); + + function isRelativePath(candidatePath: string, basePath = '') { + // validate that `candidatePath` is not attempting a redirect to somewhere + // outside of this Kibana install + const { protocol, hostname, port, pathname } = parse( + candidatePath, + false /* parseQueryString */, + true /* slashesDenoteHost */ + ); + + // We should explicitly compare `protocol`, `port` and `hostname` to null to make sure these are not + // detected in the URL at all. For example `hostname` can be empty string for Node URL parser, but + // browser (because of various bwc reasons) processes URL differently (e.g. `///abc.com` - for browser + // hostname is `abc.com`, but for Node hostname is an empty string i.e. everything between schema (`//`) + // and the first slash that belongs to path. + if (protocol !== null || hostname !== null || port !== null) { + return false; + } + + if (!String(pathname).startsWith(basePath)) { + return false; + } + + return true; + } +} diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 406697ab65d8f..69bf95e57cab9 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -83,6 +83,7 @@ declare module 'hapi' { interface Request { getSavedObjectsClient(options?: SavedObjectsClientProviderOptions): SavedObjectsClientContract; getBasePath(): string; + getDefaultRoute(): Promise; getUiSettingsService(): any; getCapabilities(): Promise; } diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx index 5a718a34b9005..3c49e5717ba42 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx @@ -14,7 +14,7 @@ import { import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { Component } from 'react'; -import { getSpaceColor } from '../../../../../../../../../spaces/common'; +import { getSpaceColor } from '../../../../../../../../../spaces/public/lib/space_attributes'; import { Space } from '../../../../../../../../../spaces/common/model/space'; import { FeaturesPrivileges, diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx index 9cc9894a0f051..75211498c57b8 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx @@ -14,7 +14,7 @@ import { import { InjectedIntl } from '@kbn/i18n/react'; import React, { Component } from 'react'; import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { getSpaceColor } from '../../../../../../../../../spaces/common/space_attributes'; +import { getSpaceColor } from '../../../../../../../../../spaces/public/lib/space_attributes'; const spaceToOption = (space?: Space, currentSelection?: 'global' | 'spaces') => { if (!space) { diff --git a/x-pack/legacy/plugins/spaces/common/constants.ts b/x-pack/legacy/plugins/spaces/common/constants.ts index 50423517bc918..11882ca2f1b3a 100644 --- a/x-pack/legacy/plugins/spaces/common/constants.ts +++ b/x-pack/legacy/plugins/spaces/common/constants.ts @@ -21,3 +21,8 @@ export const MAX_SPACE_INITIALS = 2; * @type {string} */ export const KIBANA_SPACES_STATS_TYPE = 'spaces'; + +/** + * The path to enter a space. + */ +export const ENTER_SPACE_PATH = '/spaces/enter'; diff --git a/x-pack/legacy/plugins/spaces/common/index.ts b/x-pack/legacy/plugins/spaces/common/index.ts index 0e605562ea3ea..a0842201e0f08 100644 --- a/x-pack/legacy/plugins/spaces/common/index.ts +++ b/x-pack/legacy/plugins/spaces/common/index.ts @@ -7,4 +7,4 @@ export { isReservedSpace } from './is_reserved_space'; export { MAX_SPACE_INITIALS } from './constants'; -export { getSpaceInitials, getSpaceColor } from './space_attributes'; +export { getSpaceIdFromPath, addSpaceIdToPath } from './lib/spaces_url_parser'; diff --git a/x-pack/legacy/plugins/spaces/server/lib/__snapshots__/spaces_url_parser.test.ts.snap b/x-pack/legacy/plugins/spaces/common/lib/__snapshots__/spaces_url_parser.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/server/lib/__snapshots__/spaces_url_parser.test.ts.snap rename to x-pack/legacy/plugins/spaces/common/lib/__snapshots__/spaces_url_parser.test.ts.snap diff --git a/x-pack/legacy/plugins/spaces/server/lib/spaces_url_parser.test.ts b/x-pack/legacy/plugins/spaces/common/lib/spaces_url_parser.test.ts similarity index 97% rename from x-pack/legacy/plugins/spaces/server/lib/spaces_url_parser.test.ts rename to x-pack/legacy/plugins/spaces/common/lib/spaces_url_parser.test.ts index 5878272c84924..b25d79c0a6907 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/spaces_url_parser.test.ts +++ b/x-pack/legacy/plugins/spaces/common/lib/spaces_url_parser.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_SPACE_ID } from '../../common/constants'; +import { DEFAULT_SPACE_ID } from '../constants'; import { addSpaceIdToPath, getSpaceIdFromPath } from './spaces_url_parser'; describe('getSpaceIdFromPath', () => { diff --git a/x-pack/legacy/plugins/spaces/server/lib/spaces_url_parser.ts b/x-pack/legacy/plugins/spaces/common/lib/spaces_url_parser.ts similarity index 95% rename from x-pack/legacy/plugins/spaces/server/lib/spaces_url_parser.ts rename to x-pack/legacy/plugins/spaces/common/lib/spaces_url_parser.ts index 14113cbf9d807..994ec7c59cb6e 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/spaces_url_parser.ts +++ b/x-pack/legacy/plugins/spaces/common/lib/spaces_url_parser.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_SPACE_ID } from '../../common/constants'; +import { DEFAULT_SPACE_ID } from '../constants'; export function getSpaceIdFromPath( requestBasePath: string = '/', diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index 6f9397233d1d0..a287aa2fcbb3f 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -14,12 +14,11 @@ import { AuditLogger } from '../../server/lib/audit_logger'; import mappings from './mappings.json'; import { wrapError } from './server/lib/errors'; import { getActiveSpace } from './server/lib/get_active_space'; -import { getSpaceSelectorUrl } from './server/lib/get_space_selector_url'; import { migrateToKibana660 } from './server/lib/migrations'; import { plugin } from './server/new_platform'; import { SecurityPlugin } from '../security'; import { SpacesServiceSetup } from './server/new_platform/spaces_service/spaces_service'; -import { initSpaceSelectorView } from './server/routes/views'; +import { initSpaceSelectorView, initEnterSpaceView } from './server/routes/views'; export interface SpacesPlugin { getSpaceId: SpacesServiceSetup['getSpaceId']; @@ -88,7 +87,7 @@ export const spaces = (kibana: Record) => return { spaces: [], activeSpace: null, - spaceSelectorURL: getSpaceSelectorUrl(server.config()), + serverBasePath: server.config().get('server.basePath'), }; }, async replaceInjectedVars( @@ -181,6 +180,7 @@ export const spaces = (kibana: Record) => }, }); + initEnterSpaceView(server); initSpaceSelectorView(server); server.expose('getSpaceId', (request: any) => spacesService.getSpaceId(request)); diff --git a/x-pack/legacy/plugins/spaces/public/components/space_avatar.tsx b/x-pack/legacy/plugins/spaces/public/components/space_avatar.tsx index ee3755b8df5fa..0211fe7e82643 100644 --- a/x-pack/legacy/plugins/spaces/public/components/space_avatar.tsx +++ b/x-pack/legacy/plugins/spaces/public/components/space_avatar.tsx @@ -6,9 +6,9 @@ import { EuiAvatar, isValidHex } from '@elastic/eui'; import React, { SFC } from 'react'; -import { getSpaceColor, getSpaceInitials, MAX_SPACE_INITIALS } from '../../common'; +import { MAX_SPACE_INITIALS } from '../../common'; import { Space } from '../../common/model/space'; -import { getSpaceImageUrl } from '../../common/space_attributes'; +import { getSpaceColor, getSpaceInitials, getSpaceImageUrl } from '../lib/space_attributes'; interface Props { space: Partial; diff --git a/x-pack/legacy/plugins/spaces/public/lib/index.ts b/x-pack/legacy/plugins/spaces/public/lib/index.ts index 538dd77e053f5..56ac7b8ff37f4 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/index.ts +++ b/x-pack/legacy/plugins/spaces/public/lib/index.ts @@ -5,3 +5,4 @@ */ export { SpacesManager } from './spaces_manager'; +export { getSpaceInitials, getSpaceColor, getSpaceImageUrl } from './space_attributes'; diff --git a/x-pack/legacy/plugins/spaces/common/space_attributes.test.ts b/x-pack/legacy/plugins/spaces/public/lib/space_attributes.test.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/common/space_attributes.test.ts rename to x-pack/legacy/plugins/spaces/public/lib/space_attributes.test.ts diff --git a/x-pack/legacy/plugins/spaces/common/space_attributes.ts b/x-pack/legacy/plugins/spaces/public/lib/space_attributes.ts similarity index 94% rename from x-pack/legacy/plugins/spaces/common/space_attributes.ts rename to x-pack/legacy/plugins/spaces/public/lib/space_attributes.ts index f943dcf4af105..dbb1e8fed2d0b 100644 --- a/x-pack/legacy/plugins/spaces/common/space_attributes.ts +++ b/x-pack/legacy/plugins/spaces/public/lib/space_attributes.ts @@ -5,8 +5,8 @@ */ import { VISUALIZATION_COLORS } from '@elastic/eui'; -import { MAX_SPACE_INITIALS } from './constants'; -import { Space } from './model/space'; +import { Space } from '../../common/model/space'; +import { MAX_SPACE_INITIALS } from '../../common'; // code point for lowercase "a" const FALLBACK_CODE_POINT = 97; diff --git a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts b/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts index d39b751e30a8a..e40e247e405fb 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts +++ b/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts @@ -3,21 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; import { EventEmitter } from 'events'; import { kfetch } from 'ui/kfetch'; import { SavedObjectsManagementRecord } from 'ui/management/saved_objects_management'; import { Space } from '../../common/model/space'; import { GetSpacePurpose } from '../../common/model/types'; import { CopySavedObjectsToSpaceResponse } from './copy_saved_objects_to_space/types'; +import { ENTER_SPACE_PATH } from '../../common/constants'; +import { addSpaceIdToPath } from '../../common'; export class SpacesManager extends EventEmitter { - private spaceSelectorURL: string; - - constructor(spaceSelectorURL: string) { + constructor(private readonly serverBasePath: string) { super(); - this.spaceSelectorURL = spaceSelectorURL; } public async getSpaces(purpose?: GetSpacePurpose): Promise { @@ -89,36 +86,14 @@ export class SpacesManager extends EventEmitter { } public async changeSelectedSpace(space: Space) { - await kfetch({ - pathname: `/api/spaces/v1/space/${encodeURIComponent(space.id)}/select`, - method: 'POST', - }) - .then(response => { - if (response.location) { - window.location = response.location; - } else { - this._displayError(); - } - }) - .catch(() => this._displayError()); + window.location.href = addSpaceIdToPath(this.serverBasePath, space.id, ENTER_SPACE_PATH); } public redirectToSpaceSelector() { - window.location.href = this.spaceSelectorURL; + window.location.href = `${this.serverBasePath}/spaces/space_selector`; } public async requestRefresh() { this.emit('request_refresh'); } - - public _displayError() { - toastNotifications.addDanger({ - title: i18n.translate('xpack.spaces.spacesManager.unableToChangeSpaceWarningTitle', { - defaultMessage: 'Unable to change your Space', - }), - text: i18n.translate('xpack.spaces.spacesManager.unableToChangeSpaceWarningDescription', { - defaultMessage: 'please try again later', - }), - }); - } } diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.tsx b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.tsx index 2f179083d7b90..12fa0193b59a4 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.tsx @@ -18,11 +18,11 @@ import { } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { getSpaceColor, getSpaceInitials } from '../../../../lib/space_attributes'; import { encode, imageTypes } from '../../../../../common/lib/dataurl'; import { MAX_SPACE_INITIALS } from '../../../../../common/constants'; import { Space } from '../../../../../common/model/space'; -import { getSpaceColor, getSpaceInitials } from '../../../../../common/space_attributes'; interface Props { space: Partial; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/index.tsx b/x-pack/legacy/plugins/spaces/public/views/management/index.tsx index 46a718bbc6f35..179665ed11111 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/index.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/index.tsx @@ -24,7 +24,7 @@ const MANAGE_SPACES_KEY = 'spaces'; routes.defaults(/\/management/, { resolve: { - spacesManagementSection(activeSpace: any, spaceSelectorURL: string) { + spacesManagementSection(activeSpace: any, serverBasePath: string) { function getKibanaSection() { return management.getSection('kibana'); } @@ -49,7 +49,7 @@ routes.defaults(/\/management/, { // Customize Saved Objects Management const action = new CopyToSpaceSavedObjectsManagementAction( - new SpacesManager(spaceSelectorURL), + new SpacesManager(serverBasePath), activeSpace.space ); // This route resolve function executes any time the management screen is loaded, and we want to ensure diff --git a/x-pack/legacy/plugins/spaces/public/views/management/page_routes.tsx b/x-pack/legacy/plugins/spaces/public/views/management/page_routes.tsx index d38c5c1998b3a..66cdb0d276e94 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/page_routes.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/page_routes.tsx @@ -22,11 +22,11 @@ routes.when('/management/spaces/list', { template, k7Breadcrumbs: getListBreadcrumbs, requireUICapability: 'management.kibana.spaces', - controller($scope: any, spacesNavState: SpacesNavState, spaceSelectorURL: string) { + controller($scope: any, spacesNavState: SpacesNavState, serverBasePath: string) { $scope.$$postDigest(async () => { const domNode = document.getElementById(reactRootNodeId); - const spacesManager = new SpacesManager(spaceSelectorURL); + const spacesManager = new SpacesManager(serverBasePath); render( @@ -49,11 +49,11 @@ routes.when('/management/spaces/create', { template, k7Breadcrumbs: getCreateBreadcrumbs, requireUICapability: 'management.kibana.spaces', - controller($scope: any, spacesNavState: SpacesNavState, spaceSelectorURL: string) { + controller($scope: any, spacesNavState: SpacesNavState, serverBasePath: string) { $scope.$$postDigest(async () => { const domNode = document.getElementById(reactRootNodeId); - const spacesManager = new SpacesManager(spaceSelectorURL); + const spacesManager = new SpacesManager(serverBasePath); render( @@ -85,14 +85,14 @@ routes.when('/management/spaces/edit/:spaceId', { $route: any, chrome: any, spacesNavState: SpacesNavState, - spaceSelectorURL: string + serverBasePath: string ) { $scope.$$postDigest(async () => { const domNode = document.getElementById(reactRootNodeId); const { spaceId } = $route.current.params; - const spacesManager = new SpacesManager(spaceSelectorURL); + const spacesManager = new SpacesManager(serverBasePath); render( diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control.tsx b/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control.tsx index ad2ae08374708..bac95bbf22099 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control.tsx @@ -54,9 +54,9 @@ chromeHeaderNavControlsRegistry.register((chrome: any, activeSpace: any) => ({ return; } - const spaceSelectorURL = chrome.getInjected('spaceSelectorURL'); + const serverBasePath = chrome.getInjected('serverBasePath'); - spacesManager = new SpacesManager(spaceSelectorURL); + spacesManager = new SpacesManager(serverBasePath); ReactDOM.render( diff --git a/x-pack/legacy/plugins/spaces/public/views/space_selector/index.tsx b/x-pack/legacy/plugins/spaces/public/views/space_selector/index.tsx index 935e79e73517e..8c650fa778bdd 100644 --- a/x-pack/legacy/plugins/spaces/public/views/space_selector/index.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/space_selector/index.tsx @@ -21,10 +21,10 @@ import { SpaceSelector } from './space_selector'; const module = uiModules.get('spaces_selector', []); module.controller( 'spacesSelectorController', - ($scope: any, spaces: Space[], spaceSelectorURL: string) => { + ($scope: any, spaces: Space[], serverBasePath: string) => { const domNode = document.getElementById('spaceSelectorRoot'); - const spacesManager = new SpacesManager(spaceSelectorURL); + const spacesManager = new SpacesManager(serverBasePath); render( diff --git a/x-pack/legacy/plugins/spaces/server/lib/get_active_space.ts b/x-pack/legacy/plugins/spaces/server/lib/get_active_space.ts index 907b7b164b69b..a77a945239100 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/get_active_space.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/get_active_space.ts @@ -7,7 +7,7 @@ import { Space } from '../../common/model/space'; import { wrapError } from './errors'; import { SpacesClient } from './spaces_client'; -import { getSpaceIdFromPath } from './spaces_url_parser'; +import { getSpaceIdFromPath } from '../../common'; export async function getActiveSpace( spacesClient: SpacesClient, diff --git a/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index dfd4d586554bb..511af53c13ab4 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -428,7 +428,7 @@ describe('onPostAuthInterceptor', () => { ); }, 30000); - it('allows the request to continue when accessing the root of a non-default space', async () => { + it('redirects to the "enter space" endpoint when accessing the root of a non-default space', async () => { const spaces = [ { id: 'default', @@ -449,9 +449,8 @@ describe('onPostAuthInterceptor', () => { const { response, spacesService } = await request('/s/a-space', spaces); - // OSS handles this redirection for us expect(response.status).toEqual(302); - expect(response.header.location).toEqual(`/s/a-space${defaultRoute}`); + expect(response.header.location).toEqual(`/s/a-space/spaces/enter`); expect(spacesService.scopedClient).toHaveBeenCalledWith( expect.objectContaining({ @@ -463,7 +462,7 @@ describe('onPostAuthInterceptor', () => { }, 30000); describe('with a single available space', () => { - it('it redirects to the defaultRoute within the context of the single Space when navigating to Kibana root', async () => { + it('it redirects to the "enter space" endpoint within the context of the single Space when navigating to Kibana root', async () => { const spaces = [ { id: 'a-space', @@ -477,7 +476,7 @@ describe('onPostAuthInterceptor', () => { const { response, spacesService } = await request('/', spaces); expect(response.status).toEqual(302); - expect(response.header.location).toEqual(`/s/a-space${defaultRoute}`); + expect(response.header.location).toEqual(`/s/a-space/spaces/enter`); expect(spacesService.scopedClient).toHaveBeenCalledWith( expect.objectContaining({ @@ -488,7 +487,7 @@ describe('onPostAuthInterceptor', () => { ); }); - it('it redirects to the defaultRoute within the context of the Default Space when navigating to Kibana root', async () => { + it('it redirects to the "enter space" endpoint within the context of the Default Space when navigating to Kibana root', async () => { // This is very similar to the test above, but this handles the condition where the only available space is the Default Space, // which does not have a URL Context. In this scenario, the end result is the same as the other test, but the final URL the user // is redirected to does not contain a space identifier (e.g., /s/foo) @@ -506,7 +505,7 @@ describe('onPostAuthInterceptor', () => { const { response, spacesService } = await request('/', spaces); expect(response.status).toEqual(302); - expect(response.header.location).toEqual(defaultRoute); + expect(response.header.location).toEqual('/spaces/enter'); expect(spacesService.scopedClient).toHaveBeenCalledWith( expect.objectContaining({ headers: expect.objectContaining({ diff --git a/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts b/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts index 7cd3114ced2fa..e02677d94a8da 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts @@ -6,12 +6,12 @@ import { Logger, CoreSetup } from 'src/core/server'; import { Space } from '../../../common/model/space'; import { wrapError } from '../errors'; -import { addSpaceIdToPath } from '../spaces_url_parser'; import { XPackMainPlugin } from '../../../../xpack_main/xpack_main'; import { SpacesServiceSetup } from '../../new_platform/spaces_service/spaces_service'; import { LegacyAPI } from '../../new_platform/plugin'; import { getSpaceSelectorUrl } from '../get_space_selector_url'; -import { DEFAULT_SPACE_ID } from '../../../common/constants'; +import { DEFAULT_SPACE_ID, ENTER_SPACE_PATH } from '../../../common/constants'; +import { addSpaceIdToPath } from '../../../common'; export interface OnPostAuthInterceptorDeps { getLegacyAPI(): LegacyAPI; @@ -28,7 +28,7 @@ export function initSpacesOnPostAuthRequestInterceptor({ log, http, }: OnPostAuthInterceptorDeps) { - const { serverBasePath, serverDefaultRoute } = getLegacyAPI().legacyConfig; + const { serverBasePath } = getLegacyAPI().legacyConfig; http.registerOnPostAuth(async (request, response, toolkit) => { const path = request.url.pathname!; @@ -38,6 +38,7 @@ export function initSpacesOnPostAuthRequestInterceptor({ // The root of kibana is also the root of the defaut space, // since the default space does not have a URL Identifier (i.e., `/s/foo`). const isRequestingKibanaRoot = path === '/' && spaceId === DEFAULT_SPACE_ID; + const isRequestingSpaceRoot = path === '/' && spaceId !== DEFAULT_SPACE_ID; const isRequestingApplication = path.startsWith('/app'); const spacesClient = await spacesService.scopedClient(request); @@ -54,7 +55,7 @@ export function initSpacesOnPostAuthRequestInterceptor({ // No need for an interstitial screen where there is only one possible outcome. const space = spaces[0]; - const destination = addSpaceIdToPath(serverBasePath, space.id, serverDefaultRoute); + const destination = addSpaceIdToPath(serverBasePath, space.id, ENTER_SPACE_PATH); return response.redirected({ headers: { location: destination } }); } @@ -72,6 +73,9 @@ export function initSpacesOnPostAuthRequestInterceptor({ statusCode: wrappedError.output.statusCode, }); } + } else if (isRequestingSpaceRoot) { + const destination = addSpaceIdToPath(serverBasePath, spaceId, ENTER_SPACE_PATH); + return response.redirected({ headers: { location: destination } }); } // This condition should only happen after selecting a space, or when transitioning from one application to another diff --git a/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts b/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts index 5da9bdbe6543f..114cc9bf86d46 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts @@ -11,9 +11,9 @@ import { } from 'src/core/server'; import { format } from 'url'; import { DEFAULT_SPACE_ID } from '../../../common/constants'; -import { getSpaceIdFromPath } from '../spaces_url_parser'; import { modifyUrl } from '../utils/url'; import { LegacyAPI } from '../../new_platform/plugin'; +import { getSpaceIdFromPath } from '../../../common'; export interface OnRequestInterceptorDeps { getLegacyAPI(): LegacyAPI; diff --git a/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts b/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts index 2bd9edadc52ef..ed11e6da317fa 100644 --- a/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts +++ b/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts @@ -20,7 +20,6 @@ import { checkLicense } from '../lib/check_license'; import { spacesSavedObjectsClientWrapperFactory } from '../lib/saved_objects_client/saved_objects_client_wrapper_factory'; import { SpacesAuditLogger } from '../lib/audit_logger'; import { createSpacesTutorialContextFactory } from '../lib/spaces_tutorial_context_factory'; -import { initInternalApis } from '../routes/api/v1'; import { initExternalSpacesApi } from '../routes/api/external'; import { getSpacesUsageCollector } from '../lib/get_spaces_usage_collector'; import { SpacesService } from './spaces_service'; @@ -178,13 +177,6 @@ export class Plugin { }) ); - initInternalApis({ - legacyRouter: legacyAPI.router, - getLegacyAPI: this.getLegacyAPI, - spacesService, - xpackMain: xpackMainPlugin, - }); - initExternalSpacesApi({ legacyRouter: legacyAPI.router, log: this.log, diff --git a/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.test.ts b/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.test.ts index 3200c90bca2be..817474dc0fb3a 100644 --- a/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.test.ts +++ b/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.test.ts @@ -13,9 +13,9 @@ import { SavedObjectsErrorHelpers, } from 'src/core/server'; import { DEFAULT_SPACE_ID } from '../../../common/constants'; -import { getSpaceIdFromPath } from '../../lib/spaces_url_parser'; import { createOptionalPlugin } from '../../../../../server/lib/optional_plugin'; import { LegacyAPI } from '../plugin'; +import { getSpaceIdFromPath } from '../../../common'; const mockLogger = { trace: jest.fn(), diff --git a/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.ts b/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.ts index 623e6c43b16e8..08ebc2cb31748 100644 --- a/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.ts +++ b/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.ts @@ -12,11 +12,11 @@ import { OptionalPlugin } from '../../../../../server/lib/optional_plugin'; import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { SecurityPlugin } from '../../../../security'; import { SpacesClient } from '../../lib/spaces_client'; -import { getSpaceIdFromPath, addSpaceIdToPath } from '../../lib/spaces_url_parser'; import { SpacesConfigType } from '../config'; import { namespaceToSpaceId, spaceIdToNamespace } from '../../lib/utils/namespace'; import { LegacyAPI } from '../plugin'; import { Space } from '../../../common/model/space'; +import { getSpaceIdFromPath, addSpaceIdToPath } from '../../../common'; type RequestFacade = KibanaRequest | Legacy.Request; diff --git a/x-pack/legacy/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts b/x-pack/legacy/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts index 13667555a9468..405a3dd34e7fc 100644 --- a/x-pack/legacy/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts +++ b/x-pack/legacy/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts @@ -18,7 +18,6 @@ import { createSpaces } from './create_spaces'; import { ExternalRouteDeps } from '../external'; import { SpacesService } from '../../../new_platform/spaces_service'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; -import { InternalRouteDeps } from '../v1'; import { LegacyAPI } from '../../../new_platform/plugin'; interface KibanaServer extends Legacy.Server { @@ -79,9 +78,7 @@ async function readStreamToCompletion(stream: Readable) { return (createPromiseFromStreams([stream, createConcatStream([])]) as unknown) as any[]; } -export function createTestHandler( - initApiFn: (deps: ExternalRouteDeps & InternalRouteDeps) => void -) { +export function createTestHandler(initApiFn: (deps: ExternalRouteDeps) => void) { const teardowns: TeardownFn[] = []; const spaces = createSpaces(); @@ -254,7 +251,6 @@ export function createTestHandler( }); initApiFn({ - getLegacyAPI: () => legacyAPI, routePreCheckLicenseFn: pre, savedObjects: server.savedObjects, spacesService, diff --git a/x-pack/legacy/plugins/spaces/server/routes/api/v1/index.ts b/x-pack/legacy/plugins/spaces/server/routes/api/v1/index.ts deleted file mode 100644 index ddbca3e8e3d71..0000000000000 --- a/x-pack/legacy/plugins/spaces/server/routes/api/v1/index.ts +++ /dev/null @@ -1,36 +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 { Legacy } from 'kibana'; -import { XPackMainPlugin } from '../../../../../xpack_main/xpack_main'; -import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; -import { initInternalSpacesApi } from './spaces'; -import { SpacesServiceSetup } from '../../../new_platform/spaces_service/spaces_service'; -import { LegacyAPI } from '../../../new_platform/plugin'; - -type Omit = Pick>; - -interface RouteDeps { - xpackMain: XPackMainPlugin; - spacesService: SpacesServiceSetup; - getLegacyAPI(): LegacyAPI; - legacyRouter: Legacy.Server['route']; -} - -export interface InternalRouteDeps extends Omit { - routePreCheckLicenseFn: any; -} - -export function initInternalApis({ xpackMain, ...rest }: RouteDeps) { - const routePreCheckLicenseFn = routePreCheckLicense({ xpackMain }); - - const deps: InternalRouteDeps = { - ...rest, - routePreCheckLicenseFn, - }; - - initInternalSpacesApi(deps); -} diff --git a/x-pack/legacy/plugins/spaces/server/routes/api/v1/spaces.test.ts b/x-pack/legacy/plugins/spaces/server/routes/api/v1/spaces.test.ts deleted file mode 100644 index 4d9952f4ab3dc..0000000000000 --- a/x-pack/legacy/plugins/spaces/server/routes/api/v1/spaces.test.ts +++ /dev/null @@ -1,93 +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. - */ - -jest.mock('../../../lib/route_pre_check_license', () => { - return { - routePreCheckLicense: () => (request: any, h: any) => h.continue, - }; -}); - -jest.mock('../../../../../../server/lib/get_client_shield', () => { - return { - getClient: () => { - return { - callWithInternalUser: jest.fn(() => { - return; - }), - }; - }, - }; -}); - -import Boom from 'boom'; -import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__'; -import { initInternalSpacesApi } from './spaces'; - -describe('Spaces API', () => { - let request: RequestRunner; - let teardowns: TeardownFn[]; - - beforeEach(() => { - const setup = createTestHandler(initInternalSpacesApi); - - request = setup.request; - teardowns = setup.teardowns; - }); - - afterEach(async () => { - await Promise.all(teardowns.splice(0).map(fn => fn())); - }); - - test('POST space/{id}/select should respond with the new space location', async () => { - const { response } = await request('POST', '/api/spaces/v1/space/a-space/select'); - - const { statusCode, payload } = response; - - expect(statusCode).toEqual(200); - - const result = JSON.parse(payload); - expect(result.location).toEqual('/s/a-space'); - }); - - test(`returns result of routePreCheckLicense`, async () => { - const { response } = await request('POST', '/api/spaces/v1/space/a-space/select', { - preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'), - expectSpacesClientCall: false, - }); - - const { statusCode, payload } = response; - - expect(statusCode).toEqual(403); - expect(JSON.parse(payload)).toMatchObject({ - message: 'test forbidden message', - }); - }); - - test('POST space/{id}/select should respond with 404 when the space is not found', async () => { - const { response } = await request('POST', '/api/spaces/v1/space/not-a-space/select'); - - const { statusCode } = response; - - expect(statusCode).toEqual(404); - }); - - test('POST space/{id}/select should respond with the new space location when a server.basePath is in use', async () => { - const testConfig = { - 'server.basePath': '/my/base/path', - }; - - const { response } = await request('POST', '/api/spaces/v1/space/a-space/select', { - testConfig, - }); - - const { statusCode, payload } = response; - - expect(statusCode).toEqual(200); - - const result = JSON.parse(payload); - expect(result.location).toEqual('/my/base/path/s/a-space'); - }); -}); diff --git a/x-pack/legacy/plugins/spaces/server/routes/api/v1/spaces.ts b/x-pack/legacy/plugins/spaces/server/routes/api/v1/spaces.ts deleted file mode 100644 index 3d15044d129e9..0000000000000 --- a/x-pack/legacy/plugins/spaces/server/routes/api/v1/spaces.ts +++ /dev/null @@ -1,51 +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 { Space } from '../../../../common/model/space'; -import { wrapError } from '../../../lib/errors'; -import { SpacesClient } from '../../../lib/spaces_client'; -import { addSpaceIdToPath } from '../../../lib/spaces_url_parser'; -import { getSpaceById } from '../../lib'; -import { InternalRouteDeps } from '.'; - -export function initInternalSpacesApi(deps: InternalRouteDeps) { - const { legacyRouter, spacesService, getLegacyAPI, routePreCheckLicenseFn } = deps; - - legacyRouter({ - method: 'POST', - path: '/api/spaces/v1/space/{id}/select', - async handler(request: any) { - const { savedObjects, legacyConfig } = getLegacyAPI(); - - const { SavedObjectsClient } = savedObjects; - const spacesClient: SpacesClient = await spacesService.scopedClient(request); - const id = request.params.id; - - const basePath = legacyConfig.serverBasePath; - const defaultRoute = legacyConfig.serverDefaultRoute; - try { - const existingSpace: Space | null = await getSpaceById( - spacesClient, - id, - SavedObjectsClient.errors - ); - if (!existingSpace) { - return Boom.notFound(); - } - - return { - location: addSpaceIdToPath(basePath, existingSpace.id, defaultRoute), - }; - } catch (error) { - return wrapError(error); - } - }, - options: { - pre: [routePreCheckLicenseFn], - }, - }); -} diff --git a/x-pack/legacy/plugins/spaces/server/routes/views/enter_space.ts b/x-pack/legacy/plugins/spaces/server/routes/views/enter_space.ts new file mode 100644 index 0000000000000..e560d4278b407 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/server/routes/views/enter_space.ts @@ -0,0 +1,24 @@ +/* + * 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 { Legacy } from 'kibana'; +import { ENTER_SPACE_PATH } from '../../../common/constants'; +import { wrapError } from '../../lib/errors'; + +export function initEnterSpaceView(server: Legacy.Server) { + server.route({ + method: 'GET', + path: ENTER_SPACE_PATH, + async handler(request, h) { + try { + return h.redirect(await request.getDefaultRoute()); + } catch (e) { + server.log(['spaces', 'error'], `Error navigating to space: ${e}`); + return wrapError(e); + } + }, + }); +} diff --git a/x-pack/legacy/plugins/spaces/server/routes/views/index.ts b/x-pack/legacy/plugins/spaces/server/routes/views/index.ts index a0f72886940a4..d7637e299652f 100644 --- a/x-pack/legacy/plugins/spaces/server/routes/views/index.ts +++ b/x-pack/legacy/plugins/spaces/server/routes/views/index.ts @@ -5,3 +5,4 @@ */ export { initSpaceSelectorView } from './space_selector'; +export { initEnterSpaceView } from './enter_space'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a821a66076bae..6887dab9425ce 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11074,8 +11074,6 @@ "xpack.spaces.spaceSelector.findSpacePlaceholder": "スペースを検索", "xpack.spaces.spaceSelector.noSpacesMatchSearchCriteriaDescription": "検索条件に一致するスペースがありません", "xpack.spaces.spaceSelector.selectSpacesTitle": "スペースの選択", - "xpack.spaces.spacesManager.unableToChangeSpaceWarningDescription": "後程再試行してください", - "xpack.spaces.spacesManager.unableToChangeSpaceWarningTitle": "スペースを変更できません", "xpack.spaces.spacesTitle": "スペース", "xpack.spaces.management.copyToSpace.actionDescription": "この保存されたオブジェクトを 1 つまたは複数のスペースにコピーします。", "xpack.spaces.management.copyToSpace.actionTitle": "スペースにコピー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 1f856731d43e1..649ead90b6356 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11076,8 +11076,6 @@ "xpack.spaces.spaceSelector.findSpacePlaceholder": "查找工作区", "xpack.spaces.spaceSelector.noSpacesMatchSearchCriteriaDescription": "没有匹配搜索条件的空间", "xpack.spaces.spaceSelector.selectSpacesTitle": "选择您的空间", - "xpack.spaces.spacesManager.unableToChangeSpaceWarningDescription": "请稍后重试", - "xpack.spaces.spacesManager.unableToChangeSpaceWarningTitle": "无法更改空间", "xpack.spaces.spacesTitle": "工作区", "xpack.spaces.management.copyToSpace.actionDescription": "将此已保存对象复制到一个或多个工作区", "xpack.spaces.management.copyToSpace.actionTitle": "复制到工作区", diff --git a/x-pack/test/functional/apps/spaces/enter_space.ts b/x-pack/test/functional/apps/spaces/enter_space.ts new file mode 100644 index 0000000000000..017d252b166cc --- /dev/null +++ b/x-pack/test/functional/apps/spaces/enter_space.ts @@ -0,0 +1,60 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function enterSpaceFunctonalTests({ + getService, + getPageObjects, +}: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['security', 'spaceSelector']); + + describe('Enter Space', function() { + this.tags('smoke'); + before(async () => await esArchiver.load('spaces/enter_space')); + after(async () => await esArchiver.unload('spaces/enter_space')); + + afterEach(async () => { + await PageObjects.security.logout(); + }); + + it('allows user to navigate to different spaces, respecting the configured default route', async () => { + const spaceId = 'another-space'; + + await PageObjects.security.login(null, null, { + expectSpaceSelector: true, + }); + + await PageObjects.spaceSelector.clickSpaceCard(spaceId); + + await PageObjects.spaceSelector.expectRoute(spaceId, '/app/kibana/#/dashboard'); + + await PageObjects.spaceSelector.openSpacesNav(); + + // change spaces + + await PageObjects.spaceSelector.clickSpaceAvatar('default'); + + await PageObjects.spaceSelector.expectRoute('default', '/app/canvas'); + }); + + it('falls back to the default home page when the configured default route is malformed', async () => { + await kibanaServer.uiSettings.replace({ defaultRoute: 'http://example.com/evil' }); + + // This test only works with the default space, as other spaces have an enforced relative url of `${serverBasePath}/s/space-id/${defaultRoute}` + const spaceId = 'default'; + + await PageObjects.security.login(null, null, { + expectSpaceSelector: true, + }); + + await PageObjects.spaceSelector.clickSpaceCard(spaceId); + + await PageObjects.spaceSelector.expectHomePage(spaceId); + }); + }); +} diff --git a/x-pack/test/functional/apps/spaces/index.ts b/x-pack/test/functional/apps/spaces/index.ts index 7cc704a41becc..7a876952fad83 100644 --- a/x-pack/test/functional/apps/spaces/index.ts +++ b/x-pack/test/functional/apps/spaces/index.ts @@ -12,5 +12,6 @@ export default function spacesApp({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./copy_saved_objects')); loadTestFile(require.resolve('./feature_controls/spaces_security')); loadTestFile(require.resolve('./spaces_selection')); + loadTestFile(require.resolve('./enter_space')); }); } diff --git a/x-pack/test/functional/es_archives/spaces/enter_space/data.json b/x-pack/test/functional/es_archives/spaces/enter_space/data.json new file mode 100644 index 0000000000000..462a2a1ee38fe --- /dev/null +++ b/x-pack/test/functional/es_archives/spaces/enter_space/data.json @@ -0,0 +1,83 @@ +{ + "type": "doc", + "value": { + "id": "config:6.0.0", + "index": ".kibana", + "source": { + "config": { + "buildNum": 8467, + "dateFormat:tz": "UTC", + "defaultRoute": "/app/canvas" + }, + "type": "config" + } + } +} + +{ + "type": "doc", + "value": { + "id": "another-space:config:6.0.0", + "index": ".kibana", + "source": { + "namespace": "another-space", + "config": { + "buildNum": 8467, + "dateFormat:tz": "UTC", + "defaultRoute": "/app/kibana/#dashboard" + }, + "type": "config" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "another-space:index-pattern:logstash-*", + "source": { + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "namespace": "another-space", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana", + "source": { + "space": { + "description": "This is the default space!", + "name": "Default" + }, + "type": "space" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:another-space", + "index": ".kibana", + "source": { + "space": { + "description": "This is another space", + "name": "Another Space" + }, + "type": "space" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/spaces/enter_space/mappings.json b/x-pack/test/functional/es_archives/spaces/enter_space/mappings.json new file mode 100644 index 0000000000000..f3793c7ca6780 --- /dev/null +++ b/x-pack/test/functional/es_archives/spaces/enter_space/mappings.json @@ -0,0 +1,287 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "mappings": { + "properties": { + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "defaultRoute": { + "type": "keyword" + } + } + }, + "dashboard": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "index-pattern": { + "dynamic": "strict", + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + } + } + }, + "search": { + "dynamic": "strict", + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "dynamic": "strict", + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "spaceId": { + "type": "keyword" + }, + "timelion-sheet": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "url": { + "dynamic": "strict", + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/page_objects/space_selector_page.js b/x-pack/test/functional/page_objects/space_selector_page.js index 3be1ae174ce46..ad0f48bdd50bf 100644 --- a/x-pack/test/functional/page_objects/space_selector_page.js +++ b/x-pack/test/functional/page_objects/space_selector_page.js @@ -28,14 +28,18 @@ export function SpaceSelectorPageProvider({ getService, getPageObjects }) { } async expectHomePage(spaceId) { + return await this.expectRoute(spaceId, `/app/kibana#/home`); + } + + async expectRoute(spaceId, route) { return await retry.try(async () => { - log.debug(`expectHomePage(${spaceId})`); + log.debug(`expectRoute(${spaceId}, ${route})`); await find.byCssSelector('[data-test-subj="kibanaChrome"] nav:not(.ng-hide) ', 20000); const url = await browser.getCurrentUrl(); if (spaceId === 'default') { - expect(url).to.contain(`/app/kibana#/home`); + expect(url).to.contain(route); } else { - expect(url).to.contain(`/s/${spaceId}/app/kibana#/home`); + expect(url).to.contain(`/s/${spaceId}${route}`); } }); } diff --git a/x-pack/test/spaces_api_integration/common/suites/select.ts b/x-pack/test/spaces_api_integration/common/suites/select.ts deleted file mode 100644 index 07471fe4e324f..0000000000000 --- a/x-pack/test/spaces_api_integration/common/suites/select.ts +++ /dev/null @@ -1,125 +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 { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../legacy/plugins/spaces/common/constants'; -import { getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface SelectTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; -} - -interface SelectTests { - default: SelectTest; -} - -interface SelectTestDefinition { - user?: TestDefinitionAuthentication; - currentSpaceId: string; - selectSpaceId: string; - tests: SelectTests; -} - -const nonExistantSpaceId = 'not-a-space'; - -export function selectTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const createExpectEmptyResult = () => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql(''); - }; - - const createExpectNotFoundResult = () => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - error: 'Not Found', - message: 'Not Found', - statusCode: 404, - }); - }; - - const createExpectRbacForbidden = (spaceId: any) => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unauthorized to get ${spaceId} space`, - }); - }; - - const createExpectResults = (spaceId: string) => (resp: { [key: string]: any }) => { - const allSpaces = [ - { - id: 'default', - name: 'Default Space', - description: 'This is the default space', - disabledFeatures: [], - _reserved: true, - }, - { - id: 'space_1', - name: 'Space 1', - description: 'This is the first test space', - disabledFeatures: [], - }, - { - id: 'space_2', - name: 'Space 2', - description: 'This is the second test space', - disabledFeatures: [], - }, - ]; - expect(resp.body).to.eql(allSpaces.find(space => space.id === spaceId)); - }; - - const createExpectSpaceResponse = (spaceId: string) => (resp: { [key: string]: any }) => { - if (spaceId === DEFAULT_SPACE_ID) { - expectDefaultSpaceResponse(resp); - } else { - expect(resp.body).to.eql({ - location: `/s/${spaceId}/app/kibana`, - }); - } - }; - - const expectDefaultSpaceResponse = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - location: `/app/kibana`, - }); - }; - - const makeSelectTest = (describeFn: DescribeFn) => ( - description: string, - { user = {}, currentSpaceId, selectSpaceId, tests }: SelectTestDefinition - ) => { - describeFn(description, () => { - before(() => esArchiver.load('saved_objects/spaces')); - after(() => esArchiver.unload('saved_objects/spaces')); - - it(`should return ${tests.default.statusCode}`, async () => { - return supertest - .post(`${getUrlPrefix(currentSpaceId)}/api/spaces/v1/space/${selectSpaceId}/select`) - .auth(user.username, user.password) - .expect(tests.default.statusCode) - .then(tests.default.response); - }); - }); - }; - - const selectTest = makeSelectTest(describe); - // @ts-ignore - selectTest.only = makeSelectTest(describe.only); - - return { - createExpectEmptyResult, - createExpectNotFoundResult, - createExpectRbacForbidden, - createExpectResults, - createExpectSpaceResponse, - expectDefaultSpaceResponse, - nonExistantSpaceId, - selectTest, - }; -} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts index 4493a5332b62c..300949f41f036 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts @@ -25,7 +25,6 @@ export default function({ loadTestFile, getService }: TestInvoker) { loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./get_all')); loadTestFile(require.resolve('./get')); - loadTestFile(require.resolve('./select')); loadTestFile(require.resolve('./update')); }); } diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/select.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/select.ts deleted file mode 100644 index a905fe623a7c1..0000000000000 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/select.ts +++ /dev/null @@ -1,341 +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 { AUTHENTICATION } from '../../common/lib/authentication'; -import { SPACES } from '../../common/lib/spaces'; -import { TestInvoker } from '../../common/lib/types'; -import { selectTestSuiteFactory } from '../../common/suites/select'; - -// eslint-disable-next-line import/no-default-export -export default function selectSpaceTestSuite({ getService }: TestInvoker) { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { - selectTest, - nonExistantSpaceId, - createExpectSpaceResponse, - createExpectRbacForbidden, - createExpectNotFoundResult, - } = selectTestSuiteFactory(esArchiver, supertestWithoutAuth); - - describe('select', () => { - // Tests with users that have privileges globally in Kibana - [ - { - currentSpaceId: SPACES.DEFAULT.spaceId, - selectSpaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - }, - }, - { - currentSpaceId: SPACES.SPACE_1.spaceId, - selectSpaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - }, - }, - ].forEach(scenario => { - selectTest( - `user with no access selects ${scenario.selectSpaceId} space from the ${scenario.currentSpaceId} space`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.noAccess, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(scenario.selectSpaceId), - }, - }, - } - ); - - selectTest( - `superuser selects ${scenario.selectSpaceId} space from the ${scenario.currentSpaceId} space`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.superuser, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.selectSpaceId), - }, - }, - } - ); - - selectTest( - `rbac user with all globally selects ${scenario.selectSpaceId} space from the ${scenario.currentSpaceId} space`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.allGlobally, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.selectSpaceId), - }, - }, - } - ); - - selectTest( - `dual-privileges user selects ${scenario.selectSpaceId} space from the ${scenario.currentSpaceId}`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.dualAll, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.selectSpaceId), - }, - }, - } - ); - - selectTest( - `legacy user selects ${scenario.selectSpaceId} space from the ${scenario.currentSpaceId}`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.legacyAll, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(scenario.selectSpaceId), - }, - }, - } - ); - - selectTest( - `user with read globally selects ${scenario.selectSpaceId} space from the - ${scenario.currentSpaceId} space`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.readGlobally, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.selectSpaceId), - }, - }, - } - ); - - selectTest( - `dual-privileges readonly user selects ${scenario.selectSpaceId} space from - the ${scenario.currentSpaceId}`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.dualRead, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.selectSpaceId), - }, - }, - } - ); - }); - - // Select the same space that you're currently in with users which have space specific privileges. - // Our intent is to ensure that you have privileges at the space that you're selecting. - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - selectTest( - `rbac user with all at space can select ${scenario.spaceId} - from the same space`, - { - currentSpaceId: scenario.spaceId, - selectSpaceId: scenario.spaceId, - user: scenario.users.allAtSpace, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.spaceId), - }, - }, - } - ); - - selectTest( - `rbac user with read at space can select ${scenario.spaceId} - from the same space`, - { - currentSpaceId: scenario.spaceId, - selectSpaceId: scenario.spaceId, - user: scenario.users.readAtSpace, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.spaceId), - }, - }, - } - ); - - selectTest( - `rbac user with all at other space cannot select ${scenario.spaceId} - from the same space`, - { - currentSpaceId: scenario.spaceId, - selectSpaceId: scenario.spaceId, - user: scenario.users.allAtOtherSpace, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(scenario.spaceId), - }, - }, - } - ); - }); - - // Select a different space with users that only have privileges at certain spaces. Our intent - // is to ensure that a user can select a space based on their privileges at the space that they're selecting - // not at the space that they're currently in. - [ - { - currentSpaceId: SPACES.SPACE_2.spaceId, - selectSpaceId: SPACES.SPACE_1.spaceId, - users: { - userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_2_ALL_USER, - userWithAllAtBothSpaces: AUTHENTICATION.KIBANA_RBAC_SPACE_1_2_ALL_USER, - }, - }, - ].forEach(scenario => { - selectTest( - `rbac user with all at ${scenario.selectSpaceId} can select ${scenario.selectSpaceId} - from ${scenario.currentSpaceId}`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.userWithAllAtSpace, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.selectSpaceId), - }, - }, - } - ); - - selectTest( - `rbac user with all at both spaces can select ${scenario.selectSpaceId} - from ${scenario.currentSpaceId}`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.userWithAllAtBothSpaces, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.selectSpaceId), - }, - }, - } - ); - - selectTest( - `rbac user with all at ${scenario.currentSpaceId} space cannot select ${scenario.selectSpaceId} - from ${scenario.currentSpaceId}`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.userWithAllAtOtherSpace, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(scenario.selectSpaceId), - }, - }, - } - ); - }); - - // Select non-existent spaces and ensure we get a 404 or a 403 - describe('non-existent space', () => { - [ - { - currentSpaceId: SPACES.DEFAULT.spaceId, - selectSpaceId: nonExistantSpaceId, - users: { - userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - { - currentSpaceId: SPACES.SPACE_1.spaceId, - selectSpaceId: nonExistantSpaceId, - users: { - userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - ].forEach(scenario => { - selectTest(`rbac user with all globally cannot access non-existent space`, { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.userWithAllGlobally, - tests: { - default: { - statusCode: 404, - response: createExpectNotFoundResult(), - }, - }, - }); - - selectTest(`rbac user with all at space cannot access non-existent space`, { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.userWithAllAtSpace, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(scenario.selectSpaceId), - }, - }, - }); - }); - }); - }); -} diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts index 764d1cfae22b6..1182f6bdabcff 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts @@ -17,7 +17,6 @@ export default function spacesOnlyTestSuite({ loadTestFile }: TestInvoker) { loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./get_all')); loadTestFile(require.resolve('./get')); - loadTestFile(require.resolve('./select')); loadTestFile(require.resolve('./update')); }); } diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/select.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/select.ts deleted file mode 100644 index 82a60f7d45555..0000000000000 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/select.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 { SPACES } from '../../common/lib/spaces'; -import { TestInvoker } from '../../common/lib/types'; -import { selectTestSuiteFactory } from '../../common/suites/select'; - -// eslint-disable-next-line import/no-default-export -export default function selectSpaceTestSuite({ getService }: TestInvoker) { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { - selectTest, - createExpectSpaceResponse, - createExpectNotFoundResult, - nonExistantSpaceId, - } = selectTestSuiteFactory(esArchiver, supertestWithoutAuth); - - describe('select', () => { - [ - { - spaceId: SPACES.DEFAULT.spaceId, - otherSpaceId: SPACES.SPACE_1.spaceId, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - otherSpaceId: SPACES.DEFAULT.spaceId, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - otherSpaceId: SPACES.SPACE_2.spaceId, - }, - ].forEach(scenario => { - selectTest(`can select ${scenario.otherSpaceId} from ${scenario.spaceId}`, { - currentSpaceId: scenario.spaceId, - selectSpaceId: scenario.otherSpaceId, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.otherSpaceId), - }, - }, - }); - }); - - describe('non-existant space', () => { - [ - { - spaceId: SPACES.DEFAULT.spaceId, - otherSpaceId: nonExistantSpaceId, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - otherSpaceId: nonExistantSpaceId, - }, - ].forEach(scenario => { - selectTest(`cannot select non-existant space from ${scenario.spaceId}`, { - currentSpaceId: scenario.spaceId, - selectSpaceId: scenario.otherSpaceId, - tests: { - default: { - statusCode: 404, - response: createExpectNotFoundResult(), - }, - }, - }); - }); - }); - }); -}