Support space-specific default routes (#44678)

This commit is contained in:
Larry Gregory 2019-10-02 12:05:02 -04:00 committed by GitHub
parent 51d734e9b8
commit 0bfa7ca5c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 1251 additions and 843 deletions

View file

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

View file

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

View file

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

View file

@ -3707,3 +3707,422 @@ exports[`Field for string setting should render user value if there is user valu
/>
</EuiFlexGroup>
`;
exports[`Field for stringWithValidation setting should render as read only if saving is disabled 1`] = `
<EuiFlexGroup
className="mgtAdvancedSettings__field"
>
<EuiFlexItem
grow={false}
>
<EuiDescribedFormGroup
className="mgtAdvancedSettings__fieldWrapper"
description={
<React.Fragment>
<div
dangerouslySetInnerHTML={
Object {
"__html": "Description for String test validation setting",
}
}
/>
</React.Fragment>
}
fullWidth={false}
gutterSize="l"
idAria="string:test-validation:setting-aria"
title={
<h3>
String test validation setting
</h3>
}
titleSize="xs"
>
<EuiFormRow
className="mgtAdvancedSettings__fieldRow"
describedByIds={
Array [
"string:test-validation:setting-aria",
]
}
display="row"
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={null}
isInvalid={false}
label="string:test-validation:setting"
labelType="label"
>
<EuiFieldText
aria-label="string test validation setting"
compressed={false}
data-test-subj="advancedSetting-editField-string:test-validation:setting"
disabled={true}
fullWidth={false}
isLoading={false}
onChange={[Function]}
onKeyDown={[Function]}
value="foo-default"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</EuiFlexItem>
<EuiFlexItem
grow={false}
/>
</EuiFlexGroup>
`;
exports[`Field for stringWithValidation setting should render as read only with help text if overridden 1`] = `
<EuiFlexGroup
className="mgtAdvancedSettings__field"
>
<EuiFlexItem
grow={false}
>
<EuiDescribedFormGroup
className="mgtAdvancedSettings__fieldWrapper"
description={
<React.Fragment>
<div
dangerouslySetInnerHTML={
Object {
"__html": "Description for String test validation setting",
}
}
/>
<React.Fragment>
<EuiSpacer
size="s"
/>
<EuiText
size="xs"
>
<React.Fragment>
<FormattedMessage
defaultMessage="Default: {value}"
id="kbn.management.settings.field.defaultValueText"
values={
Object {
"value": <EuiCode>
foo-default
</EuiCode>,
}
}
/>
</React.Fragment>
</EuiText>
</React.Fragment>
</React.Fragment>
}
fullWidth={false}
gutterSize="l"
idAria="string:test-validation:setting-aria"
title={
<h3>
String test validation setting
</h3>
}
titleSize="xs"
>
<EuiFormRow
className="mgtAdvancedSettings__fieldRow"
describedByIds={
Array [
"string:test-validation:setting-aria",
]
}
display="row"
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<EuiText
size="xs"
>
<FormattedMessage
defaultMessage="This setting is overridden by the Kibana server and can not be changed."
id="kbn.management.settings.field.helpText"
values={Object {}}
/>
</EuiText>
}
isInvalid={false}
label="string:test-validation:setting"
labelType="label"
>
<EuiFieldText
aria-label="string test validation setting"
compressed={false}
data-test-subj="advancedSetting-editField-string:test-validation:setting"
disabled={true}
fullWidth={false}
isLoading={false}
onChange={[Function]}
onKeyDown={[Function]}
value="fooUserValue"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</EuiFlexItem>
<EuiFlexItem
grow={false}
/>
</EuiFlexGroup>
`;
exports[`Field for stringWithValidation setting should render custom setting icon if it is custom 1`] = `
<EuiFlexGroup
className="mgtAdvancedSettings__field"
>
<EuiFlexItem
grow={false}
>
<EuiDescribedFormGroup
className="mgtAdvancedSettings__fieldWrapper"
description={
<React.Fragment>
<div
dangerouslySetInnerHTML={
Object {
"__html": "Description for String test validation setting",
}
}
/>
</React.Fragment>
}
fullWidth={false}
gutterSize="l"
idAria="string:test-validation:setting-aria"
title={
<h3>
String test validation setting
<EuiIconTip
aria-label="Custom setting"
color="primary"
content={
<FormattedMessage
defaultMessage="Custom setting"
id="kbn.management.settings.field.customSettingTooltip"
values={Object {}}
/>
}
type="asterisk"
/>
</h3>
}
titleSize="xs"
>
<EuiFormRow
className="mgtAdvancedSettings__fieldRow"
describedByIds={
Array [
"string:test-validation:setting-aria",
]
}
display="row"
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={null}
isInvalid={false}
label="string:test-validation:setting"
labelType="label"
>
<EuiFieldText
aria-label="string test validation setting"
compressed={false}
data-test-subj="advancedSetting-editField-string:test-validation:setting"
disabled={false}
fullWidth={false}
isLoading={false}
onChange={[Function]}
onKeyDown={[Function]}
value="foo-default"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</EuiFlexItem>
<EuiFlexItem
grow={false}
/>
</EuiFlexGroup>
`;
exports[`Field for stringWithValidation setting should render default value if there is no user value set 1`] = `
<EuiFlexGroup
className="mgtAdvancedSettings__field"
>
<EuiFlexItem
grow={false}
>
<EuiDescribedFormGroup
className="mgtAdvancedSettings__fieldWrapper"
description={
<React.Fragment>
<div
dangerouslySetInnerHTML={
Object {
"__html": "Description for String test validation setting",
}
}
/>
</React.Fragment>
}
fullWidth={false}
gutterSize="l"
idAria="string:test-validation:setting-aria"
title={
<h3>
String test validation setting
</h3>
}
titleSize="xs"
>
<EuiFormRow
className="mgtAdvancedSettings__fieldRow"
describedByIds={
Array [
"string:test-validation:setting-aria",
]
}
display="row"
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={null}
isInvalid={false}
label="string:test-validation:setting"
labelType="label"
>
<EuiFieldText
aria-label="string test validation setting"
compressed={false}
data-test-subj="advancedSetting-editField-string:test-validation:setting"
disabled={false}
fullWidth={false}
isLoading={false}
onChange={[Function]}
onKeyDown={[Function]}
value="foo-default"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</EuiFlexItem>
<EuiFlexItem
grow={false}
/>
</EuiFlexGroup>
`;
exports[`Field for stringWithValidation setting should render user value if there is user value is set 1`] = `
<EuiFlexGroup
className="mgtAdvancedSettings__field"
>
<EuiFlexItem
grow={false}
>
<EuiDescribedFormGroup
className="mgtAdvancedSettings__fieldWrapper"
description={
<React.Fragment>
<div
dangerouslySetInnerHTML={
Object {
"__html": "Description for String test validation setting",
}
}
/>
<React.Fragment>
<EuiSpacer
size="s"
/>
<EuiText
size="xs"
>
<React.Fragment>
<FormattedMessage
defaultMessage="Default: {value}"
id="kbn.management.settings.field.defaultValueText"
values={
Object {
"value": <EuiCode>
foo-default
</EuiCode>,
}
}
/>
</React.Fragment>
</EuiText>
</React.Fragment>
</React.Fragment>
}
fullWidth={false}
gutterSize="l"
idAria="string:test-validation:setting-aria"
title={
<h3>
String test validation setting
</h3>
}
titleSize="xs"
>
<EuiFormRow
className="mgtAdvancedSettings__fieldRow"
describedByIds={
Array [
"string:test-validation:setting-aria",
]
}
display="row"
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<span>
<span>
<EuiLink
aria-label="Reset string test validation setting to default"
color="primary"
data-test-subj="advancedSetting-resetField-string:test-validation:setting"
onClick={[Function]}
type="button"
>
<FormattedMessage
defaultMessage="Reset to default"
id="kbn.management.settings.field.resetToDefaultLinkText"
values={Object {}}
/>
</EuiLink>
   
</span>
</span>
}
isInvalid={false}
label="string:test-validation:setting"
labelType="label"
>
<EuiFieldText
aria-label="string test validation setting"
compressed={false}
data-test-subj="advancedSetting-editField-string:test-validation:setting"
disabled={false}
fullWidth={false}
isLoading={false}
onChange={[Function]}
onKeyDown={[Function]}
value="fooUserValue"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</EuiFlexItem>
<EuiFlexItem
grow={false}
/>
</EuiFlexGroup>
`;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -83,6 +83,7 @@ declare module 'hapi' {
interface Request {
getSavedObjectsClient(options?: SavedObjectsClientProviderOptions): SavedObjectsClientContract;
getBasePath(): string;
getDefaultRoute(): Promise<string>;
getUiSettingsService(): any;
getCapabilities(): Promise<Capabilities>;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<string, any>) =>
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<string, any>) =>
},
});
initEnterSpaceView(server);
initSpaceSelectorView(server);
server.expose('getSpaceId', (request: any) => spacesService.getSpaceId(request));

View file

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

View file

@ -5,3 +5,4 @@
*/
export { SpacesManager } from './spaces_manager';
export { getSpaceInitials, getSpaceColor, getSpaceImageUrl } from './space_attributes';

View file

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

View file

@ -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<Space[]> {
@ -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',
}),
});
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
interface RouteDeps {
xpackMain: XPackMainPlugin;
spacesService: SpacesServiceSetup;
getLegacyAPI(): LegacyAPI;
legacyRouter: Legacy.Server['route'];
}
export interface InternalRouteDeps extends Omit<RouteDeps, 'xpackMain'> {
routePreCheckLicenseFn: any;
}
export function initInternalApis({ xpackMain, ...rest }: RouteDeps) {
const routePreCheckLicenseFn = routePreCheckLicense({ xpackMain });
const deps: InternalRouteDeps = {
...rest,
routePreCheckLicenseFn,
};
initInternalSpacesApi(deps);
}

View file

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

View file

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

View file

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

View file

@ -5,3 +5,4 @@
*/
export { initSpaceSelectorView } from './space_selector';
export { initEnterSpaceView } from './enter_space';

View file

@ -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": "スペースにコピー",

View file

@ -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": "复制到工作区",

View file

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

View file

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

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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