diff --git a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js index d0a3160018fea..d0cc7091d7767 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js +++ b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js @@ -25,12 +25,15 @@ import { setupUsers, DEFAULT_SUPERUSER_PASS } from './auth'; export async function runElasticsearch({ config, options }) { const { log, esFrom } = options; - const isOss = config.get('esTestCluster.license') === 'oss'; + const license = config.get('esTestCluster.license'); + const isTrialLicense = config.get('esTestCluster.license') === 'trial'; const cluster = createEsTestCluster({ port: config.get('servers.elasticsearch.port'), - password: !isOss ? DEFAULT_SUPERUSER_PASS : config.get('servers.elasticsearch.password'), - license: config.get('esTestCluster.license'), + password: isTrialLicense + ? DEFAULT_SUPERUSER_PASS + : config.get('servers.elasticsearch.password'), + license, log, basePath: resolve(KIBANA_ROOT, '.es'), esFrom: esFrom || config.get('esTestCluster.from'), @@ -40,7 +43,7 @@ export async function runElasticsearch({ config, options }) { await cluster.start(esArgs); - if (!isOss) { + if (isTrialLicense) { await setupUsers(log, config); } diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index f92ebae39e517..b93d3a0ac5371 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -26,6 +26,7 @@ import { Project } from './project'; export const PROJECTS = [ new Project(resolve(REPO_ROOT, 'tsconfig.json')), new Project(resolve(REPO_ROOT, 'x-pack/tsconfig.json')), + new Project(resolve(REPO_ROOT, 'x-pack/test/tsconfig.json'), 'x-pack/test'), // NOTE: using glob.sync rather than glob-all or globby // because it takes less than 10 ms, while the other modules diff --git a/src/server/saved_objects/service/lib/__snapshots__/priority_collection.test.ts.snap b/src/server/saved_objects/service/lib/__snapshots__/priority_collection.test.ts.snap new file mode 100644 index 0000000000000..fd96c54450cf7 --- /dev/null +++ b/src/server/saved_objects/service/lib/__snapshots__/priority_collection.test.ts.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`1, 1 throws Error 1`] = `"Already have entry with this priority"`; diff --git a/src/server/saved_objects/service/lib/priority_collection.test.ts b/src/server/saved_objects/service/lib/priority_collection.test.ts new file mode 100644 index 0000000000000..9256b2e913931 --- /dev/null +++ b/src/server/saved_objects/service/lib/priority_collection.test.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { PriorityCollection } from './priority_collection'; + +test(`1, 2, 3`, () => { + const priorityCollection = new PriorityCollection(); + priorityCollection.add(1, 1); + priorityCollection.add(2, 2); + priorityCollection.add(3, 3); + expect(priorityCollection.toPrioritizedArray()).toEqual([1, 2, 3]); +}); + +test(`3, 2, 1`, () => { + const priorityCollection = new PriorityCollection(); + priorityCollection.add(3, 3); + priorityCollection.add(2, 2); + priorityCollection.add(1, 1); + expect(priorityCollection.toPrioritizedArray()).toEqual([1, 2, 3]); +}); + +test(`2, 3, 1`, () => { + const priorityCollection = new PriorityCollection(); + priorityCollection.add(2, 2); + priorityCollection.add(3, 3); + priorityCollection.add(1, 1); + expect(priorityCollection.toPrioritizedArray()).toEqual([1, 2, 3]); +}); + +test(`Number.MAX_VALUE, NUMBER.MIN_VALUE, 1`, () => { + const priorityCollection = new PriorityCollection(); + priorityCollection.add(Number.MAX_VALUE, 3); + priorityCollection.add(Number.MIN_VALUE, 1); + priorityCollection.add(1, 2); + expect(priorityCollection.toPrioritizedArray()).toEqual([1, 2, 3]); +}); + +test(`1, 1 throws Error`, () => { + const priorityCollection = new PriorityCollection(); + priorityCollection.add(1, 1); + expect(() => priorityCollection.add(1, 1)).toThrowErrorMatchingSnapshot(); +}); diff --git a/src/server/saved_objects/service/lib/priority_collection.ts b/src/server/saved_objects/service/lib/priority_collection.ts new file mode 100644 index 0000000000000..ab8edaaaea22f --- /dev/null +++ b/src/server/saved_objects/service/lib/priority_collection.ts @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +interface PriorityCollectionEntry { + priority: number; + value: T; +} + +export class PriorityCollection { + private readonly array: Array> = []; + + public add(priority: number, value: T) { + let i = 0; + while (i < this.array.length) { + const current = this.array[i]; + if (priority === current.priority) { + throw new Error('Already have entry with this priority'); + } + + if (priority < current.priority) { + break; + } + ++i; + } + this.array.splice(i, 0, { priority, value }); + } + + public toPrioritizedArray(): T[] { + return this.array.map(entry => entry.value); + } +} diff --git a/src/server/saved_objects/service/lib/scoped_client_provider.js b/src/server/saved_objects/service/lib/scoped_client_provider.js index ddcc9c1c3ff56..05cc97945ef0b 100644 --- a/src/server/saved_objects/service/lib/scoped_client_provider.js +++ b/src/server/saved_objects/service/lib/scoped_client_provider.js @@ -16,13 +16,14 @@ * specific language governing permissions and limitations * under the License. */ +import { PriorityCollection } from './priority_collection'; /** * Provider for the Scoped Saved Object Client. */ export class ScopedSavedObjectsClientProvider { - _wrapperFactories = []; + _wrapperFactories = new PriorityCollection(); constructor({ defaultClientFactory @@ -30,16 +31,8 @@ export class ScopedSavedObjectsClientProvider { this._originalClientFactory = this._clientFactory = defaultClientFactory; } - // the client wrapper factories are put at the front of the array, so that - // when we use `reduce` below they're invoked in LIFO order. This is so that - // if multiple plugins register their client wrapper factories, then we can use - // the plugin dependencies/optionalDependencies to implicitly control the order - // in which these are used. For example, if we have a plugin a that declares a - // dependency on plugin b, that means that plugin b's client wrapper would want - // to be able to run first when the SavedObjectClient methods are invoked to - // provide additional context to plugin a's client wrapper. - addClientWrapperFactory(wrapperFactory) { - this._wrapperFactories.unshift(wrapperFactory); + addClientWrapperFactory(priority, wrapperFactory) { + this._wrapperFactories.add(priority, wrapperFactory); } setClientFactory(customClientFactory) { @@ -55,11 +48,13 @@ export class ScopedSavedObjectsClientProvider { request, }); - return this._wrapperFactories.reduce((clientToWrap, wrapperFactory) => { - return wrapperFactory({ - request, - client: clientToWrap, - }); - }, client); + return this._wrapperFactories + .toPrioritizedArray() + .reduceRight((clientToWrap, wrapperFactory) => { + return wrapperFactory({ + request, + client: clientToWrap, + }); + }, client); } } diff --git a/src/server/saved_objects/service/lib/scoped_client_provider.test.js b/src/server/saved_objects/service/lib/scoped_client_provider.test.js index 219f35559c884..52a98c08edde5 100644 --- a/src/server/saved_objects/service/lib/scoped_client_provider.test.js +++ b/src/server/saved_objects/service/lib/scoped_client_provider.test.js @@ -64,40 +64,20 @@ test(`throws error when more than one scoped saved objects client factory is set }).toThrowErrorMatchingSnapshot(); }); -test(`invokes and uses instance from single added wrapper factory`, () => { +test(`invokes and uses wrappers in specified order`, () => { const defaultClient = Symbol(); const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient); const clientProvider = new ScopedSavedObjectsClientProvider({ defaultClientFactory: defaultClientFactoryMock }); - const wrappedClient = Symbol(); - const clientWrapperFactoryMock = jest.fn().mockReturnValue(wrappedClient); - const request = Symbol(); - - clientProvider.addClientWrapperFactory(clientWrapperFactoryMock); - const actualClient = clientProvider.getClient(request); - - expect(actualClient).toBe(wrappedClient); - expect(clientWrapperFactoryMock).toHaveBeenCalledWith({ - request, - client: defaultClient - }); -}); - -test(`invokes and uses wrappers in LIFO order`, () => { - const defaultClient = Symbol(); - const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient); - const clientProvider = new ScopedSavedObjectsClientProvider({ - defaultClientFactory: defaultClientFactoryMock - }); - const firstWrappedClient = Symbol(); + const firstWrappedClient = Symbol('first client'); const firstClientWrapperFactoryMock = jest.fn().mockReturnValue(firstWrappedClient); - const secondWrapperClient = Symbol(); + const secondWrapperClient = Symbol('second client'); const secondClientWrapperFactoryMock = jest.fn().mockReturnValue(secondWrapperClient); const request = Symbol(); - clientProvider.addClientWrapperFactory(firstClientWrapperFactoryMock); - clientProvider.addClientWrapperFactory(secondClientWrapperFactoryMock); + clientProvider.addClientWrapperFactory(1, secondClientWrapperFactoryMock); + clientProvider.addClientWrapperFactory(0, firstClientWrapperFactoryMock); const actualClient = clientProvider.getClient(request); expect(actualClient).toBe(firstWrappedClient); diff --git a/test/api_integration/apis/saved_objects/bulk_create.js b/test/api_integration/apis/saved_objects/bulk_create.js new file mode 100644 index 0000000000000..153dda4691fa6 --- /dev/null +++ b/test/api_integration/apis/saved_objects/bulk_create.js @@ -0,0 +1,121 @@ +/* + * 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 expect from 'expect.js'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + const BULK_REQUESTS = [ + { + type: 'visualization', + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + attributes: { + title: 'An existing visualization' + } + }, + { + type: 'dashboard', + id: 'a01b2f57-fcfd-4864-b735-09e28f0d815e', + attributes: { + title: 'A great new dashboard' + } + }, + ]; + + describe('_bulk_create', () => { + describe('with kibana index', () => { + before(() => esArchiver.load('saved_objects/basic')); + after(() => esArchiver.unload('saved_objects/basic')); + + it('should return 200 with individual responses', async () => ( + await supertest + .post(`/api/saved_objects/_bulk_create`) + .send(BULK_REQUESTS) + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + saved_objects: [ + { + type: 'visualization', + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + error: { + 'message': 'version conflict, document already exists', + 'statusCode': 409 + } + }, + { + type: 'dashboard', + id: 'a01b2f57-fcfd-4864-b735-09e28f0d815e', + updated_at: resp.body.saved_objects[1].updated_at, + version: 1, + attributes: { + title: 'A great new dashboard' + } + }, + ] + }); + }) + )); + }); + + describe('without kibana index', () => { + before(async () => ( + // just in case the kibana server has recreated it + await es.indices.delete({ + index: '.kibana', + ignore: [404], + }) + )); + + it('should return 200 with individual responses', async () => ( + await supertest + .post('/api/saved_objects/_bulk_create') + .send(BULK_REQUESTS) + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + saved_objects: [ + { + type: 'visualization', + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + updated_at: resp.body.saved_objects[0].updated_at, + version: 1, + attributes: { + title: 'An existing visualization' + } + }, + { + type: 'dashboard', + id: 'a01b2f57-fcfd-4864-b735-09e28f0d815e', + updated_at: resp.body.saved_objects[1].updated_at, + version: 1, + attributes: { + title: 'A great new dashboard' + } + }, + ] + }); + }) + )); + }); + }); +} diff --git a/test/api_integration/apis/saved_objects/index.js b/test/api_integration/apis/saved_objects/index.js index c7694557c836c..fce7f9eba767e 100644 --- a/test/api_integration/apis/saved_objects/index.js +++ b/test/api_integration/apis/saved_objects/index.js @@ -19,6 +19,7 @@ export default function ({ loadTestFile }) { describe('saved_objects', () => { + loadTestFile(require.resolve('./bulk_create')); loadTestFile(require.resolve('./bulk_get')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); diff --git a/tsconfig.json b/tsconfig.json index da6008a67d849..7a16f02128992 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -46,6 +46,7 @@ // Forbid unused local variables as the rule was deprecated by ts-lint "noUnusedLocals": true, + }, "include": [ "src/**/*" diff --git a/x-pack/package.json b/x-pack/package.json index 375b5c390a894..2068663468f62 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -25,8 +25,11 @@ "@kbn/es": "link:../packages/kbn-es", "@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers", "@kbn/test": "link:../packages/kbn-test", + "@types/expect.js": "^0.3.29", "@types/jest": "^23.3.1", + "@types/mocha": "^5.2.5", "@types/pngjs": "^3.3.1", + "@types/supertest": "^2.0.5", "abab": "^1.0.4", "ansi-colors": "^3.0.5", "ansicolors": "0.3.2", @@ -42,7 +45,7 @@ "enzyme": "3.2.0", "enzyme-adapter-react-16": "^1.1.1", "enzyme-to-json": "3.3.1", - "expect.js": "0.3.1", + "expect.js": "^0.3.1", "fancy-log": "^1.3.2", "fetch-mock": "^5.13.1", "gulp": "3.9.1", @@ -166,4 +169,4 @@ "engines": { "yarn": "^1.6.0" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/security/common/constants.js b/x-pack/plugins/security/common/constants.js index 1b762d5f15acc..a62085787cd47 100644 --- a/x-pack/plugins/security/common/constants.js +++ b/x-pack/plugins/security/common/constants.js @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export const ALL_RESOURCE = '*'; +export const GLOBAL_RESOURCE = '*'; +export const IGNORED_TYPES = ['space']; diff --git a/x-pack/plugins/security/common/model/kibana_privilege.ts b/x-pack/plugins/security/common/model/kibana_privilege.ts index 834e62570fe26..20cac65b4ca79 100644 --- a/x-pack/plugins/security/common/model/kibana_privilege.ts +++ b/x-pack/plugins/security/common/model/kibana_privilege.ts @@ -5,3 +5,5 @@ */ export type KibanaPrivilege = 'none' | 'read' | 'all'; + +export const KibanaAppPrivileges: KibanaPrivilege[] = ['read', 'all']; diff --git a/x-pack/plugins/security/index.js b/x-pack/plugins/security/index.js index 68601a5ef07ba..7262eadb999c6 100644 --- a/x-pack/plugins/security/index.js +++ b/x-pack/plugins/security/index.js @@ -16,12 +16,12 @@ import { validateConfig } from './server/lib/validate_config'; import { authenticateFactory } from './server/lib/auth_redirect'; import { checkLicense } from './server/lib/check_license'; import { initAuthenticator } from './server/lib/authentication/authenticator'; -import { initPrivilegesApi } from './server/routes/api/v1/privileges'; import { SecurityAuditLogger } from './server/lib/audit_logger'; import { AuditLogger } from '../../server/lib/audit_logger'; -import { SecureSavedObjectsClient } from './server/lib/saved_objects_client/secure_saved_objects_client'; -import { initAuthorizationService, registerPrivilegesWithCluster } from './server/lib/authorization'; +import { createAuthorizationService, registerPrivilegesWithCluster } from './server/lib/authorization'; import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize'; +import { SecureSavedObjectsClientWrapper } from './server/lib/saved_objects_client/secure_saved_objects_client_wrapper'; +import { deepFreeze } from './server/lib/deep_freeze'; export const security = (kibana) => new kibana.Plugin({ id: 'security', @@ -106,7 +106,8 @@ export const security = (kibana) => new kibana.Plugin({ server.auth.strategy('session', 'login', 'required'); // exposes server.plugins.security.authorization - initAuthorizationService(server); + const authorization = createAuthorizationService(server, xpackInfoFeature); + server.expose('authorization', deepFreeze(authorization)); watchStatusAndLicenseToInitialize(xpackMainPlugin, plugin, async (license) => { if (license.allowRbac) { @@ -124,35 +125,41 @@ export const security = (kibana) => new kibana.Plugin({ const { callWithRequest, callWithInternalUser } = adminCluster; const callCluster = (...args) => callWithRequest(request, ...args); + if (authorization.mode.useRbacForRequest(request)) { + const internalRepository = savedObjects.getSavedObjectsRepository(callWithInternalUser); + return new savedObjects.SavedObjectsClient(internalRepository); + } + const callWithRequestRepository = savedObjects.getSavedObjectsRepository(callCluster); + return new savedObjects.SavedObjectsClient(callWithRequestRepository); + }); - if (!xpackInfoFeature.getLicenseCheckResults().allowRbac) { - return new savedObjects.SavedObjectsClient(callWithRequestRepository); + savedObjects.addScopedSavedObjectsClientWrapperFactory(Number.MIN_VALUE, ({ client, request }) => { + if (authorization.mode.useRbacForRequest(request)) { + const { spaces } = server.plugins; + + return new SecureSavedObjectsClientWrapper({ + actions: authorization.actions, + auditLogger, + baseClient: client, + checkPrivilegesWithRequest: authorization.checkPrivilegesWithRequest, + errors: savedObjects.SavedObjectsClient.errors, + request, + savedObjectTypes: savedObjects.types, + spaces, + }); } - const { authorization } = server.plugins.security; - const checkPrivileges = authorization.checkPrivilegesWithRequest(request); - const internalRepository = savedObjects.getSavedObjectsRepository(callWithInternalUser); - - return new SecureSavedObjectsClient({ - internalRepository, - callWithRequestRepository, - errors: savedObjects.SavedObjectsClient.errors, - checkPrivileges, - auditLogger, - savedObjectTypes: savedObjects.types, - actions: authorization.actions, - }); + return client; }); getUserProvider(server); - await initAuthenticator(server); + await initAuthenticator(server, authorization.mode); initAuthenticateApi(server); initUsersApi(server); initPublicRolesApi(server); initIndicesApi(server); - initPrivilegesApi(server); initLoginView(server, xpackMainPlugin); initLogoutView(server); diff --git a/x-pack/plugins/security/public/services/application_privilege.js b/x-pack/plugins/security/public/services/application_privilege.js index 615188cad33d7..00db6cccfb13e 100644 --- a/x-pack/plugins/security/public/services/application_privilege.js +++ b/x-pack/plugins/security/public/services/application_privilege.js @@ -10,5 +10,9 @@ import { uiModules } from 'ui/modules'; const module = uiModules.get('security', ['ngResource']); module.service('ApplicationPrivileges', ($resource, chrome) => { const baseUrl = chrome.addBasePath('/api/security/v1/privileges'); - return $resource(baseUrl); + return $resource(baseUrl, null, { + query: { + isArray: false, + } + }); }); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx index ee1de5d99e191..0fd03481eec1d 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx @@ -24,7 +24,7 @@ import React, { ChangeEvent, Component, Fragment, HTMLProps } from 'react'; import { toastNotifications } from 'ui/notify'; import { Space } from '../../../../../../spaces/common/model/space'; import { IndexPrivilege } from '../../../../../common/model/index_privilege'; -import { KibanaApplicationPrivilege } from '../../../../../common/model/kibana_application_privilege'; +import { KibanaPrivilege } from '../../../../../common/model/kibana_privilege'; import { Role } from '../../../../../common/model/role'; import { isReservedRole } from '../../../../lib/role'; import { deleteRole, saveRole } from '../../../../objects'; @@ -42,7 +42,7 @@ interface Props { rbacEnabled: boolean; allowDocumentLevelSecurity: boolean; allowFieldLevelSecurity: boolean; - kibanaAppPrivileges: KibanaApplicationPrivilege[]; + kibanaAppPrivileges: KibanaPrivilege[]; notifier: any; spaces?: Space[]; spacesEnabled: boolean; diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges.test.tsx.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges.test.tsx.snap index 85f0acaf92a40..373e62adca6ff 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges.test.tsx.snap +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges.test.tsx.snap @@ -9,9 +9,7 @@ exports[` renders without crashing 1`] = ` editable={true} kibanaAppPrivileges={ Array [ - Object { - "name": "all", - }, + "all", ] } onChange={[MockFunction]} diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.test.tsx index 400c2ab9e2b96..82a1133f7aaa2 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.test.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.test.tsx @@ -6,7 +6,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { KibanaApplicationPrivilege } from '../../../../../../../common/model/kibana_application_privilege'; +import { KibanaPrivilege } from '../../../../../../../../security/common/model/kibana_privilege'; import { RoleValidator } from '../../../lib/validate_role'; import { KibanaPrivileges } from './kibana_privileges'; import { SimplePrivilegeForm } from './simple_privilege_form'; @@ -39,7 +39,7 @@ const buildProps = (customProps = {}) => { }, ], editable: true, - kibanaAppPrivileges: [{ name: 'all' } as KibanaApplicationPrivilege], + kibanaAppPrivileges: ['all' as KibanaPrivilege], onChange: jest.fn(), validator: new RoleValidator(), ...customProps, diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.tsx index d9a4ba73af963..1ff7b62f715a7 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.tsx @@ -6,7 +6,7 @@ import React, { Component } from 'react'; import { Space } from '../../../../../../../../spaces/common/model/space'; -import { KibanaApplicationPrivilege } from '../../../../../../../common/model/kibana_application_privilege'; +import { KibanaPrivilege } from '../../../../../../../common/model/kibana_privilege'; import { Role } from '../../../../../../../common/model/role'; import { RoleValidator } from '../../../lib/validate_role'; import { CollapsiblePanel } from '../../collapsible_panel'; @@ -18,7 +18,7 @@ interface Props { spacesEnabled: boolean; spaces?: Space[]; editable: boolean; - kibanaAppPrivileges: KibanaApplicationPrivilege[]; + kibanaAppPrivileges: KibanaPrivilege[]; onChange: (role: Role) => void; validator: RoleValidator; } diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_form.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_form.test.tsx index 018aed7df9bb1..d6ecbdb705b74 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_form.test.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_form.test.tsx @@ -24,14 +24,7 @@ const buildProps = (customProps?: any) => { }, }, editable: true, - kibanaAppPrivileges: [ - { - name: 'all', - }, - { - name: 'read', - }, - ], + kibanaAppPrivileges: ['all', 'read'], onChange: jest.fn(), ...customProps, }; diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_form.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_form.tsx index 4d8892d88fce4..71b881c4a85ef 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_form.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_form.tsx @@ -10,7 +10,6 @@ import { EuiFormRow, } from '@elastic/eui'; import React, { Component, Fragment } from 'react'; -import { KibanaApplicationPrivilege } from '../../../../../../../common/model/kibana_application_privilege'; import { KibanaPrivilege } from '../../../../../../../common/model/kibana_privilege'; import { Role } from '../../../../../../../common/model/role'; import { isReservedRole } from '../../../../../../lib/role'; @@ -19,7 +18,7 @@ import { copyRole } from '../../../lib/copy_role'; import { PrivilegeSelector } from './privilege_selector'; interface Props { - kibanaAppPrivileges: KibanaApplicationPrivilege[]; + kibanaAppPrivileges: KibanaPrivilege[]; role: Role; onChange: (role: Role) => void; editable: boolean; @@ -30,7 +29,6 @@ export class SimplePrivilegeForm extends Component { const { kibanaAppPrivileges, role } = this.props; const assignedPrivileges = role.kibana; - const availablePrivileges = kibanaAppPrivileges.map(privilege => privilege.name); const kibanaPrivilege: KibanaPrivilege = assignedPrivileges.global.length > 0 @@ -45,7 +43,7 @@ export class SimplePrivilegeForm extends Component { { }, ], editable: true, - kibanaAppPrivileges: [ - { - name: 'all', - }, - { - name: 'read', - }, - ], + kibanaAppPrivileges: ['all', 'read'], onChange: jest.fn(), validator: new RoleValidator(), ...customProps, diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.tsx index 060f89726bb72..2801862cabf94 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.tsx @@ -17,7 +17,6 @@ import { } from '@elastic/eui'; import React, { Component, Fragment } from 'react'; import { Space } from '../../../../../../../../spaces/common/model/space'; -import { KibanaApplicationPrivilege } from '../../../../../../../common/model/kibana_application_privilege'; import { KibanaPrivilege } from '../../../../../../../common/model/kibana_privilege'; import { Role } from '../../../../../../../common/model/role'; import { isReservedRole } from '../../../../../../lib/role'; @@ -32,7 +31,7 @@ import { PrivilegeSpaceForm } from './privilege_space_form'; import { PrivilegeSpaceTable } from './privilege_space_table'; interface Props { - kibanaAppPrivileges: KibanaApplicationPrivilege[]; + kibanaAppPrivileges: KibanaPrivilege[]; role: Role; spaces: Space[]; onChange: (role: Role) => void; @@ -74,7 +73,6 @@ export class SpaceAwarePrivilegeForm extends Component { const { kibanaAppPrivileges, role } = this.props; const assignedPrivileges = role.kibana; - const availablePrivileges = kibanaAppPrivileges.map(privilege => privilege.name); const basePrivilege = assignedPrivileges.global.length > 0 ? assignedPrivileges.global[0] : NO_PRIVILEGE_VALUE; @@ -99,7 +97,7 @@ export class SpaceAwarePrivilegeForm extends Component { { - {this.renderSpacePrivileges(basePrivilege, availablePrivileges)} + {this.renderSpacePrivileges(basePrivilege, kibanaAppPrivileges)} ); } diff --git a/x-pack/plugins/security/public/views/management/edit_role/index.js b/x-pack/plugins/security/public/views/management/edit_role/index.js index 5b8b57d6c0101..5c744c908824c 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/index.js +++ b/x-pack/plugins/security/public/views/management/edit_role/index.js @@ -27,6 +27,7 @@ import { EditRolePage } from './components'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { KibanaAppPrivileges } from '../../../../common/model/kibana_privilege'; routes.when(`${EDIT_ROLES_PATH}/:name?`, { template, @@ -67,11 +68,6 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, { return role.then(res => res.toJSON()); }, - kibanaApplicationPrivilege(ApplicationPrivileges, kbnUrl, Promise, Private) { - return ApplicationPrivileges.query().$promise - .then(privileges => privileges.map(p => p.toJSON())) - .catch(checkLicenseError(kbnUrl, Promise, Private)); - }, users(ShieldUser, kbnUrl, Promise, Private) { // $promise is used here because the result is an ngResource, not a promise itself return ShieldUser.query().$promise @@ -93,7 +89,6 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, { const Notifier = $injector.get('Notifier'); - const kibanaApplicationPrivilege = $route.current.locals.kibanaApplicationPrivilege; const role = $route.current.locals.role; const xpackInfo = Private(XPackInfoProvider); @@ -132,7 +127,7 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, { render( { let server; let session; let cluster; + let authorizationMode; beforeEach(() => { server = serverFixture(); session = sinon.createStubInstance(Session); @@ -34,6 +35,8 @@ describe('Authenticator', () => { cluster = sinon.stub({ callWithRequest() {} }); sandbox.stub(ClientShield, 'getClient').returns(cluster); + authorizationMode = { initialize: sinon.stub() }; + server.config.returns(config); server.register.yields(); @@ -83,7 +86,7 @@ describe('Authenticator', () => { server.plugins.kibana.systemApi.isSystemApiRequest.returns(true); session.clear.throws(new Error('`Session.clear` is not supposed to be called!')); - await initAuthenticator(server); + await initAuthenticator(server, authorizationMode); // Second argument will be a method we'd like to test. authenticate = server.expose.withArgs('authenticate').firstCall.args[1]; @@ -112,6 +115,18 @@ describe('Authenticator', () => { expect(authenticationResult.error).to.be(failureReason); }); + it(`doesn't initialize authorizationMode when authentication fails.`, async () => { + const request = requestFixture({ headers: { authorization: 'Basic ***' } }); + session.get.withArgs(request).returns(Promise.resolve(null)); + + const failureReason = new Error('Not Authorized'); + cluster.callWithRequest.withArgs(request).returns(Promise.reject(failureReason)); + + await authenticate(request); + + sinon.assert.notCalled(authorizationMode.initialize); + }); + it('returns user that authentication provider returns.', async () => { const request = requestFixture({ headers: { authorization: 'Basic ***' } }); const user = { username: 'user' }; @@ -125,6 +140,15 @@ describe('Authenticator', () => { }); }); + it('initiliazes authorizationMode when authentication succeeds.', async () => { + const request = requestFixture({ headers: { authorization: 'Basic ***' } }); + const user = { username: 'user' }; + cluster.callWithRequest.withArgs(request).returns(Promise.resolve(user)); + + await authenticate(request); + sinon.assert.calledWith(authorizationMode.initialize, request); + }); + it('creates session whenever authentication provider returns state to store.', async () => { const user = { username: 'user' }; const systemAPIRequest = requestFixture({ headers: { authorization: 'Basic xxx' } }); diff --git a/x-pack/plugins/security/server/lib/authentication/authenticator.js b/x-pack/plugins/security/server/lib/authentication/authenticator.js index 3f11f47892105..e731c6724e9d4 100644 --- a/x-pack/plugins/security/server/lib/authentication/authenticator.js +++ b/x-pack/plugins/security/server/lib/authentication/authenticator.js @@ -102,11 +102,13 @@ class Authenticator { * @param {Hapi.Server} server HapiJS Server instance. * @param {AuthScopeService} authScope AuthScopeService instance. * @param {Session} session Session instance. + * @param {AuthorizationMode} authorizationMode AuthorizationMode instance */ - constructor(server, authScope, session) { + constructor(server, authScope, session, authorizationMode) { this._server = server; this._authScope = authScope; this._session = session; + this._authorizationMode = authorizationMode; const config = this._server.config(); const authProviders = config.get('xpack.security.authProviders'); @@ -168,6 +170,8 @@ class Authenticator { } if (authenticationResult.succeeded()) { + // we have to do this here, as the auth scope's could be dependent on this + await this._authorizationMode.initialize(request); return AuthenticationResult.succeeded({ ...authenticationResult.user, // Complement user returned from the provider with scopes. @@ -269,10 +273,10 @@ class Authenticator { } } -export async function initAuthenticator(server) { +export async function initAuthenticator(server, authorizationMode) { const session = await Session.create(server); const authScope = new AuthScopeService(); - const authenticator = new Authenticator(server, authScope, session); + const authenticator = new Authenticator(server, authScope, session, authorizationMode); server.expose('authenticate', (request) => authenticator.authenticate(request)); server.expose('deauthenticate', (request) => authenticator.deauthenticate(request)); diff --git a/x-pack/plugins/security/server/lib/authorization/__snapshots__/check_privileges.test.js.snap b/x-pack/plugins/security/server/lib/authorization/__snapshots__/check_privileges.test.js.snap index 7609d57b702fa..634600a2549d6 100644 --- a/x-pack/plugins/security/server/lib/authorization/__snapshots__/check_privileges.test.js.snap +++ b/x-pack/plugins/security/server/lib/authorization/__snapshots__/check_privileges.test.js.snap @@ -1,13 +1,27 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`with a malformed Elasticsearch response throws a validation error when an extra index privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "index" fails because [child "default-index" fails because ["oopsAnExtraPrivilege" is not allowed]]]`; +exports[`#checkPrivilegesAtSpace throws error when checking for login and user has login but doesn't have version 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; -exports[`with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because ["oops-an-unexpected-privilege" is not allowed]]]]`; +exports[`#checkPrivilegesAtSpace with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because ["action:saved_objects/bar-type/get" is not allowed]]]]`; -exports[`with a malformed Elasticsearch response throws a validation error when index privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "index" fails because [child "default-index" fails because [child "read" fails because ["read" is required]]]]`; +exports[`#checkPrivilegesAtSpace with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because [child "action:saved_objects/foo-type/get" fails because ["action:saved_objects/foo-type/get" is required]]]]]`; -exports[`with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]`; +exports[`#checkPrivilegesAtSpaces throws error when Elasticsearch returns malformed response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]`; -exports[`with index privileges throws error if missing version privilege and has login privilege 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; +exports[`#checkPrivilegesAtSpaces throws error when checking for login and user has login but doesn't have version 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; -exports[`with no index privileges throws error if missing version privilege and has login privilege 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; +exports[`#checkPrivilegesAtSpaces with a malformed Elasticsearch response throws a validation error when an a space is missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]`; + +exports[`#checkPrivilegesAtSpaces with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]`; + +exports[`#checkPrivilegesAtSpaces with a malformed Elasticsearch response throws a validation error when an extra space is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because ["space:space_3" is not allowed]]]`; + +exports[`#checkPrivilegesAtSpaces with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]`; + +exports[`#checkPrivilegesGlobally throws error when Elasticsearch returns malformed response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]`; + +exports[`#checkPrivilegesGlobally throws error when checking for login and user has login but doesn't have version 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; + +exports[`#checkPrivilegesGlobally with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because ["action:saved_objects/bar-type/get" is not allowed]]]]`; + +exports[`#checkPrivilegesGlobally with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "action:saved_objects/foo-type/get" fails because ["action:saved_objects/foo-type/get" is required]]]]]`; diff --git a/x-pack/plugins/security/server/lib/authorization/__snapshots__/init.test.js.snap b/x-pack/plugins/security/server/lib/authorization/__snapshots__/init.test.js.snap deleted file mode 100644 index c65b0d2d6ae39..0000000000000 --- a/x-pack/plugins/security/server/lib/authorization/__snapshots__/init.test.js.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`deep freezes exposed service 1`] = `"Cannot delete property 'checkPrivilegesWithRequest' of #"`; - -exports[`deep freezes exposed service 2`] = `"Cannot add property foo, object is not extensible"`; - -exports[`deep freezes exposed service 3`] = `"Cannot assign to read only property 'login' of object '#'"`; - -exports[`deep freezes exposed service 4`] = `"Cannot assign to read only property 'application' of object '#'"`; diff --git a/x-pack/plugins/security/server/lib/authorization/__snapshots__/mode.test.js.snap b/x-pack/plugins/security/server/lib/authorization/__snapshots__/mode.test.js.snap new file mode 100644 index 0000000000000..66840335528ce --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/__snapshots__/mode.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#initialize can't be initialized twice for the same request 1`] = `"Authorization mode is already intitialized"`; diff --git a/x-pack/plugins/security/server/lib/authorization/__snapshots__/space_application_privileges_serializer.test.js.snap b/x-pack/plugins/security/server/lib/authorization/__snapshots__/space_application_privileges_serializer.test.js.snap new file mode 100644 index 0000000000000..0a943137989ec --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/__snapshots__/space_application_privileges_serializer.test.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#privilege #deserialize throws error if privilege doesn't start with space_ 1`] = `"Space privilege should have started with space_"`; + +exports[`#resource #deserialize throws error if resource doesn't start with space: 1`] = `"Resource should have started with space:"`; diff --git a/x-pack/plugins/security/server/lib/authorization/__snapshots__/validate_es_response.test.js.snap b/x-pack/plugins/security/server/lib/authorization/__snapshots__/validate_es_response.test.js.snap index 5e1e26a9023ae..226002545a378 100644 --- a/x-pack/plugins/security/server/lib/authorization/__snapshots__/validate_es_response.test.js.snap +++ b/x-pack/plugins/security/server/lib/authorization/__snapshots__/validate_es_response.test.js.snap @@ -4,40 +4,18 @@ exports[`validateEsPrivilegeResponse fails validation when an action is malforme exports[`validateEsPrivilegeResponse fails validation when an action is missing in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [child \\"action2\\" fails because [\\"action2\\" is required]]]]"`; +exports[`validateEsPrivilegeResponse fails validation when an expected resource property is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"bar-resource\\" fails because [\\"bar-resource\\" is required]]]"`; + exports[`validateEsPrivilegeResponse fails validation when an extra action is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [\\"action4\\" is not allowed]]]"`; exports[`validateEsPrivilegeResponse fails validation when an extra application is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [\\"otherApplication\\" is not allowed]"`; -exports[`validateEsPrivilegeResponse fails validation when an unexpected resource property is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [\\"foo-resource\\" is required]]]"`; +exports[`validateEsPrivilegeResponse fails validation when an unexpected resource property is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"bar-resource\\" fails because [\\"bar-resource\\" is required]]]"`; exports[`validateEsPrivilegeResponse fails validation when the "application" property is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [\\"application\\" is required]"`; -exports[`validateEsPrivilegeResponse fails validation when the expected resource property is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [\\"foo-resource\\" is required]]]"`; - exports[`validateEsPrivilegeResponse fails validation when the requested application is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [\\"foo-application\\" is required]]"`; exports[`validateEsPrivilegeResponse fails validation when the resource propertry is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [\\"foo-resource\\" must be an object]]]"`; -exports[`validateEsPrivilegeResponse legacy should fail if the create index privilege is malformed 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"create\\" fails because [\\"create\\" must be a boolean]]]"`; - -exports[`validateEsPrivilegeResponse legacy should fail if the create index privilege is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"create\\" fails because [\\"create\\" is required]]]"`; - -exports[`validateEsPrivilegeResponse legacy should fail if the delete index privilege is malformed 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"delete\\" fails because [\\"delete\\" must be a boolean]]]"`; - -exports[`validateEsPrivilegeResponse legacy should fail if the delete index privilege is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"delete\\" fails because [\\"delete\\" is required]]]"`; - -exports[`validateEsPrivilegeResponse legacy should fail if the index privilege response contains an extra privilege 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [\\"foo-permission\\" is not allowed]]"`; - -exports[`validateEsPrivilegeResponse legacy should fail if the index privilege response returns an extra index 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [\\"anotherIndex\\" is not allowed]"`; - -exports[`validateEsPrivilegeResponse legacy should fail if the index property is missing 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [\\"index\\" is required]"`; - -exports[`validateEsPrivilegeResponse legacy should fail if the kibana index is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [\\".kibana\\" is required]]"`; - -exports[`validateEsPrivilegeResponse legacy should fail if the read index privilege is malformed 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"read\\" fails because [\\"read\\" must be a boolean]]]"`; - -exports[`validateEsPrivilegeResponse legacy should fail if the read index privilege is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"read\\" fails because [\\"read\\" is required]]]"`; - -exports[`validateEsPrivilegeResponse legacy should fail if the view_index_metadata index privilege is malformed 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"view_index_metadata\\" fails because [\\"view_index_metadata\\" must be a boolean]]]"`; - -exports[`validateEsPrivilegeResponse legacy should fail if the view_index_metadata index privilege is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"view_index_metadata\\" fails because [\\"view_index_metadata\\" is required]]]"`; +exports[`validateEsPrivilegeResponse fails validation when there are no resource properties in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [\\"foo-resource\\" is required]]]"`; diff --git a/x-pack/plugins/security/server/lib/authorization/actions.js b/x-pack/plugins/security/server/lib/authorization/actions.js index 432698a003cb3..e47e1edd5d4c4 100644 --- a/x-pack/plugins/security/server/lib/authorization/actions.js +++ b/x-pack/plugins/security/server/lib/authorization/actions.js @@ -22,5 +22,6 @@ export function actionsFactory(config) { }, login: `action:login`, version: `version:${kibanaVersion}`, + manageSpaces: 'action:manage_spaces/*', }; } diff --git a/x-pack/plugins/security/server/lib/authorization/actions.test.js b/x-pack/plugins/security/server/lib/authorization/actions.test.js index 17834438e1781..9ae2265557d21 100644 --- a/x-pack/plugins/security/server/lib/authorization/actions.test.js +++ b/x-pack/plugins/security/server/lib/authorization/actions.test.js @@ -66,4 +66,14 @@ describe('#getSavedObjectAction()', () => { expect(() => actions.getSavedObjectAction('saved-object-type', action)).toThrowErrorMatchingSnapshot(); }); }); + + describe('#manageSpaces', () => { + test('returns action:manage_spaces/*', () => { + const mockConfig = createMockConfig(); + + const actions = actionsFactory(mockConfig); + + expect(actions.manageSpaces).toEqual('action:manage_spaces/*'); + }); + }); }); diff --git a/x-pack/plugins/security/server/lib/authorization/check_privileges.js b/x-pack/plugins/security/server/lib/authorization/check_privileges.js index b12658708f2d3..bdd9781f97771 100644 --- a/x-pack/plugins/security/server/lib/authorization/check_privileges.js +++ b/x-pack/plugins/security/server/lib/authorization/check_privileges.js @@ -4,94 +4,85 @@ * you may not use this file except in compliance with the Elastic License. */ -import { uniq } from 'lodash'; -import { ALL_RESOURCE } from '../../../common/constants'; -import { buildLegacyIndexPrivileges } from './privileges'; +import { pick, transform, uniq } from 'lodash'; +import { GLOBAL_RESOURCE } from '../../../common/constants'; +import { spaceApplicationPrivilegesSerializer } from './space_application_privileges_serializer'; import { validateEsPrivilegeResponse } from './validate_es_response'; -export const CHECK_PRIVILEGES_RESULT = { - UNAUTHORIZED: Symbol('Unauthorized'), - AUTHORIZED: Symbol('Authorized'), - LEGACY: Symbol('Legacy'), -}; - -export function checkPrivilegesWithRequestFactory(shieldClient, config, actions, application) { +export function checkPrivilegesWithRequestFactory(actions, application, shieldClient) { const { callWithRequest } = shieldClient; - const kibanaIndex = config.get('kibana.index'); - const hasIncompatibileVersion = (applicationPrivilegesResponse) => { - return !applicationPrivilegesResponse[actions.version] && applicationPrivilegesResponse[actions.login]; - }; - - const hasAllApplicationPrivileges = (applicationPrivilegesResponse) => { - return Object.values(applicationPrivilegesResponse).every(val => val === true); - }; - - const hasNoApplicationPrivileges = (applicationPrivilegesResponse) => { - return Object.values(applicationPrivilegesResponse).every(val => val === false); - }; - - const isLegacyFallbackEnabled = () => { - return config.get('xpack.security.authorization.legacyFallback.enabled'); - }; - - const hasLegacyPrivileges = (indexPrivilegesResponse) => { - return Object.values(indexPrivilegesResponse).includes(true); - }; - - const determineResult = (applicationPrivilegesResponse, indexPrivilegesResponse) => { - if (hasAllApplicationPrivileges(applicationPrivilegesResponse)) { - return CHECK_PRIVILEGES_RESULT.AUTHORIZED; - } - - if ( - isLegacyFallbackEnabled() && - hasNoApplicationPrivileges(applicationPrivilegesResponse) && - hasLegacyPrivileges(indexPrivilegesResponse) - ) { - return CHECK_PRIVILEGES_RESULT.LEGACY; - } - - return CHECK_PRIVILEGES_RESULT.UNAUTHORIZED; + return Object.values(applicationPrivilegesResponse).some(resource => !resource[actions.version] && resource[actions.login]); }; return function checkPrivilegesWithRequest(request) { - return async function checkPrivileges(privileges) { + const checkPrivilegesAtResources = async (resources, privilegeOrPrivileges) => { + const privileges = Array.isArray(privilegeOrPrivileges) ? privilegeOrPrivileges : [privilegeOrPrivileges]; const allApplicationPrivileges = uniq([actions.version, actions.login, ...privileges]); + const hasPrivilegesResponse = await callWithRequest(request, 'shield.hasPrivileges', { body: { applications: [{ application, - resources: [ALL_RESOURCE], + resources, privileges: allApplicationPrivileges }], - index: [{ - names: [kibanaIndex], - privileges: buildLegacyIndexPrivileges() - }], } }); - validateEsPrivilegeResponse(hasPrivilegesResponse, application, allApplicationPrivileges, [ALL_RESOURCE], kibanaIndex); + validateEsPrivilegeResponse(hasPrivilegesResponse, application, allApplicationPrivileges, resources); - const applicationPrivilegesResponse = hasPrivilegesResponse.application[application][ALL_RESOURCE]; - const indexPrivilegesResponse = hasPrivilegesResponse.index[kibanaIndex]; + const applicationPrivilegesResponse = hasPrivilegesResponse.application[application]; if (hasIncompatibileVersion(applicationPrivilegesResponse)) { throw new Error('Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.'); } return { - result: determineResult(applicationPrivilegesResponse, indexPrivilegesResponse), + hasAllRequested: hasPrivilegesResponse.has_all_requested, username: hasPrivilegesResponse.username, + // we need to filter out the non requested privileges from the response + resourcePrivileges: transform(applicationPrivilegesResponse, (result, value, key) => { + result[key] = pick(value, privileges); + }), + }; + }; - // we only return missing privileges that they're specifically checking for - missing: Object.keys(applicationPrivilegesResponse) - .filter(privilege => privileges.includes(privilege)) - .filter(privilege => !applicationPrivilegesResponse[privilege]) + const checkPrivilegesAtResource = async (resource, privilegeOrPrivileges) => { + const { hasAllRequested, username, resourcePrivileges } = await checkPrivilegesAtResources([resource], privilegeOrPrivileges); + return { + hasAllRequested, + username, + privileges: resourcePrivileges[resource], }; }; + + return { + // TODO: checkPrivileges.atResources isn't necessary once we have the ES API to list all privileges + // this should be removed when we switch to this API, and is not covered by unit tests currently + atResources: checkPrivilegesAtResources, + async atSpace(spaceId, privilegeOrPrivileges) { + const spaceResource = spaceApplicationPrivilegesSerializer.resource.serialize(spaceId); + return await checkPrivilegesAtResource(spaceResource, privilegeOrPrivileges); + }, + async atSpaces(spaceIds, privilegeOrPrivileges) { + const spaceResources = spaceIds.map(spaceId => spaceApplicationPrivilegesSerializer.resource.serialize(spaceId)); + const { hasAllRequested, username, resourcePrivileges } = await checkPrivilegesAtResources(spaceResources, privilegeOrPrivileges); + return { + hasAllRequested, + username, + // we need to turn the resource responses back into the space ids + spacePrivileges: transform(resourcePrivileges, (result, value, key) => { + result[spaceApplicationPrivilegesSerializer.resource.deserialize(key)] = value; + }), + }; + + }, + async globally(privilegeOrPrivileges) { + return await checkPrivilegesAtResource(GLOBAL_RESOURCE, privilegeOrPrivileges); + }, + }; }; } diff --git a/x-pack/plugins/security/server/lib/authorization/check_privileges.test.js b/x-pack/plugins/security/server/lib/authorization/check_privileges.test.js index 510ec3e4852b7..f74528d4fd20d 100644 --- a/x-pack/plugins/security/server/lib/authorization/check_privileges.test.js +++ b/x-pack/plugins/security/server/lib/authorization/check_privileges.test.js @@ -5,37 +5,17 @@ */ import { uniq } from 'lodash'; -import { checkPrivilegesWithRequestFactory, CHECK_PRIVILEGES_RESULT } from './check_privileges'; - -import { ALL_RESOURCE } from '../../../common/constants'; +import { checkPrivilegesWithRequestFactory } from './check_privileges'; +import { GLOBAL_RESOURCE } from '../../../common/constants'; const application = 'kibana-our_application'; -const defaultVersion = 'default-version'; -const defaultKibanaIndex = 'default-index'; -const savedObjectTypes = ['foo-type', 'bar-type']; const mockActions = { login: 'mock-action:login', version: 'mock-action:version', }; -const createMockConfig = (settings = {}) => { - const mockConfig = { - get: jest.fn() - }; - - const defaultSettings = { - 'pkg.version': defaultVersion, - 'kibana.index': defaultKibanaIndex, - 'xpack.security.authorization.legacyFallback.enabled': true, - }; - - mockConfig.get.mockImplementation(key => { - return key in settings ? settings[key] : defaultSettings[key]; - }); - - return mockConfig; -}; +const savedObjectTypes = ['foo-type', 'bar-type']; const createMockShieldClient = (response) => { const mockCallWithRequest = jest.fn(); @@ -47,424 +27,841 @@ const createMockShieldClient = (response) => { }; }; -const checkPrivilegesTest = ( - description, { - settings, - privileges, - applicationPrivilegesResponse, - indexPrivilegesResponse, +describe('#checkPrivilegesAtSpace', () => { + const checkPrivilegesAtSpaceTest = (description, { + spaceId, + privilegeOrPrivileges, + esHasPrivilegesResponse, expectedResult, - expectErrorThrown, + expectErrorThrown }) => { + test(description, async () => { + const mockShieldClient = createMockShieldClient(esHasPrivilegesResponse); + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory(mockActions, application, mockShieldClient); + const request = Symbol(); + const checkPrivileges = checkPrivilegesWithRequest(request); + + let actualResult; + let errorThrown = null; + try { + actualResult = await checkPrivileges.atSpace(spaceId, privilegeOrPrivileges); + } catch (err) { + errorThrown = err; + } - test(description, async () => { - const username = 'foo-username'; - const mockConfig = createMockConfig(settings); - const mockShieldClient = createMockShieldClient({ - username, - application: { - [application]: { - [ALL_RESOURCE]: applicationPrivilegesResponse + expect(mockShieldClient.callWithRequest).toHaveBeenCalledWith(request, 'shield.hasPrivileges', { + body: { + applications: [{ + application, + resources: [`space:${spaceId}`], + privileges: uniq([ + mockActions.version, + mockActions.login, + ...Array.isArray(privilegeOrPrivileges) ? privilegeOrPrivileges : [privilegeOrPrivileges], + ]) + }] } - }, - index: { - [defaultKibanaIndex]: indexPrivilegesResponse - }, - }); + }); - const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory(mockShieldClient, mockConfig, mockActions, application); - const request = Symbol(); - const checkPrivileges = checkPrivilegesWithRequest(request); - - let actualResult; - let errorThrown = null; - try { - actualResult = await checkPrivileges(privileges); - } catch (err) { - errorThrown = err; - } - - - expect(mockShieldClient.callWithRequest).toHaveBeenCalledWith(request, 'shield.hasPrivileges', { - body: { - applications: [{ - application, - resources: [ALL_RESOURCE], - privileges: uniq([ - mockActions.version, mockActions.login, ...privileges - ]) - }], - index: [{ - names: [defaultKibanaIndex], - privileges: ['create', 'delete', 'read', 'view_index_metadata'] - }], + if (expectedResult) { + expect(errorThrown).toBeNull(); + expect(actualResult).toEqual(expectedResult); } - }); - - if (expectedResult) { - expect(errorThrown).toBeNull(); - expect(actualResult).toEqual(expectedResult); - } - - if (expectErrorThrown) { - expect(errorThrown).toMatchSnapshot(); - } - }); -}; -describe(`with no index privileges`, () => { - const indexPrivilegesResponse = { - create: false, - delete: false, - read: false, - view_index_metadata: false, + if (expectErrorThrown) { + expect(errorThrown).toMatchSnapshot(); + } + }); }; - checkPrivilegesTest('returns authorized if they have all application privileges', { - username: 'foo-username', - privileges: [ - `action:saved_objects/${savedObjectTypes[0]}/get` - ], - applicationPrivilegesResponse: { - [mockActions.version]: true, - [mockActions.login]: true, - [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + checkPrivilegesAtSpaceTest('successful when checking for login and user has login', { + spaceId: 'space_1', + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + } + } + } }, - indexPrivilegesResponse, expectedResult: { - result: CHECK_PRIVILEGES_RESULT.AUTHORIZED, + hasAllRequested: true, username: 'foo-username', - missing: [], - } + privileges: { + [mockActions.login]: true + } + }, }); - checkPrivilegesTest('returns unauthorized and missing application action when checking missing application action', { - username: 'foo-username', - privileges: [ - `action:saved_objects/${savedObjectTypes[0]}/get`, - `action:saved_objects/${savedObjectTypes[0]}/create`, - ], - applicationPrivilegesResponse: { - [mockActions.version]: true, - [mockActions.login]: true, - [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, - [`action:saved_objects/${savedObjectTypes[0]}/create`]: false, + checkPrivilegesAtSpaceTest(`failure when checking for login and user doesn't have login`, { + spaceId: 'space_1', + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: false, + [mockActions.version]: true, + } + } + } }, - indexPrivilegesResponse, expectedResult: { - result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED, + hasAllRequested: false, username: 'foo-username', - missing: [`action:saved_objects/${savedObjectTypes[0]}/create`], - } + privileges: { + [mockActions.login]: false + } + }, + }); + + checkPrivilegesAtSpaceTest(`throws error when checking for login and user has login but doesn't have version`, { + spaceId: 'space_1', + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: false, + } + } + } + }, + expectErrorThrown: true, }); - checkPrivilegesTest('returns unauthorized and missing login when checking missing login action', { - username: 'foo-username', - privileges: [ - mockActions.login - ], - applicationPrivilegesResponse: { - [mockActions.login]: false, - [mockActions.version]: false, + checkPrivilegesAtSpaceTest(`successful when checking for two actions and the user has both`, { + spaceId: 'space_1', + privilegeOrPrivileges: [`action:saved_objects/${savedObjectTypes[0]}/get`, `action:saved_objects/${savedObjectTypes[1]}/get`], + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + } + } + } }, - indexPrivilegesResponse, expectedResult: { - result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED, + hasAllRequested: true, username: 'foo-username', - missing: [mockActions.login], - } + privileges: { + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + } + }, }); - checkPrivilegesTest('returns unauthorized and missing version if checking missing version action', { - username: 'foo-username', - privileges: [ - mockActions.version - ], - applicationPrivilegesResponse: { - [mockActions.login]: false, - [mockActions.version]: false, + checkPrivilegesAtSpaceTest(`failure when checking for two actions and the user has only one`, { + spaceId: 'space_1', + privilegeOrPrivileges: [`action:saved_objects/${savedObjectTypes[0]}/get`, `action:saved_objects/${savedObjectTypes[1]}/get`], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + } + } + } }, - indexPrivilegesResponse, expectedResult: { - result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED, + hasAllRequested: false, username: 'foo-username', - missing: [mockActions.version], - } + privileges: { + [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + } + }, }); - checkPrivilegesTest('throws error if missing version privilege and has login privilege', { - username: 'foo-username', - privileges: [ - `action:saved_objects/${savedObjectTypes[0]}/get` - ], - applicationPrivilegesResponse: { - [mockActions.login]: true, - [mockActions.version]: false, - [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, - }, - indexPrivilegesResponse, - expectErrorThrown: true + describe('with a malformed Elasticsearch response', () => { + checkPrivilegesAtSpaceTest(`throws a validation error when an extra privilege is present in the response`, { + spaceId: 'space_1', + privilegeOrPrivileges: [`action:saved_objects/${savedObjectTypes[0]}/get`], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + } + } + } + }, + expectErrorThrown: true, + }); + + checkPrivilegesAtSpaceTest(`throws a validation error when privileges are missing in the response`, { + spaceId: 'space_1', + privilegeOrPrivileges: [`action:saved_objects/${savedObjectTypes[0]}/get`], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + } + } + } + }, + expectErrorThrown: true, + }); }); }); -describe(`with index privileges`, () => { - const indexPrivilegesResponse = { - create: true, - delete: true, - read: true, - view_index_metadata: true, +describe('#checkPrivilegesAtSpaces', () => { + const checkPrivilegesAtSpacesTest = (description, { + spaceIds, + privilegeOrPrivileges, + esHasPrivilegesResponse, + expectedResult, + expectErrorThrown + }) => { + test(description, async () => { + const mockShieldClient = createMockShieldClient(esHasPrivilegesResponse); + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory(mockActions, application, mockShieldClient); + const request = Symbol(); + const checkPrivileges = checkPrivilegesWithRequest(request); + + let actualResult; + let errorThrown = null; + try { + actualResult = await checkPrivileges.atSpaces(spaceIds, privilegeOrPrivileges); + } catch (err) { + errorThrown = err; + } + + expect(mockShieldClient.callWithRequest).toHaveBeenCalledWith(request, 'shield.hasPrivileges', { + body: { + applications: [{ + application, + resources: spaceIds.map(spaceId => `space:${spaceId}`), + privileges: uniq([ + mockActions.version, + mockActions.login, + ...Array.isArray(privilegeOrPrivileges) ? privilegeOrPrivileges : [privilegeOrPrivileges], + ]) + }] + } + }); + + if (expectedResult) { + expect(errorThrown).toBeNull(); + expect(actualResult).toEqual(expectedResult); + } + + if (expectErrorThrown) { + expect(errorThrown).toMatchSnapshot(); + } + }); }; - checkPrivilegesTest('returns authorized if they have all application privileges', { - username: 'foo-username', - privileges: [ - `action:saved_objects/${savedObjectTypes[0]}/get` - ], - applicationPrivilegesResponse: { - [mockActions.version]: true, - [mockActions.login]: true, - [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + checkPrivilegesAtSpacesTest('successful when checking for login and user has login at both spaces', { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + } + } + } }, - indexPrivilegesResponse, expectedResult: { - result: CHECK_PRIVILEGES_RESULT.AUTHORIZED, + hasAllRequested: true, username: 'foo-username', - missing: [], - } + spacePrivileges: { + space_1: { + [mockActions.login]: true + }, + space_2: { + [mockActions.login]: true + }, + } + }, }); - checkPrivilegesTest('returns unauthorized and missing application action when checking missing application action', { - username: 'foo-username', - privileges: [ - `action:saved_objects/${savedObjectTypes[0]}/get`, - `action:saved_objects/${savedObjectTypes[0]}/create`, - ], - applicationPrivilegesResponse: { - [mockActions.version]: true, - [mockActions.login]: true, - [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, - [`action:saved_objects/${savedObjectTypes[0]}/create`]: false, + checkPrivilegesAtSpacesTest('failure when checking for login and user has login at only one space', { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + 'space:space_2': { + [mockActions.login]: false, + [mockActions.version]: true, + } + } + } }, - indexPrivilegesResponse, expectedResult: { - result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED, + hasAllRequested: false, username: 'foo-username', - missing: [`action:saved_objects/${savedObjectTypes[0]}/create`], - } + spacePrivileges: { + space_1: { + [mockActions.login]: true + }, + space_2: { + [mockActions.login]: false + }, + } + }, }); - checkPrivilegesTest('returns legacy and missing login when checking missing login action and fallback is enabled', { - username: 'foo-username', - privileges: [ - mockActions.login - ], - applicationPrivilegesResponse: { - [mockActions.login]: false, - [mockActions.version]: false, - }, - indexPrivilegesResponse, - expectedResult: { - result: CHECK_PRIVILEGES_RESULT.LEGACY, + checkPrivilegesAtSpacesTest(`throws error when checking for login and user has login but doesn't have version`, { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: false, username: 'foo-username', - missing: [mockActions.login], - } + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: false, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: false, + } + } + } + }, + expectErrorThrown: true, }); - checkPrivilegesTest('returns unauthorized and missing login when checking missing login action and fallback is disabled', { - settings: { - 'xpack.security.authorization.legacyFallback.enabled': false, + checkPrivilegesAtSpacesTest(`throws error when Elasticsearch returns malformed response`, { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [`action:saved_objects/${savedObjectTypes[0]}/get`, `action:saved_objects/${savedObjectTypes[1]}/get`], + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + }, + 'space:space_2': { + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + } + } + } }, - username: 'foo-username', - privileges: [ - mockActions.login - ], - applicationPrivilegesResponse: { - [mockActions.login]: false, - [mockActions.version]: false, + expectErrorThrown: true, + }); + + checkPrivilegesAtSpacesTest(`successful when checking for two actions at two spaces and user has it all`, { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [`action:saved_objects/${savedObjectTypes[0]}/get`, `action:saved_objects/${savedObjectTypes[1]}/get`], + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + } + } + } }, - indexPrivilegesResponse, expectedResult: { - result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED, + hasAllRequested: true, username: 'foo-username', - missing: [mockActions.login], - } + spacePrivileges: { + space_1: { + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + }, + space_2: { + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + } + } + }, }); - checkPrivilegesTest('returns legacy and missing version if checking missing version action and fallback is enabled', { - username: 'foo-username', - privileges: [ - mockActions.version - ], - applicationPrivilegesResponse: { - [mockActions.login]: false, - [mockActions.version]: false, + checkPrivilegesAtSpacesTest(`failure when checking for two actions at two spaces and user has one action at one space`, { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [`action:saved_objects/${savedObjectTypes[0]}/get`, `action:saved_objects/${savedObjectTypes[1]}/get`], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: false, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: false, + } + } + } }, - indexPrivilegesResponse, expectedResult: { - result: CHECK_PRIVILEGES_RESULT.LEGACY, + hasAllRequested: false, username: 'foo-username', - missing: [mockActions.version], - } + spacePrivileges: { + space_1: { + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: false, + }, + space_2: { + [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: false, + } + } + }, }); - checkPrivilegesTest('returns unauthorized and missing version if checking missing version action and fallback is disabled', { - settings: { - 'xpack.security.authorization.legacyFallback.enabled': false, - }, - username: 'foo-username', - privileges: [ - mockActions.version - ], - applicationPrivilegesResponse: { - [mockActions.login]: false, - [mockActions.version]: false, + checkPrivilegesAtSpacesTest(`failure when checking for two actions at two spaces and user has two actions at one space`, { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [`action:saved_objects/${savedObjectTypes[0]}/get`, `action:saved_objects/${savedObjectTypes[1]}/get`], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: false, + } + } + } }, - indexPrivilegesResponse, expectedResult: { - result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED, + hasAllRequested: false, username: 'foo-username', - missing: [mockActions.version], - } - }); - - checkPrivilegesTest('throws error if missing version privilege and has login privilege', { - username: 'foo-username', - privileges: [ - `action:saved_objects/${savedObjectTypes[0]}/get` - ], - applicationPrivilegesResponse: { - [mockActions.login]: true, - [mockActions.version]: false, - [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + spacePrivileges: { + space_1: { + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + }, + space_2: { + [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: false, + } + } }, - indexPrivilegesResponse, - expectErrorThrown: true }); -}); -describe('with no application privileges', () => { - ['create', 'delete', 'read', 'view_index_metadata'].forEach(indexPrivilege => { - checkPrivilegesTest(`returns legacy if they have ${indexPrivilege} privilege on the kibana index and fallback is enabled`, { - username: 'foo-username', - privileges: [ - `action:saved_objects/${savedObjectTypes[0]}/get` - ], - applicationPrivilegesResponse: { - [mockActions.version]: false, - [mockActions.login]: false, - [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, - }, - indexPrivilegesResponse: { - create: false, - delete: false, - read: false, - view_index_metadata: false, - [indexPrivilege]: true + checkPrivilegesAtSpacesTest( + `failure when checking for two actions at two spaces and user has two actions at one space & one action at the other`, { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [`action:saved_objects/${savedObjectTypes[0]}/get`, `action:saved_objects/${savedObjectTypes[1]}/get`], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: false, + } + } + } }, expectedResult: { - result: CHECK_PRIVILEGES_RESULT.LEGACY, + hasAllRequested: false, username: 'foo-username', - missing: [`action:saved_objects/${savedObjectTypes[0]}/get`], - } + spacePrivileges: { + space_1: { + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + }, + space_2: { + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: false, + } + } + }, }); - checkPrivilegesTest(`returns unauthorized if they have ${indexPrivilege} privilege on the kibana index and fallback is disabled`, { - settings: { - 'xpack.security.authorization.legacyFallback.enabled': false, + describe('with a malformed Elasticsearch response', () => { + checkPrivilegesAtSpacesTest(`throws a validation error when an extra privilege is present in the response`, { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [`action:saved_objects/${savedObjectTypes[0]}/get`], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + }, + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + } + } + } }, - username: 'foo-username', - privileges: [ - `action:saved_objects/${savedObjectTypes[0]}/get` - ], - applicationPrivilegesResponse: { - [mockActions.version]: false, - [mockActions.login]: false, - [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + expectErrorThrown: true, + }); + + checkPrivilegesAtSpacesTest(`throws a validation error when privileges are missing in the response`, { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [`action:saved_objects/${savedObjectTypes[0]}/get`], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + } + } + } }, - indexPrivilegesResponse: { - create: false, - delete: false, - read: false, - view_index_metadata: false, - [indexPrivilege]: true + expectErrorThrown: true, + }); + + checkPrivilegesAtSpacesTest(`throws a validation error when an extra space is present in the response`, { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [`action:saved_objects/${savedObjectTypes[0]}/get`], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + }, + 'space:space_3': { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + }, + } + } }, - expectedResult: { - result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED, + expectErrorThrown: true, + }); + + checkPrivilegesAtSpacesTest(`throws a validation error when an a space is missing in the response`, { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [`action:saved_objects/${savedObjectTypes[0]}/get`], + esHasPrivilegesResponse: { + has_all_requested: false, username: 'foo-username', - missing: [`action:saved_objects/${savedObjectTypes[0]}/get`], - } + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + } + } + } + }, + expectErrorThrown: true, }); }); }); -describe('with a malformed Elasticsearch response', () => { - const indexPrivilegesResponse = { - create: true, - delete: true, - read: true, - view_index_metadata: true, +describe('#checkPrivilegesGlobally', () => { + const checkPrivilegesGloballyTest = (description, { + privilegeOrPrivileges, + esHasPrivilegesResponse, + expectedResult, + expectErrorThrown + }) => { + test(description, async () => { + const mockShieldClient = createMockShieldClient(esHasPrivilegesResponse); + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory(mockActions, application, mockShieldClient); + const request = Symbol(); + const checkPrivileges = checkPrivilegesWithRequest(request); + + let actualResult; + let errorThrown = null; + try { + actualResult = await checkPrivileges.globally(privilegeOrPrivileges); + } catch (err) { + errorThrown = err; + } + + expect(mockShieldClient.callWithRequest).toHaveBeenCalledWith(request, 'shield.hasPrivileges', { + body: { + applications: [{ + application, + resources: [GLOBAL_RESOURCE], + privileges: uniq([ + mockActions.version, + mockActions.login, + ...Array.isArray(privilegeOrPrivileges) ? privilegeOrPrivileges : [privilegeOrPrivileges], + ]) + }] + } + }); + + if (expectedResult) { + expect(errorThrown).toBeNull(); + expect(actualResult).toEqual(expectedResult); + } + + if (expectErrorThrown) { + expect(errorThrown).toMatchSnapshot(); + } + }); }; - checkPrivilegesTest('throws a validation error when an extra privilege is present in the response', { - username: 'foo-username', - privileges: [ - `action:saved_objects/${savedObjectTypes[0]}/get`, - ], - applicationPrivilegesResponse: { - [mockActions.version]: true, - [mockActions.login]: true, - [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, - ['oops-an-unexpected-privilege']: true, + checkPrivilegesGloballyTest('successful when checking for login and user has login', { + spaceId: 'space_1', + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + } + } + } + }, + expectedResult: { + hasAllRequested: true, + username: 'foo-username', + privileges: { + [mockActions.login]: true + } + }, + }); + + checkPrivilegesGloballyTest(`failure when checking for login and user doesn't have login`, { + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: false, + [mockActions.version]: true, + } + } + } + }, + expectedResult: { + hasAllRequested: false, + username: 'foo-username', + privileges: { + [mockActions.login]: false + } + }, + }); + + checkPrivilegesGloballyTest(`throws error when checking for login and user has login but doesn't have version`, { + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: false, + } + } + } }, - indexPrivilegesResponse, expectErrorThrown: true, }); - checkPrivilegesTest('throws a validation error when privileges are missing in the response', { - username: 'foo-username', - privileges: [ - `action:saved_objects/${savedObjectTypes[0]}/get`, - ], - applicationPrivilegesResponse: { - [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + checkPrivilegesGloballyTest(`throws error when Elasticsearch returns malformed response`, { + privilegeOrPrivileges: [`action:saved_objects/${savedObjectTypes[0]}/get`, `action:saved_objects/${savedObjectTypes[1]}/get`], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + } + } + } }, - indexPrivilegesResponse, expectErrorThrown: true, }); - checkPrivilegesTest('throws a validation error when an extra index privilege is present in the response', { - username: 'foo-username', - privileges: [ - `action:saved_objects/${savedObjectTypes[0]}/get`, - ], - applicationPrivilegesResponse: { - [mockActions.version]: true, - [mockActions.login]: true, - [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + checkPrivilegesGloballyTest(`successful when checking for two actions and the user has both`, { + privilegeOrPrivileges: [`action:saved_objects/${savedObjectTypes[0]}/get`, `action:saved_objects/${savedObjectTypes[1]}/get`], + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + } + } + } }, - indexPrivilegesResponse: { - ...indexPrivilegesResponse, - oopsAnExtraPrivilege: true, + expectedResult: { + hasAllRequested: true, + username: 'foo-username', + privileges: { + [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + } }, - expectErrorThrown: true, }); - const missingIndexPrivileges = { - ...indexPrivilegesResponse - }; - delete missingIndexPrivileges.read; - - checkPrivilegesTest('throws a validation error when index privileges are missing in the response', { - username: 'foo-username', - privileges: [ - `action:saved_objects/${savedObjectTypes[0]}/get`, - ], - applicationPrivilegesResponse: { - [mockActions.version]: true, - [mockActions.login]: true, - [`action:saved_objects/${savedObjectTypes[0]}/get`]: true, + checkPrivilegesGloballyTest(`failure when checking for two actions and the user has only one`, { + privilegeOrPrivileges: [`action:saved_objects/${savedObjectTypes[0]}/get`, `action:saved_objects/${savedObjectTypes[1]}/get`], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + } + } + } }, - indexPrivilegesResponse: missingIndexPrivileges, - expectErrorThrown: true, + expectedResult: { + hasAllRequested: false, + username: 'foo-username', + privileges: { + [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + } + }, + }); + + describe('with a malformed Elasticsearch response', () => { + checkPrivilegesGloballyTest(`throws a validation error when an extra privilege is present in the response`, { + privilegeOrPrivileges: [`action:saved_objects/${savedObjectTypes[0]}/get`], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + [`action:saved_objects/${savedObjectTypes[0]}/get`]: false, + [`action:saved_objects/${savedObjectTypes[1]}/get`]: true, + } + } + } + }, + expectErrorThrown: true, + }); + + checkPrivilegesGloballyTest(`throws a validation error when privileges are missing in the response`, { + privilegeOrPrivileges: [`action:saved_objects/${savedObjectTypes[0]}/get`], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + } + } + } + }, + expectErrorThrown: true, + }); }); }); diff --git a/x-pack/plugins/security/server/lib/authorization/index.js b/x-pack/plugins/security/server/lib/authorization/index.js index e0029a3caeafd..1fb754202832f 100644 --- a/x-pack/plugins/security/server/lib/authorization/index.js +++ b/x-pack/plugins/security/server/lib/authorization/index.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { CHECK_PRIVILEGES_RESULT } from './check_privileges'; export { registerPrivilegesWithCluster } from './register_privileges_with_cluster'; export { buildPrivilegeMap } from './privileges'; -export { initAuthorizationService } from './init'; +export { createAuthorizationService } from './service'; +export { spaceApplicationPrivilegesSerializer } from './space_application_privileges_serializer'; diff --git a/x-pack/plugins/security/server/lib/authorization/mode.js b/x-pack/plugins/security/server/lib/authorization/mode.js new file mode 100644 index 0000000000000..37800ca4e3911 --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/mode.js @@ -0,0 +1,81 @@ +/* + * 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 { GLOBAL_RESOURCE } from '../../../common/constants'; +import { spaceApplicationPrivilegesSerializer } from './space_application_privileges_serializer'; + +const hasAnyPrivileges = privileges => { + return Object.values(privileges).some(hasPrivilege => hasPrivilege === true); +}; + +const hasAnyResourcePrivileges = resourcePrivileges => { + return Object.values(resourcePrivileges).some(resource => hasAnyPrivileges(resource)); +}; + +export function authorizationModeFactory( + actions, + checkPrivilegesWithRequest, + config, + plugins, + savedObjects, + xpackInfoFeature +) { + const useRbacForRequestCache = new WeakMap(); + + // TODO: This logic will change once we have the ES API to list all privileges + // and is not covered by unit tests currently + const shouldUseRbacForRequest = async (request) => { + if (!config.get('xpack.security.authorization.legacyFallback.enabled')) { + return true; + } + + const adminCluster = plugins.elasticsearch.getCluster('admin'); + const { callWithInternalUser } = adminCluster; + + const internalSavedObjectsRepository = savedObjects.getSavedObjectsRepository( + callWithInternalUser + ); + + const checkPrivileges = checkPrivilegesWithRequest(request); + if (!plugins.spaces) { + const { privileges } = await checkPrivileges.globally(actions.login); + return hasAnyPrivileges(privileges); + } + + const { saved_objects: spaceSavedObjects } = await internalSavedObjectsRepository.find({ type: 'space' }); + const spaceResources = spaceSavedObjects.map(space => spaceApplicationPrivilegesSerializer.resource.serialize(space.id)); + const allResources = [GLOBAL_RESOURCE, ...spaceResources]; + const { resourcePrivileges } = await checkPrivileges.atResources(allResources, actions.login); + return hasAnyResourcePrivileges(resourcePrivileges); + }; + + const isRbacEnabled = () => xpackInfoFeature.getLicenseCheckResults().allowRbac; + + return { + async initialize(request) { + if (useRbacForRequestCache.has(request)) { + throw new Error('Authorization mode is already intitialized'); + } + + if (!isRbacEnabled()) { + useRbacForRequestCache.set(request, true); + return; + } + + const result = await shouldUseRbacForRequest(request); + useRbacForRequestCache.set(request, result); + }, + + useRbacForRequest(request) { + // the following can happen when the user isn't authenticated. Either true or false would work here, + // but we're going to go with false as this is closer to the "legacy" behavior + if (!useRbacForRequestCache.has(request)) { + return false; + } + + return useRbacForRequestCache.get(request); + }, + }; +} diff --git a/x-pack/plugins/security/server/lib/authorization/mode.test.js b/x-pack/plugins/security/server/lib/authorization/mode.test.js new file mode 100644 index 0000000000000..5f14842710b56 --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/mode.test.js @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { authorizationModeFactory } from './mode'; + +const createMockConfig = (settings) => { + const mockConfig = { + get: jest.fn() + }; + + mockConfig.get.mockImplementation(key => { + return settings[key]; + }); + + return mockConfig; +}; + +const createMockXpackInfoFeature = (allowRbac) => { + return { + getLicenseCheckResults() { + return { + allowRbac + }; + } + }; +}; + +describe(`#initialize`, () => { + test(`can't be initialized twice for the same request`, async () => { + const mockConfig = createMockConfig(); + const mockXpackInfoFeature = createMockXpackInfoFeature(); + const mode = authorizationModeFactory({}, {}, mockConfig, {}, {}, mockXpackInfoFeature); + const request = {}; + + await mode.initialize(request); + expect(mode.initialize(request)).rejects.toThrowErrorMatchingSnapshot(); + }); +}); + +describe(`#useRbacForRequest`, () => { + test(`return false if not initialized for request`, async () => { + const mockConfig = createMockConfig(); + const mockXpackInfoFeature = createMockXpackInfoFeature(); + const mode = authorizationModeFactory({}, {}, mockConfig, {}, {}, mockXpackInfoFeature); + const request = {}; + + const result = mode.useRbacForRequest(request); + expect(result).toBe(false); + }); + + test(`returns true if legacy fallback is disabled`, async () => { + const mockConfig = createMockConfig({ + 'xpack.security.authorization.legacyFallback.enabled': false, + }); + const mockXpackInfoFeature = createMockXpackInfoFeature(); + const mode = authorizationModeFactory({}, {}, mockConfig, {}, {}, mockXpackInfoFeature); + const request = {}; + + await mode.initialize(request); + const result = mode.useRbacForRequest(request); + expect(result).toBe(true); + }); +}); diff --git a/x-pack/plugins/security/server/lib/authorization/privileges.js b/x-pack/plugins/security/server/lib/authorization/privileges.js index 6f64871ed7556..7e9f53f873a0d 100644 --- a/x-pack/plugins/security/server/lib/authorization/privileges.js +++ b/x-pack/plugins/security/server/lib/authorization/privileges.js @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export function buildPrivilegeMap(savedObjectTypes, application, actions) { +import { IGNORED_TYPES } from '../../../common/constants'; + +export function buildPrivilegeMap(savedObjectTypes, actions) { const buildSavedObjectsActions = (savedObjectActions) => { return savedObjectTypes + .filter(type => !IGNORED_TYPES.includes(type)) .map(type => savedObjectActions.map(savedObjectAction => actions.getSavedObjectAction(type, savedObjectAction))) .reduce((acc, types) => [...acc, ...types], []); }; @@ -14,21 +17,43 @@ export function buildPrivilegeMap(savedObjectTypes, application, actions) { // the following list of privileges should only be added to, you can safely remove actions, but not privileges as // it's a backwards compatibility issue and we'll have to at least adjust registerPrivilegesWithCluster to support it return { - all: { - application, - name: 'all', - actions: [actions.version, 'action:*'], - metadata: {} + global: { + all: [ + actions.version, + 'action:*' + ], + read: [ + actions.version, + actions.login, + ...buildSavedObjectsActions([ + 'get', + 'bulk_get', + 'find' + ]) + ], + }, + space: { + all: [ + actions.version, + actions.login, + ...buildSavedObjectsActions([ + 'create', + 'bulk_create', + 'delete', + 'get', + 'bulk_get', + 'find', + 'update' + ]) + ], + read: [ + actions.version, + actions.login, + ...buildSavedObjectsActions([ + 'get', + 'bulk_get', + 'find']) + ], }, - read: { - application, - name: 'read', - actions: [actions.version, actions.login, ...buildSavedObjectsActions(['get', 'bulk_get', 'find'])], - metadata: {} - } }; } - -export function buildLegacyIndexPrivileges() { - return ['create', 'delete', 'read', 'view_index_metadata']; -} diff --git a/x-pack/plugins/security/server/lib/authorization/register_privileges_with_cluster.js b/x-pack/plugins/security/server/lib/authorization/register_privileges_with_cluster.js index 826cdab4b4204..6845dd7590e2d 100644 --- a/x-pack/plugins/security/server/lib/authorization/register_privileges_with_cluster.js +++ b/x-pack/plugins/security/server/lib/authorization/register_privileges_with_cluster.js @@ -7,6 +7,33 @@ import { difference, isEmpty, isEqual } from 'lodash'; import { buildPrivilegeMap } from './privileges'; import { getClient } from '../../../../../server/lib/get_client_shield'; +import { spaceApplicationPrivilegesSerializer } from './space_application_privileges_serializer'; + +const serializePrivileges = (application, privilegeMap) => { + return { + [application]: { + ...Object.entries(privilegeMap.global).reduce((acc, [privilegeName, privilegeActions]) => { + acc[privilegeName] = { + application, + name: privilegeName, + actions: privilegeActions, + metadata: {}, + }; + return acc; + }, {}), + ...Object.entries(privilegeMap.space).reduce((acc, [privilegeName, privilegeActions]) => { + const name = spaceApplicationPrivilegesSerializer.privilege.serialize(privilegeName); + acc[name] = { + application, + name, + actions: privilegeActions, + metadata: {}, + }; + return acc; + }, {}) + } + }; +}; export async function registerPrivilegesWithCluster(server) { @@ -14,6 +41,16 @@ export async function registerPrivilegesWithCluster(server) { const { types: savedObjectTypes } = server.savedObjects; const { actions, application } = authorization; + const arePrivilegesEqual = (existingPrivileges, expectedPrivileges) => { + // when comparing privileges, the order of the actions doesn't matter, lodash's isEqual + // doesn't know how to compare Sets + return isEqual(existingPrivileges, expectedPrivileges, (value, other, key) => { + if (key === 'actions' && Array.isArray(value) && Array.isArray(other)) { + return isEqual(value.sort(), other.sort()); + } + }); + }; + const shouldRemovePrivileges = (existingPrivileges, expectedPrivileges) => { if (isEmpty(existingPrivileges)) { return false; @@ -22,9 +59,8 @@ export async function registerPrivilegesWithCluster(server) { return difference(Object.keys(existingPrivileges[application]), Object.keys(expectedPrivileges[application])).length > 0; }; - const expectedPrivileges = { - [application]: buildPrivilegeMap(savedObjectTypes, application, actions) - }; + const privilegeMap = buildPrivilegeMap(savedObjectTypes, actions); + const expectedPrivileges = serializePrivileges(application, privilegeMap); server.log(['security', 'debug'], `Registering Kibana Privileges with Elasticsearch for ${application}`); @@ -34,7 +70,7 @@ export async function registerPrivilegesWithCluster(server) { // we only want to post the privileges when they're going to change as Elasticsearch has // to clear the role cache to get these changes reflected in the _has_privileges API const existingPrivileges = await callCluster(`shield.getPrivilege`, { privilege: application }); - if (isEqual(existingPrivileges, expectedPrivileges)) { + if (arePrivilegesEqual(existingPrivileges, expectedPrivileges)) { server.log(['security', 'debug'], `Kibana Privileges already registered with Elasticearch for ${application}`); return; } diff --git a/x-pack/plugins/security/server/lib/authorization/register_privileges_with_cluster.test.js b/x-pack/plugins/security/server/lib/authorization/register_privileges_with_cluster.test.js index f326d85fdeee3..b2a391aa49573 100644 --- a/x-pack/plugins/security/server/lib/authorization/register_privileges_with_cluster.test.js +++ b/x-pack/plugins/security/server/lib/authorization/register_privileges_with_cluster.test.js @@ -14,10 +14,12 @@ jest.mock('./privileges', () => ({ buildPrivilegeMap: jest.fn(), })); +const application = 'default-application'; + const registerPrivilegesWithClusterTest = (description, { settings = {}, savedObjectTypes, - expectedPrivileges, + privilegeMap, existingPrivileges, throwErrorWhenGettingPrivileges, throwErrorWhenPuttingPrivileges, @@ -32,7 +34,6 @@ const registerPrivilegesWithClusterTest = (description, { }; const defaultVersion = 'default-version'; - const application = 'default-application'; const createMockServer = () => { const mockServer = { @@ -65,8 +66,8 @@ const registerPrivilegesWithClusterTest = (description, { return mockServer; }; - const createExpectUpdatedPrivileges = (mockServer, mockCallWithInternalUser, privileges, error) => { - return () => { + const createExpectUpdatedPrivileges = (mockServer, mockCallWithInternalUser, error) => { + return (postPrivilegesBody) => { expect(error).toBeUndefined(); expect(mockCallWithInternalUser).toHaveBeenCalledTimes(2); expect(mockCallWithInternalUser).toHaveBeenCalledWith('shield.getPrivilege', { @@ -75,9 +76,7 @@ const registerPrivilegesWithClusterTest = (description, { expect(mockCallWithInternalUser).toHaveBeenCalledWith( 'shield.postPrivileges', { - body: { - [application]: privileges - }, + body: postPrivilegesBody, } ); @@ -137,9 +136,7 @@ const registerPrivilegesWithClusterTest = (description, { return {}; } - return { - [application]: existingPrivileges - }; + return existingPrivileges; }) .mockImplementationOnce(async () => { if (throwErrorWhenPuttingPrivileges) { @@ -147,7 +144,7 @@ const registerPrivilegesWithClusterTest = (description, { } }); - buildPrivilegeMap.mockReturnValue(expectedPrivileges); + buildPrivilegeMap.mockReturnValue(privilegeMap); let error; try { @@ -157,7 +154,7 @@ const registerPrivilegesWithClusterTest = (description, { } assert({ - expectUpdatedPrivileges: createExpectUpdatedPrivileges(mockServer, mockCallWithInternalUser, expectedPrivileges, error), + expectUpdatedPrivileges: createExpectUpdatedPrivileges(mockServer, mockCallWithInternalUser, error), expectDidntUpdatePrivileges: createExpectDidntUpdatePrivileges(mockServer, mockCallWithInternalUser, error), expectErrorThrown: createExpectErrorThrown(mockServer, error), mocks: { @@ -168,10 +165,7 @@ const registerPrivilegesWithClusterTest = (description, { }); }; -registerPrivilegesWithClusterTest(`passes saved object types, application and actions to buildPrivilegeMap`, { - settings: { - 'pkg.version': 'foo-version' - }, +registerPrivilegesWithClusterTest(`passes saved object types, and actions to buildPrivilegeMap`, { savedObjectTypes: [ 'foo-type', 'bar-type', @@ -179,146 +173,249 @@ registerPrivilegesWithClusterTest(`passes saved object types, application and ac assert: ({ mocks }) => { expect(mocks.buildPrivilegeMap).toHaveBeenCalledWith( ['foo-type', 'bar-type'], - mocks.server.plugins.security.authorization.application, mocks.server.plugins.security.authorization.actions, ); }, }); registerPrivilegesWithClusterTest(`inserts privileges when we don't have any existing privileges`, { - expectedPrivileges: { - expected: true + privilegeMap: { + global: { + foo: ['action:foo'] + }, + space: { + bar: ['action:bar'] + } }, existingPrivileges: null, assert: ({ expectUpdatedPrivileges }) => { - expectUpdatedPrivileges(); - } -}); - -registerPrivilegesWithClusterTest(`updates privileges when simple top-level privileges values don't match`, { - expectedPrivileges: { - expected: true - }, - existingPrivileges: { - expected: false - }, - assert: ({ expectUpdatedPrivileges }) => { - expectUpdatedPrivileges(); - } -}); - -registerPrivilegesWithClusterTest(`throws error when we have two different top-level privileges`, { - expectedPrivileges: { - notExpected: true - }, - existingPrivileges: { - expected: true - }, - assert: ({ expectErrorThrown }) => { - expectErrorThrown(`Privileges are missing and can't be removed, currently.`); - } -}); - -registerPrivilegesWithClusterTest(`updates privileges when we want to add a top-level privilege`, { - expectedPrivileges: { - expected: true, - new: false, - }, - existingPrivileges: { - expected: true, - }, - assert: ({ expectUpdatedPrivileges }) => { - expectUpdatedPrivileges(); + expectUpdatedPrivileges({ + [application]: { + foo: { + application, + name: 'foo', + actions: ['action:foo'], + metadata: {}, + }, + space_bar: { + application, + name: 'space_bar', + actions: ['action:bar'], + metadata: {}, + } + } + }); } }); -registerPrivilegesWithClusterTest(`updates privileges when nested privileges values don't match`, { - expectedPrivileges: { - kibana: { - expected: true +registerPrivilegesWithClusterTest(`throws error when we should be removing privilege`, { + privilegeMap: { + global: { + foo: ['action:foo'], + }, + space: { + bar: ['action:bar'] } }, existingPrivileges: { - kibana: { - expected: false + [application]: { + foo: { + application, + name: 'foo', + actions: ['action:not-foo'], + metadata: {}, + }, + quz: { + application, + name: 'quz', + actions: ['action:not-quz'], + metadata: {}, + }, + space_bar: { + application, + name: 'space_bar', + actions: ['action:not-bar'], + metadata: {}, + } } }, - assert: ({ expectUpdatedPrivileges }) => { - expectUpdatedPrivileges(); + assert: ({ expectErrorThrown }) => { + expectErrorThrown(`Privileges are missing and can't be removed, currently.`); } }); -registerPrivilegesWithClusterTest(`updates privileges when we have two different nested privileges`, { - expectedPrivileges: { - kibana: { - notExpected: true +registerPrivilegesWithClusterTest(`updates privileges when actions don't match`, { + privilegeMap: { + global: { + foo: ['action:foo'] + }, + space: { + bar: ['action:bar'] } }, existingPrivileges: { - kibana: { - expected: false + [application]: { + foo: { + application, + name: 'foo', + actions: ['action:not-foo'], + metadata: {}, + }, + space_bar: { + application, + name: 'space_bar', + actions: ['action:not-bar'], + metadata: {}, + } } }, assert: ({ expectUpdatedPrivileges }) => { - expectUpdatedPrivileges(); + expectUpdatedPrivileges({ + [application]: { + foo: { + application, + name: 'foo', + actions: ['action:foo'], + metadata: {}, + }, + space_bar: { + application, + name: 'space_bar', + actions: ['action:bar'], + metadata: {}, + } + } + }); } }); -registerPrivilegesWithClusterTest(`updates privileges when nested privileges arrays don't match`, { - expectedPrivileges: { - kibana: { - expected: ['one', 'two'] +registerPrivilegesWithClusterTest(`updates privileges when global privilege added`, { + privilegeMap: { + global: { + foo: ['action:foo'], + quz: ['action:quz'] + }, + space: { + bar: ['action:bar'] } }, existingPrivileges: { - kibana: { - expected: ['one'] + [application]: { + foo: { + application, + name: 'foo', + actions: ['action:not-foo'], + metadata: {}, + }, + space_bar: { + application, + name: 'space_bar', + actions: ['action:not-bar'], + metadata: {}, + } } }, assert: ({ expectUpdatedPrivileges }) => { - expectUpdatedPrivileges(); + expectUpdatedPrivileges({ + [application]: { + foo: { + application, + name: 'foo', + actions: ['action:foo'], + metadata: {}, + }, + quz: { + application, + name: 'quz', + actions: ['action:quz'], + metadata: {}, + }, + space_bar: { + application, + name: 'space_bar', + actions: ['action:bar'], + metadata: {}, + } + } + }); } }); -registerPrivilegesWithClusterTest(`updates privileges when nested property array values are reordered`, { - expectedPrivileges: { - kibana: { - foo: ['one', 'two'] +registerPrivilegesWithClusterTest(`updates privileges when space privilege added`, { + privilegeMap: { + global: { + foo: ['action:foo'], + }, + space: { + bar: ['action:bar'], + quz: ['action:quz'] } }, existingPrivileges: { - kibana: { - foo: ['two', 'one'] + [application]: { + foo: { + application, + name: 'foo', + actions: ['action:not-foo'], + metadata: {}, + }, + space_bar: { + application, + name: 'space_bar', + actions: ['action:not-bar'], + metadata: {}, + } } }, assert: ({ expectUpdatedPrivileges }) => { - expectUpdatedPrivileges(); - } -}); - -registerPrivilegesWithClusterTest(`doesn't update privileges when simple top-level privileges match`, { - expectedPrivileges: { - expected: true - }, - existingPrivileges: { - expected: true - }, - assert: ({ expectDidntUpdatePrivileges }) => { - expectDidntUpdatePrivileges(); + expectUpdatedPrivileges({ + [application]: { + foo: { + application, + name: 'foo', + actions: ['action:foo'], + metadata: {}, + }, + space_bar: { + application, + name: 'space_bar', + actions: ['action:bar'], + metadata: {}, + }, + space_quz: { + application, + name: 'space_quz', + actions: ['action:quz'], + metadata: {}, + }, + } + }); } }); -registerPrivilegesWithClusterTest(`doesn't update privileges when nested properties are reordered`, { - expectedPrivileges: { - kibana: { - foo: true, - bar: false +registerPrivilegesWithClusterTest(`doesn't update privileges when order of actions differ`, { + privilegeMap: { + global: { + foo: ['action:foo', 'action:quz'] + }, + space: { + bar: ['action:bar'] } }, existingPrivileges: { - kibana: { - bar: false, - foo: true + [application]: { + foo: { + application, + name: 'foo', + actions: ['action:quz', 'action:foo'], + metadata: {}, + }, + space_bar: { + application, + name: 'space_bar', + actions: ['action:bar'], + metadata: {}, + } } }, assert: ({ expectDidntUpdatePrivileges }) => { @@ -327,6 +424,10 @@ registerPrivilegesWithClusterTest(`doesn't update privileges when nested propert }); registerPrivilegesWithClusterTest(`throws and logs error when errors getting privileges`, { + privilegeMap: { + global: {}, + space: {} + }, throwErrorWhenGettingPrivileges: new Error('Error getting privileges'), assert: ({ expectErrorThrown }) => { expectErrorThrown('Error getting privileges'); @@ -334,18 +435,15 @@ registerPrivilegesWithClusterTest(`throws and logs error when errors getting pri }); registerPrivilegesWithClusterTest(`throws and logs error when errors putting privileges`, { - expectedPrivileges: { - kibana: { - foo: false, - bar: false - } - }, - existingPrivileges: { - kibana: { - foo: true, - bar: true + privilegeMap: { + global: { + foo: [] + }, + space: { + bar: [] } }, + existingPrivileges: null, throwErrorWhenPuttingPrivileges: new Error('Error putting privileges'), assert: ({ expectErrorThrown }) => { expectErrorThrown('Error putting privileges'); diff --git a/x-pack/plugins/security/server/lib/authorization/init.js b/x-pack/plugins/security/server/lib/authorization/service.js similarity index 58% rename from x-pack/plugins/security/server/lib/authorization/init.js rename to x-pack/plugins/security/server/lib/authorization/service.js index f99bf6d25d26f..1d02330b9b199 100644 --- a/x-pack/plugins/security/server/lib/authorization/init.js +++ b/x-pack/plugins/security/server/lib/authorization/service.js @@ -5,20 +5,30 @@ */ import { actionsFactory } from './actions'; +import { authorizationModeFactory } from './mode'; import { checkPrivilegesWithRequestFactory } from './check_privileges'; -import { deepFreeze } from './deep_freeze'; import { getClient } from '../../../../../server/lib/get_client_shield'; -export function initAuthorizationService(server) { +export function createAuthorizationService(server, xpackInfoFeature) { const shieldClient = getClient(server); const config = server.config(); const actions = actionsFactory(config); const application = `kibana-${config.get('kibana.index')}`; + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory(actions, application, shieldClient); + const mode = authorizationModeFactory( + actions, + checkPrivilegesWithRequest, + config, + server.plugins, + server.savedObjects, + xpackInfoFeature + ); - server.expose('authorization', deepFreeze({ + return { actions, application, - checkPrivilegesWithRequest: checkPrivilegesWithRequestFactory(shieldClient, config, actions, application), - })); + checkPrivilegesWithRequest, + mode, + }; } diff --git a/x-pack/plugins/security/server/lib/authorization/init.test.js b/x-pack/plugins/security/server/lib/authorization/service.test.js similarity index 60% rename from x-pack/plugins/security/server/lib/authorization/init.test.js rename to x-pack/plugins/security/server/lib/authorization/service.test.js index d70e08934c131..f0bfe46c35b7c 100644 --- a/x-pack/plugins/security/server/lib/authorization/init.test.js +++ b/x-pack/plugins/security/server/lib/authorization/service.test.js @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { initAuthorizationService } from './init'; +import { createAuthorizationService } from './service'; import { actionsFactory } from './actions'; import { checkPrivilegesWithRequestFactory } from './check_privileges'; import { getClient } from '../../../../../server/lib/get_client_shield'; +import { authorizationModeFactory } from './mode'; jest.mock('./check_privileges', () => ({ checkPrivilegesWithRequestFactory: jest.fn(), @@ -21,6 +22,10 @@ jest.mock('./actions', () => ({ actionsFactory: jest.fn(), })); +jest.mock('./mode', () => ({ + authorizationModeFactory: jest.fn(), +})); + const createMockConfig = (settings = {}) => { const mockConfig = { get: jest.fn() @@ -38,7 +43,9 @@ test(`calls server.expose with exposed services`, () => { }); const mockServer = { expose: jest.fn(), - config: jest.fn().mockReturnValue(mockConfig) + config: jest.fn().mockReturnValue(mockConfig), + plugins: Symbol(), + savedObjects: Symbol(), }; const mockShieldClient = Symbol(); getClient.mockReturnValue(mockShieldClient); @@ -47,37 +54,20 @@ test(`calls server.expose with exposed services`, () => { const mockActions = Symbol(); actionsFactory.mockReturnValue(mockActions); mockConfig.get.mock; + const mockXpackInfoFeature = Symbol(); - initAuthorizationService(mockServer); + createAuthorizationService(mockServer, mockXpackInfoFeature); const application = `kibana-${kibanaIndex}`; expect(getClient).toHaveBeenCalledWith(mockServer); expect(actionsFactory).toHaveBeenCalledWith(mockConfig); - expect(checkPrivilegesWithRequestFactory).toHaveBeenCalledWith(mockShieldClient, mockConfig, mockActions, application); - expect(mockServer.expose).toHaveBeenCalledWith('authorization', { - actions: mockActions, - application, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - }); -}); - -test(`deep freezes exposed service`, () => { - const mockConfig = createMockConfig({ - 'kibana.index': '' - }); - const mockServer = { - expose: jest.fn(), - config: jest.fn().mockReturnValue(mockConfig) - }; - actionsFactory.mockReturnValue({ - login: 'login', - }); - - initAuthorizationService(mockServer); - - const exposed = mockServer.expose.mock.calls[0][1]; - expect(() => delete exposed.checkPrivilegesWithRequest).toThrowErrorMatchingSnapshot(); - expect(() => exposed.foo = 'bar').toThrowErrorMatchingSnapshot(); - expect(() => exposed.actions.login = 'not-login').toThrowErrorMatchingSnapshot(); - expect(() => exposed.application = 'changed').toThrowErrorMatchingSnapshot(); + expect(checkPrivilegesWithRequestFactory).toHaveBeenCalledWith(mockActions, application, mockShieldClient); + expect(authorizationModeFactory).toHaveBeenCalledWith( + mockActions, + mockCheckPrivilegesWithRequest, + mockConfig, + mockServer.plugins, + mockServer.savedObjects, + mockXpackInfoFeature, + ); }); diff --git a/x-pack/plugins/security/server/lib/authorization/space_application_privileges_serializer.js b/x-pack/plugins/security/server/lib/authorization/space_application_privileges_serializer.js new file mode 100644 index 0000000000000..2906f07e9f5ce --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/space_application_privileges_serializer.js @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const privilegePrefix = `space_`; +const resourcePrefix = `space:`; + +export const spaceApplicationPrivilegesSerializer = { + privilege: { + serialize(privilege) { + return `${privilegePrefix}${privilege}`; + }, + deserialize(privilege) { + if (!privilege.startsWith(privilegePrefix)) { + throw new Error(`Space privilege should have started with ${privilegePrefix}`); + } + + return privilege.slice(privilegePrefix.length); + }, + }, + resource: { + serialize(spaceId) { + return `${resourcePrefix}${spaceId}`; + }, + deserialize(resource) { + if (!resource.startsWith(resourcePrefix)) { + throw new Error(`Resource should have started with ${resourcePrefix}`); + } + + return resource.slice(resourcePrefix.length); + } + }, +}; diff --git a/x-pack/plugins/security/server/lib/authorization/space_application_privileges_serializer.test.js b/x-pack/plugins/security/server/lib/authorization/space_application_privileges_serializer.test.js new file mode 100644 index 0000000000000..2277d09e498db --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/space_application_privileges_serializer.test.js @@ -0,0 +1,51 @@ +/* + * 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 { spaceApplicationPrivilegesSerializer } from './space_application_privileges_serializer'; + +describe('#privilege', () => { + describe('#serialize', () => { + test(`prepends privilege with space_`, () => { + const result = spaceApplicationPrivilegesSerializer.privilege.serialize('all'); + expect(result).toBe('space_all'); + }); + }); + + describe('#deserialize', () => { + test(`throws error if privilege doesn't start with space_`, () => { + expect( + () => spaceApplicationPrivilegesSerializer.privilege.deserialize('foo_space_all') + ).toThrowErrorMatchingSnapshot(); + }); + + test(`removes space_ from the start`, () => { + const result = spaceApplicationPrivilegesSerializer.privilege.deserialize('space_all'); + expect(result).toBe('all'); + }); + }); +}); + +describe('#resource', () => { + describe('#serialize', () => { + test(`prepends resource with space:`, () => { + const result = spaceApplicationPrivilegesSerializer.resource.serialize('marketing'); + expect(result).toBe('space:marketing'); + }); + }); + + describe('#deserialize', () => { + test(`throws error if resource doesn't start with space:`, () => { + expect( + () => spaceApplicationPrivilegesSerializer.resource.deserialize('foo:space:something') + ).toThrowErrorMatchingSnapshot(); + }); + + test(`removes space: from the start`, () => { + const result = spaceApplicationPrivilegesSerializer.resource.deserialize('space:marketing'); + expect(result).toBe('marketing'); + }); + }); +}); diff --git a/x-pack/plugins/security/server/lib/authorization/validate_es_response.js b/x-pack/plugins/security/server/lib/authorization/validate_es_response.js index 34d618398bc3d..2819983cf43c8 100644 --- a/x-pack/plugins/security/server/lib/authorization/validate_es_response.js +++ b/x-pack/plugins/security/server/lib/authorization/validate_es_response.js @@ -5,19 +5,9 @@ */ import Joi from 'joi'; -import { buildLegacyIndexPrivileges } from './privileges'; -const legacyIndexPrivilegesSchema = Joi.object({ - ...buildLegacyIndexPrivileges().reduce((acc, privilege) => { - return { - ...acc, - [privilege]: Joi.bool().required() - }; - }, {}) -}).required(); - -export function validateEsPrivilegeResponse(response, application, actions, resources, kibanaIndex) { - const schema = buildValidationSchema(application, actions, resources, kibanaIndex); +export function validateEsPrivilegeResponse(response, application, actions, resources) { + const schema = buildValidationSchema(application, actions, resources); const { error, value } = schema.validate(response); if (error) { @@ -38,7 +28,7 @@ function buildActionsValidationSchema(actions) { }).required(); } -function buildValidationSchema(application, actions, resources, kibanaIndex) { +function buildValidationSchema(application, actions, resources) { const actionValidationSchema = buildActionsValidationSchema(actions); @@ -58,8 +48,6 @@ function buildValidationSchema(application, actions, resources, kibanaIndex) { application: Joi.object({ [application]: resourceValidationSchema, }).required(), - index: Joi.object({ - [kibanaIndex]: legacyIndexPrivilegesSchema - }).required() + index: Joi.object(), }).required(); } diff --git a/x-pack/plugins/security/server/lib/authorization/validate_es_response.test.js b/x-pack/plugins/security/server/lib/authorization/validate_es_response.test.js index f3dbad1b56ac9..a7fba14229de2 100644 --- a/x-pack/plugins/security/server/lib/authorization/validate_es_response.test.js +++ b/x-pack/plugins/security/server/lib/authorization/validate_es_response.test.js @@ -5,11 +5,10 @@ */ import { validateEsPrivilegeResponse } from "./validate_es_response"; -import { buildLegacyIndexPrivileges } from "./privileges"; -const resource = 'foo-resource'; +const resource1 = 'foo-resource'; +const resource2 = 'bar-resource'; const application = 'foo-application'; -const kibanaIndex = '.kibana'; const commonResponse = { username: 'user', @@ -17,31 +16,27 @@ const commonResponse = { }; describe('validateEsPrivilegeResponse', () => { - const legacyIndexResponse = { - [kibanaIndex]: { - 'create': true, - 'delete': true, - 'read': true, - 'view_index_metadata': true, - } - }; it('should validate a proper response', () => { const response = { ...commonResponse, application: { [application]: { - [resource]: { + [resource1]: { + action1: true, + action2: true, + action3: true + }, + [resource2]: { action1: true, action2: true, action3: true } } - }, - index: legacyIndexResponse + } }; - const result = validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex); + const result = validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]); expect(result).toEqual(response); }); @@ -50,17 +45,21 @@ describe('validateEsPrivilegeResponse', () => { ...commonResponse, application: { [application]: { - [resource]: { + [resource1]: { action1: true, action3: true + }, + [resource2]: { + action1: true, + action2: true, + action3: true } } - }, - index: legacyIndexResponse + } }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex) + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) ).toThrowErrorMatchingSnapshot(); }); @@ -69,19 +68,23 @@ describe('validateEsPrivilegeResponse', () => { ...commonResponse, application: { [application]: { - [resource]: { + [resource1]: { action1: true, action2: true, action3: true, action4: true, + }, + [resource2]: { + action1: true, + action2: true, + action3: true } } - }, - index: legacyIndexResponse + } }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex) + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) ).toThrowErrorMatchingSnapshot(); }); @@ -90,18 +93,22 @@ describe('validateEsPrivilegeResponse', () => { ...commonResponse, application: { [application]: { - [resource]: { + [resource1]: { action1: true, action2: true, action3: 'not a boolean', + }, + [resource2]: { + action1: true, + action2: true, + action3: true, } } }, - index: legacyIndexResponse }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex) + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) ).toThrowErrorMatchingSnapshot(); }); @@ -110,25 +117,34 @@ describe('validateEsPrivilegeResponse', () => { ...commonResponse, application: { [application]: { - [resource]: { + [resource1]: { + action1: true, + action2: true, + action3: true, + }, + [resource2]: { action1: true, action2: true, action3: true, } }, otherApplication: { - [resource]: { + [resource1]: { + action1: true, + action2: true, + action3: true, + }, + [resource2]: { action1: true, action2: true, action3: true, } } }, - index: legacyIndexResponse }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex) + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) ).toThrowErrorMatchingSnapshot(); }); @@ -136,11 +152,10 @@ describe('validateEsPrivilegeResponse', () => { const response = { ...commonResponse, application: {}, - index: legacyIndexResponse }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex) + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) ).toThrowErrorMatchingSnapshot(); }); @@ -151,21 +166,40 @@ describe('validateEsPrivilegeResponse', () => { }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex) + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) + ).toThrowErrorMatchingSnapshot(); + }); + + it('fails validation when an expected resource property is missing from the response', () => { + const response = { + ...commonResponse, + application: { + [application]: { + [resource1]: { + action1: true, + action2: true, + action3: true, + }, + } + }, + }; + + expect(() => + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) ).toThrowErrorMatchingSnapshot(); }); - it('fails validation when the expected resource property is missing from the response', () => { + it('fails validation when there are no resource properties in the response', () => { const response = { ...commonResponse, application: { - [application]: {} + [application]: { + } }, - index: legacyIndexResponse }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex) + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) ).toThrowErrorMatchingSnapshot(); }); @@ -174,6 +208,11 @@ describe('validateEsPrivilegeResponse', () => { ...commonResponse, application: { [application]: { + [resource1]: { + action1: true, + action2: true, + action3: true, + }, 'other-resource': { action1: true, action2: true, @@ -181,11 +220,10 @@ describe('validateEsPrivilegeResponse', () => { } } }, - index: legacyIndexResponse }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex) + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) ).toThrowErrorMatchingSnapshot(); }); @@ -194,164 +232,18 @@ describe('validateEsPrivilegeResponse', () => { ...commonResponse, application: { [application]: { - [resource]: 'not-an-object' + [resource1]: 'not-an-object', + [resource2]: { + action1: true, + action2: true, + action3: true, + }, } }, - index: legacyIndexResponse }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex) + validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) ).toThrowErrorMatchingSnapshot(); }); - - describe('legacy', () => { - it('should validate a proper response', () => { - const response = { - ...commonResponse, - application: { - [application]: { - [resource]: { - action1: true - } - } - }, - index: legacyIndexResponse - }; - - const result = validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex); - expect(result).toEqual(response); - }); - - it('should fail if the index property is missing', () => { - const response = { - ...commonResponse, - application: { - [application]: { - [resource]: { - action1: true - } - } - } - }; - - expect(() => - validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex) - ).toThrowErrorMatchingSnapshot(); - }); - - it('should fail if the kibana index is missing from the response', () => { - const response = { - ...commonResponse, - application: { - [application]: { - [resource]: { - action1: true - } - } - }, - index: {} - }; - - expect(() => - validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex) - ).toThrowErrorMatchingSnapshot(); - }); - - it('should fail if the index privilege response returns an extra index', () => { - const response = { - ...commonResponse, - application: { - [application]: { - [resource]: { - action1: true - } - } - }, - index: { - ...legacyIndexResponse, - 'anotherIndex': { - foo: true - } - } - }; - - expect(() => - validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex) - ).toThrowErrorMatchingSnapshot(); - }); - - it('should fail if the index privilege response contains an extra privilege', () => { - const response = { - ...commonResponse, - application: { - [application]: { - [resource]: { - action1: true - } - } - }, - index: { - [kibanaIndex]: { - ...legacyIndexResponse[kibanaIndex], - 'foo-permission': true - } - } - }; - - expect(() => - validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex) - ).toThrowErrorMatchingSnapshot(); - }); - - buildLegacyIndexPrivileges().forEach(privilege => { - test(`should fail if the ${privilege} index privilege is missing from the response`, () => { - const response = { - ...commonResponse, - application: { - [application]: { - [resource]: { - action1: true - } - } - }, - index: { - [kibanaIndex]: { - ...legacyIndexResponse[kibanaIndex] - } - } - }; - - delete response.index[kibanaIndex][privilege]; - - expect(() => - validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex) - ).toThrowErrorMatchingSnapshot(); - }); - - test(`should fail if the ${privilege} index privilege is malformed`, () => { - const response = { - ...commonResponse, - application: { - [application]: { - [resource]: { - action1: true - } - } - }, - index: { - [kibanaIndex]: { - ...legacyIndexResponse[kibanaIndex] - } - } - }; - - response.index[kibanaIndex][privilege] = 'not a boolean'; - - expect(() => - validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex) - ).toThrowErrorMatchingSnapshot(); - }); - }); - }); }); diff --git a/x-pack/plugins/security/server/lib/authorization/deep_freeze.js b/x-pack/plugins/security/server/lib/deep_freeze.js similarity index 100% rename from x-pack/plugins/security/server/lib/authorization/deep_freeze.js rename to x-pack/plugins/security/server/lib/deep_freeze.js diff --git a/x-pack/plugins/security/server/lib/authorization/deep_freeze.test.js b/x-pack/plugins/security/server/lib/deep_freeze.test.js similarity index 100% rename from x-pack/plugins/security/server/lib/authorization/deep_freeze.test.js rename to x-pack/plugins/security/server/lib/deep_freeze.test.js diff --git a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js deleted file mode 100644 index 95a4e9aa0eaac..0000000000000 --- a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js +++ /dev/null @@ -1,1177 +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 { SecureSavedObjectsClient } from './secure_saved_objects_client'; -import { CHECK_PRIVILEGES_RESULT } from '../authorization/check_privileges'; - -const createMockErrors = () => { - const forbiddenError = new Error('Mock ForbiddenError'); - const generalError = new Error('Mock GeneralError'); - - return { - forbiddenError, - decorateForbiddenError: jest.fn().mockReturnValue(forbiddenError), - generalError, - decorateGeneralError: jest.fn().mockReturnValue(generalError) - }; -}; - -const createMockAuditLogger = () => { - return { - savedObjectsAuthorizationFailure: jest.fn(), - savedObjectsAuthorizationSuccess: jest.fn(), - }; -}; - -const createMockActions = () => { - return { - getSavedObjectAction(type, action) { - return `mock-action:saved_objects/${type}/${action}`; - } - }; -}; - -describe('#errors', () => { - test(`assigns errors from constructor to .errors`, () => { - const errors = Symbol(); - - const client = new SecureSavedObjectsClient({ errors }); - - expect(client.errors).toBe(errors); - }); -}); - -describe('#create', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async () => { - throw new Error(); - }); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: mockActions, - }); - - await expect(client.create(type)).rejects.toThrowError(mockErrors.generalError); - - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'create')]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({ - result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED, - username, - missing: privileges, - })); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: mockActions, - }); - const attributes = Symbol(); - const options = Symbol(); - - await expect(client.create(type, attributes, options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'create')]); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'create', - [type], - [mockActions.getSavedObjectAction(type, 'create')], - { - type, - attributes, - options, - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of internalRepository.create when authorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockRepository = { - create: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn().mockImplementation(async () => ({ - result: CHECK_PRIVILEGES_RESULT.AUTHORIZED, - username, - })); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClient({ - internalRepository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: createMockActions(), - }); - const attributes = Symbol(); - const options = Symbol(); - - const result = await client.create(type, attributes, options); - - expect(result).toBe(returnValue); - expect(mockRepository.create).toHaveBeenCalledWith(type, attributes, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'create', [type], { - type, - attributes, - options, - }); - }); - - test(`returns result of callWithRequestRepository.create when legacy`, async () => { - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockRepository = { - create: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({ - result: CHECK_PRIVILEGES_RESULT.LEGACY, - username, - missing: privileges, - })); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClient({ - callWithRequestRepository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: createMockActions(), - }); - const attributes = Symbol(); - const options = Symbol(); - - const result = await client.create(type, attributes, options); - - expect(result).toBe(returnValue); - expect(mockRepository.create).toHaveBeenCalledWith(type, attributes, options); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - }); -}); - -describe('#bulkCreate', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async () => { - throw new Error(); - }); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: mockActions, - }); - - await expect(client.bulkCreate([{ type }])).rejects.toThrowError(mockErrors.generalError); - - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'bulk_create')]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({ - result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED, - username, - missing: [ - privileges[0] - ], - })); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: mockActions, - }); - const objects = [ - { type: type1 }, - { type: type1 }, - { type: type2 }, - ]; - const options = Symbol(); - - await expect(client.bulkCreate(objects, options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockCheckPrivileges).toHaveBeenCalledWith([ - mockActions.getSavedObjectAction(type1, 'bulk_create'), - mockActions.getSavedObjectAction(type2, 'bulk_create'), - ]); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'bulk_create', - [type1, type2], - [mockActions.getSavedObjectAction(type1, 'bulk_create')], - { - objects, - options, - } - ); - }); - - test(`returns result of internalRepository.bulkCreate when authorized`, async () => { - const username = Symbol(); - const type1 = 'foo'; - const type2 = 'bar'; - const returnValue = Symbol(); - const mockRepository = { - bulkCreate: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn().mockImplementation(async () => ({ - result: CHECK_PRIVILEGES_RESULT.AUTHORIZED, - username, - })); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClient({ - internalRepository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: createMockActions(), - }); - const objects = [ - { type: type1, otherThing: 'sup' }, - { type: type2, otherThing: 'everyone' }, - ]; - const options = Symbol(); - - const result = await client.bulkCreate(objects, options); - - expect(result).toBe(returnValue); - expect(mockRepository.bulkCreate).toHaveBeenCalledWith(objects, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'bulk_create', [type1, type2], { - objects, - options, - }); - }); - - test(`returns result of callWithRequestRepository.bulkCreate when legacy`, async () => { - const username = Symbol(); - const type1 = 'foo'; - const type2 = 'bar'; - const returnValue = Symbol(); - const mockRepository = { - bulkCreate: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({ - result: CHECK_PRIVILEGES_RESULT.LEGACY, - username, - missing: privileges, - })); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClient({ - callWithRequestRepository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: createMockActions(), - }); - const objects = [ - { type: type1, otherThing: 'sup' }, - { type: type2, otherThing: 'everyone' }, - ]; - const options = Symbol(); - - const result = await client.bulkCreate(objects, options); - - expect(result).toBe(returnValue); - expect(mockRepository.bulkCreate).toHaveBeenCalledWith(objects, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); -}); - -describe('#delete', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async () => { - throw new Error(); - }); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: mockActions, - }); - - await expect(client.delete(type)).rejects.toThrowError(mockErrors.generalError); - - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'delete')]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({ - result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED, - username, - missing: privileges, - })); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: mockActions, - }); - const id = Symbol(); - const options = Symbol(); - - await expect(client.delete(type, id, options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'delete')]); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'delete', - [type], - [mockActions.getSavedObjectAction(type, 'delete')], - { - type, - id, - options, - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of internalRepository.delete when authorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockRepository = { - delete: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn().mockImplementation(async () => ({ - result: CHECK_PRIVILEGES_RESULT.AUTHORIZED, - username, - })); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClient({ - internalRepository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: createMockActions(), - }); - const id = Symbol(); - const options = Symbol(); - - const result = await client.delete(type, id, options); - - expect(result).toBe(returnValue); - expect(mockRepository.delete).toHaveBeenCalledWith(type, id, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'delete', [type], { - type, - id, - options, - }); - }); - - test(`returns result of internalRepository.delete when legacy`, async () => { - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockRepository = { - delete: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({ - result: CHECK_PRIVILEGES_RESULT.LEGACY, - username, - missing: privileges, - })); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClient({ - callWithRequestRepository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: createMockActions(), - }); - const id = Symbol(); - const options = Symbol(); - - const result = await client.delete(type, id, options); - - expect(result).toBe(returnValue); - expect(mockRepository.delete).toHaveBeenCalledWith(type, id, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); -}); - -describe('#find', () => { - describe('type', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async () => { - throw new Error(); - }); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: mockActions, - }); - - await expect(client.find({ type })).rejects.toThrowError(mockErrors.generalError); - - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'find')]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when type's singular and unauthorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const mockRepository = {}; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({ - result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED, - username, - missing: privileges, - })); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - internalRepository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: mockActions, - }); - const options = { type }; - - await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'find')]); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'find', - [type], - [mockActions.getSavedObjectAction(type, 'find')], - { - options, - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when type's an array and unauthorized`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => { - return { - result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED, - username, - missing: [ - privileges[0] - ], - }; - }); - - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: mockActions, - }); - const options = { type: [type1, type2] }; - - await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockCheckPrivileges).toHaveBeenCalledWith([ - mockActions.getSavedObjectAction(type1, 'find'), - mockActions.getSavedObjectAction(type2, 'find') - ]); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'find', - [type1, type2], - [mockActions.getSavedObjectAction(type1, 'find')], - { - options, - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of internalRepository.find when authorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockRepository = { - find: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn().mockImplementation(async () => ({ - result: CHECK_PRIVILEGES_RESULT.AUTHORIZED, - username, - })); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClient({ - internalRepository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: createMockActions(), - }); - const options = { type }; - - const result = await client.find(options); - - expect(result).toBe(returnValue); - expect(mockRepository.find).toHaveBeenCalledWith({ type }); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'find', [type], { - options, - }); - }); - - test(`returns result of callWithRequestRepository.find when legacy`, async () => { - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockRepository = { - find: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({ - result: CHECK_PRIVILEGES_RESULT.LEGACY, - username, - missing: privileges, - })); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClient({ - callWithRequestRepository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: createMockActions(), - }); - const options = { type }; - - const result = await client.find(options); - - expect(result).toBe(returnValue); - expect(mockRepository.find).toHaveBeenCalledWith({ type }); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - }); - - describe('no type', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const mockRepository = {}; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async () => { - throw new Error(); - }); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - repository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - savedObjectTypes: [type1, type2], - actions: mockActions, - }); - - await expect(client.find()).rejects.toThrowError(mockErrors.generalError); - - expect(mockCheckPrivileges).toHaveBeenCalledWith([ - mockActions.getSavedObjectAction(type1, 'find'), - mockActions.getSavedObjectAction(type2, 'find'), - ]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const mockRepository = {}; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({ - result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED, - username, - missing: [ - privileges[0] - ], - })); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - repository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - savedObjectTypes: [type], - actions: mockActions, - }); - const options = Symbol(); - - await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'find')]); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'find', - [type], - [mockActions.getSavedObjectAction(type, 'find')], - { - options, - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of callWithRequestRepository.find when legacy`, async () => { - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockRepository = { - find: jest.fn().mockReturnValue(returnValue) - }; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({ - result: CHECK_PRIVILEGES_RESULT.LEGACY, - username, - missing: privileges, - })); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - callWithRequestRepository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - savedObjectTypes: [type], - actions: mockActions, - }); - const options = Symbol(); - - const result = await client.find(options); - - expect(result).toBe(returnValue); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'find')]); - expect(mockRepository.find).toHaveBeenCalledWith(options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`specifies authorized types when calling repository.find()`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const mockRepository = { - find: jest.fn(), - }; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({ - result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED, - missing: [ - privileges[0] - ] - })); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - internalRepository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - savedObjectTypes: [type1, type2], - actions: mockActions, - }); - - await client.find({}); - - expect(mockCheckPrivileges).toHaveBeenCalledWith([ - mockActions.getSavedObjectAction(type1, 'find'), - mockActions.getSavedObjectAction(type2, 'find'), - ]); - expect(mockRepository.find).toHaveBeenCalledWith(expect.objectContaining({ - type: [type2], - })); - }); - - test(`returns result of repository.find`, async () => { - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockRepository = { - find: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn().mockImplementation(async () => ({ - result: CHECK_PRIVILEGES_RESULT.AUTHORIZED, - username, - missing: [], - })); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClient({ - internalRepository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - savedObjectTypes: [type], - actions: createMockActions(), - }); - const options = Symbol(); - - const result = await client.find(options); - - expect(result).toBe(returnValue); - expect(mockRepository.find).toHaveBeenCalledWith({ type: [type] }); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'find', [type], { - options, - }); - }); - }); -}); - -describe('#bulkGet', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async () => { - throw new Error(); - }); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: mockActions, - }); - - await expect(client.bulkGet([{ type }])).rejects.toThrowError(mockErrors.generalError); - - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'bulk_get')]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({ - result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED, - username, - missing: [ - privileges[0] - ], - })); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: mockActions, - }); - const objects = [ - { type: type1 }, - { type: type1 }, - { type: type2 }, - ]; - const options = Symbol(); - - await expect(client.bulkGet(objects, options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockCheckPrivileges).toHaveBeenCalledWith([ - mockActions.getSavedObjectAction(type1, 'bulk_get'), - mockActions.getSavedObjectAction(type2, 'bulk_get'), - ]); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'bulk_get', - [type1, type2], - [mockActions.getSavedObjectAction(type1, 'bulk_get')], - { - objects, - options, - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of internalRepository.bulkGet when authorized`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const returnValue = Symbol(); - const mockRepository = { - bulkGet: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn().mockImplementation(async () => ({ - result: CHECK_PRIVILEGES_RESULT.AUTHORIZED, - username, - })); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClient({ - internalRepository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: createMockActions(), - }); - const objects = [ - { type: type1, id: 'foo-id' }, - { type: type2, id: 'bar-id' }, - ]; - const options = Symbol(); - - const result = await client.bulkGet(objects, options); - - expect(result).toBe(returnValue); - expect(mockRepository.bulkGet).toHaveBeenCalledWith(objects, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'bulk_get', [type1, type2], { - objects, - options, - }); - }); - - test(`returns result of callWithRequestRepository.bulkGet when legacy`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const returnValue = Symbol(); - const mockRepository = { - bulkGet: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({ - result: CHECK_PRIVILEGES_RESULT.LEGACY, - username, - missing: privileges, - })); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClient({ - callWithRequestRepository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: createMockActions(), - }); - const objects = [ - { type: type1, id: 'foo-id' }, - { type: type2, id: 'bar-id' }, - ]; - const options = Symbol(); - - const result = await client.bulkGet(objects, options); - - expect(result).toBe(returnValue); - expect(mockRepository.bulkGet).toHaveBeenCalledWith(objects, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); -}); - -describe('#get', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async () => { - throw new Error(); - }); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: mockActions, - }); - - await expect(client.get(type)).rejects.toThrowError(mockErrors.generalError); - - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'get')]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({ - result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED, - username, - missing: privileges, - })); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: mockActions, - }); - const id = Symbol(); - const options = Symbol(); - - await expect(client.get(type, id, options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'get')]); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'get', - [type], - [mockActions.getSavedObjectAction(type, 'get')], - { - type, - id, - options, - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of internalRepository.get when authorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockRepository = { - get: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn().mockImplementation(async () => ({ - result: CHECK_PRIVILEGES_RESULT.AUTHORIZED, - username, - })); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClient({ - internalRepository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: createMockActions(), - }); - const id = Symbol(); - const options = Symbol(); - - const result = await client.get(type, id, options); - - expect(result).toBe(returnValue); - expect(mockRepository.get).toHaveBeenCalledWith(type, id, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'get', [type], { - type, - id, - options, - }); - }); - - test(`returns result of callWithRequestRepository.get when user isn't authorized and has legacy fallback`, async () => { - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockRepository = { - get: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({ - result: CHECK_PRIVILEGES_RESULT.LEGACY, - username, - missing: privileges, - })); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClient({ - callWithRequestRepository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: createMockActions(), - }); - const id = Symbol(); - const options = Symbol(); - - const result = await client.get(type, id, options); - - expect(result).toBe(returnValue); - expect(mockRepository.get).toHaveBeenCalledWith(type, id, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); -}); - -describe('#update', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async () => { - throw new Error(); - }); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: mockActions, - }); - - await expect(client.update(type)).rejects.toThrowError(mockErrors.generalError); - - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'update')]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({ - result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED, - username, - missing: privileges, - })); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const client = new SecureSavedObjectsClient({ - errors: mockErrors, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: mockActions, - }); - const id = Symbol(); - const attributes = Symbol(); - const options = Symbol(); - - await expect(client.update(type, id, attributes, options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'update')]); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'update', - [type], - [mockActions.getSavedObjectAction(type, 'update')], - { - type, - id, - attributes, - options, - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of repository.update when authorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockRepository = { - update: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn().mockImplementation(async () => ({ - result: CHECK_PRIVILEGES_RESULT.AUTHORIZED, - username, - })); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClient({ - internalRepository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: createMockActions(), - }); - const id = Symbol(); - const attributes = Symbol(); - const options = Symbol(); - - const result = await client.update(type, id, attributes, options); - - expect(result).toBe(returnValue); - expect(mockRepository.update).toHaveBeenCalledWith(type, id, attributes, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'update', [type], { - type, - id, - attributes, - options, - }); - }); - - test(`returns result of repository.update when legacy`, async () => { - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockRepository = { - update: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({ - result: CHECK_PRIVILEGES_RESULT.LEGACY, - username, - missing: privileges, - })); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClient({ - callWithRequestRepository: mockRepository, - checkPrivileges: mockCheckPrivileges, - auditLogger: mockAuditLogger, - actions: createMockActions(), - }); - const id = Symbol(); - const attributes = Symbol(); - const options = Symbol(); - - const result = await client.update(type, id, attributes, options); - - expect(result).toBe(returnValue); - expect(mockRepository.update).toHaveBeenCalledWith(type, id, attributes, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.js similarity index 51% rename from x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js rename to x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.js index bd32c89614993..d7c14d4a0aed7 100644 --- a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js +++ b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.js @@ -5,55 +5,60 @@ */ import { get, uniq } from 'lodash'; -import { CHECK_PRIVILEGES_RESULT } from '../authorization/check_privileges'; +import { IGNORED_TYPES } from '../../../common/constants'; -export class SecureSavedObjectsClient { +export class SecureSavedObjectsClientWrapper { constructor(options) { const { - errors, - internalRepository, - callWithRequestRepository, - checkPrivileges, + actions, auditLogger, + baseClient, + checkPrivilegesWithRequest, + errors, + request, savedObjectTypes, - actions, + spaces, } = options; this.errors = errors; - this._internalRepository = internalRepository; - this._callWithRequestRepository = callWithRequestRepository; - this._checkPrivileges = checkPrivileges; + this._actions = actions; this._auditLogger = auditLogger; + this._baseClient = baseClient; + this._checkPrivileges = checkPrivilegesWithRequest(request); + this._request = request; this._savedObjectTypes = savedObjectTypes; - this._actions = actions; + this._spaces = spaces; } async create(type, attributes = {}, options = {}) { - return await this._execute( + await this._ensureAuthorized( type, 'create', { type, attributes, options }, - repository => repository.create(type, attributes, options), ); + + return await this._baseClient.create(type, attributes, options); } async bulkCreate(objects, options = {}) { const types = uniq(objects.map(o => o.type)); - return await this._execute( + await this._ensureAuthorized( types, 'bulk_create', { objects, options }, - repository => repository.bulkCreate(objects, options), ); + + return await this._baseClient.bulkCreate(objects, options); } - async delete(type, id, options = {}) { - return await this._execute( + async delete(type, id, options) { + await this._ensureAuthorized( type, 'delete', { type, id, options }, - repository => repository.delete(type, id, options), ); + + return await this._baseClient.delete(type, id, options); } async find(options = {}) { @@ -66,58 +71,68 @@ export class SecureSavedObjectsClient { async bulkGet(objects = [], options = {}) { const types = uniq(objects.map(o => o.type)); - return await this._execute( + await this._ensureAuthorized( types, 'bulk_get', { objects, options }, - repository => repository.bulkGet(objects, options) ); + + return await this._baseClient.bulkGet(objects, options); } async get(type, id, options = {}) { - return await this._execute( + await this._ensureAuthorized( type, 'get', { type, id, options }, - repository => repository.get(type, id, options) ); + + return await this._baseClient.get(type, id, options); } async update(type, id, attributes, options = {}) { - return await this._execute( + await this._ensureAuthorized( type, 'update', { type, id, attributes, options }, - repository => repository.update(type, id, attributes, options) ); + + return await this._baseClient.update(type, id, attributes, options); } async _checkSavedObjectPrivileges(actions) { try { - return await this._checkPrivileges(actions); + if (this._spaces) { + const spaceId = this._spaces.getSpaceId(this._request); + return await this._checkPrivileges.atSpace(spaceId, actions); + } + else { + return await this._checkPrivileges.globally(actions); + } } catch(error) { const { reason } = get(error, 'body.error', {}); throw this.errors.decorateGeneralError(error, reason); } } - async _execute(typeOrTypes, action, args, fn) { + async _ensureAuthorized(typeOrTypes, action, args) { const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; const actions = types.map(type => this._actions.getSavedObjectAction(type, action)); - const { result, username, missing } = await this._checkSavedObjectPrivileges(actions); - - switch (result) { - case CHECK_PRIVILEGES_RESULT.AUTHORIZED: - this._auditLogger.savedObjectsAuthorizationSuccess(username, action, types, args); - return await fn(this._internalRepository); - case CHECK_PRIVILEGES_RESULT.LEGACY: - return await fn(this._callWithRequestRepository); - case CHECK_PRIVILEGES_RESULT.UNAUTHORIZED: - this._auditLogger.savedObjectsAuthorizationFailure(username, action, types, missing, args); - const msg = `Unable to ${action} ${[...types].sort().join(',')}, missing ${[...missing].sort().join(',')}`; - throw this.errors.decorateForbiddenError(new Error(msg)); - default: - throw new Error('Unexpected result from hasPrivileges'); + const { hasAllRequested, username, privileges } = await this._checkSavedObjectPrivileges(actions); + + if (hasAllRequested) { + this._auditLogger.savedObjectsAuthorizationSuccess(username, action, types, args); + } else { + const missing = this._getMissingPrivileges(privileges); + this._auditLogger.savedObjectsAuthorizationFailure( + username, + action, + types, + missing, + args + ); + const msg = `Unable to ${action} ${[...types].sort().join(',')}, missing ${[...missing].sort().join(',')}`; + throw this.errors.decorateForbiddenError(new Error(msg)); } } @@ -125,16 +140,12 @@ export class SecureSavedObjectsClient { const action = 'find'; // we have to filter for only their authorized types - const types = this._savedObjectTypes; + const types = this._savedObjectTypes.filter(type => !IGNORED_TYPES.includes(type)); const typesToPrivilegesMap = new Map(types.map(type => [type, this._actions.getSavedObjectAction(type, action)])); - const { result, username, missing } = await this._checkSavedObjectPrivileges(Array.from(typesToPrivilegesMap.values())); - - if (result === CHECK_PRIVILEGES_RESULT.LEGACY) { - return await this._callWithRequestRepository.find(options); - } + const { username, privileges } = await this._checkSavedObjectPrivileges(Array.from(typesToPrivilegesMap.values())); const authorizedTypes = Array.from(typesToPrivilegesMap.entries()) - .filter(([, privilege]) => !missing.includes(privilege)) + .filter(([, privilege]) => privileges[privilege]) .map(([type]) => type); if (authorizedTypes.length === 0) { @@ -142,27 +153,36 @@ export class SecureSavedObjectsClient { username, action, types, - missing, + this._getMissingPrivileges(privileges), { options } ); throw this.errors.decorateForbiddenError(new Error(`Not authorized to find saved_object`)); } - this._auditLogger.savedObjectsAuthorizationSuccess(username, action, authorizedTypes, { options }); - - return await this._internalRepository.find( - { + this._auditLogger.savedObjectsAuthorizationSuccess(username, action, authorizedTypes, { + options: { ...options, type: authorizedTypes, - }); + } + }); + + return await this._baseClient.find({ + ...options, + type: authorizedTypes + }); } async _findWithTypes(options) { - return await this._execute( + await this._ensureAuthorized( options.type, 'find', { options }, - repository => repository.find(options) ); + + return await this._baseClient.find(options); + } + + _getMissingPrivileges(response) { + return Object.keys(response).filter(privilege => !response[privilege]); } } diff --git a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.test.js b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.test.js new file mode 100644 index 0000000000000..df8c7770a47ce --- /dev/null +++ b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.test.js @@ -0,0 +1,2513 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper'; + +const createMockErrors = () => { + const forbiddenError = new Error('Mock ForbiddenError'); + const generalError = new Error('Mock GeneralError'); + + return { + forbiddenError, + decorateForbiddenError: jest.fn().mockReturnValue(forbiddenError), + generalError, + decorateGeneralError: jest.fn().mockReturnValue(generalError) + }; +}; + +const createMockAuditLogger = () => { + return { + savedObjectsAuthorizationFailure: jest.fn(), + savedObjectsAuthorizationSuccess: jest.fn(), + }; +}; + +const createMockActions = () => { + return { + getSavedObjectAction(type, action) { + return `mock-action:saved_objects/${type}/${action}`; + } + }; +}; + +describe('#errors', () => { + test(`assigns errors from constructor to .errors`, () => { + const errors = Symbol(); + + const client = new SecureSavedObjectsClientWrapper({ + checkPrivilegesWithRequest: () => {}, + errors + }); + + expect(client.errors).toBe(errors); + }); +}); + +describe(`spaces disabled`, () => { + describe('#create', () => { + test(`throws decorated GeneralError when checkPrivileges.globally rejects promise`, async () => { + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + globally: jest.fn(async () => { + throw new Error('An actual error would happen here'); + }) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockActions = createMockActions(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + + await expect(client.create(type)).rejects.toThrowError(mockErrors.generalError); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'create')]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const type = 'foo'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + globally: jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'create')]: false, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + const attributes = Symbol(); + const options = Symbol(); + + await expect(client.create(type, attributes, options)).rejects.toThrowError(mockErrors.forbiddenError); + + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'create')]); + expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'create', + [type], + [mockActions.getSavedObjectAction(type, 'create')], + { + type, + attributes, + options, + } + ); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.create when authorized`, async () => { + const type = 'foo'; + const username = Symbol(); + const returnValue = Symbol(); + const mockActions = createMockActions(); + const mockBaseClient = { + create: jest.fn().mockReturnValue(returnValue) + }; + const mockCheckPrivileges = { + globally: jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'create')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: mockBaseClient, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: null, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + const attributes = Symbol(); + const options = Symbol(); + + const result = await client.create(type, attributes, options); + + expect(result).toBe(returnValue); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'create')]); + expect(mockBaseClient.create).toHaveBeenCalledWith(type, attributes, options); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'create', [type], { + type, + attributes, + options, + }); + }); + }); + + describe('#bulkCreate', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + globally: jest.fn(async () => { + throw new Error('An actual error would happen here'); + }) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockActions = createMockActions(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + + await expect(client.bulkCreate([{ type }])).rejects.toThrowError(mockErrors.generalError); + + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'bulk_create')]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const type1 = 'foo'; + const type2 = 'bar'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + globally: jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.getSavedObjectAction(type1, 'bulk_create')]: false, + [mockActions.getSavedObjectAction(type2, 'bulk_create')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + const objects = [ + { type: type1 }, + { type: type1 }, + { type: type2 }, + ]; + const options = Symbol(); + + await expect(client.bulkCreate(objects, options)).rejects.toThrowError(mockErrors.forbiddenError); + + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([ + mockActions.getSavedObjectAction(type1, 'bulk_create'), + mockActions.getSavedObjectAction(type2, 'bulk_create'), + ]); + expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'bulk_create', + [type1, type2], + [mockActions.getSavedObjectAction(type1, 'bulk_create')], + { + objects, + options, + } + ); + }); + + test(`returns result of baseClient.bulkCreate when authorized`, async () => { + const username = Symbol(); + const type1 = 'foo'; + const type2 = 'bar'; + const returnValue = Symbol(); + const mockBaseClient = { + bulkCreate: jest.fn().mockReturnValue(returnValue) + }; + const mockActions = createMockActions(); + const mockCheckPrivileges = { + globally: jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.getSavedObjectAction(type1, 'bulk_create')]: true, + [mockActions.getSavedObjectAction(type2, 'bulk_create')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: mockBaseClient, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: null, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + const objects = [ + { type: type1, otherThing: 'sup' }, + { type: type2, otherThing: 'everyone' }, + ]; + const options = Symbol(); + + const result = await client.bulkCreate(objects, options); + + expect(result).toBe(returnValue); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([ + mockActions.getSavedObjectAction(type1, 'bulk_create'), + mockActions.getSavedObjectAction(type2, 'bulk_create'), + ]); + expect(mockBaseClient.bulkCreate).toHaveBeenCalledWith(objects, options); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'bulk_create', [type1, type2], { + objects, + options, + }); + }); + }); + + describe('#delete', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + globally: jest.fn(async () => { + throw new Error('An actual error would happen here'); + }) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockActions = createMockActions(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + + await expect(client.delete(type)).rejects.toThrowError(mockErrors.generalError); + + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'delete')]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const type = 'foo'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + globally: jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'delete')]: false, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + const id = Symbol(); + + await expect(client.delete(type, id)).rejects.toThrowError(mockErrors.forbiddenError); + + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'delete')]); + expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'delete', + [type], + [mockActions.getSavedObjectAction(type, 'delete')], + { + type, + id, + } + ); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of internalRepository.delete when authorized`, async () => { + const type = 'foo'; + const username = Symbol(); + const returnValue = Symbol(); + const mockActions = createMockActions(); + const mockBaseClient = { + delete: jest.fn().mockReturnValue(returnValue) + }; + const mockCheckPrivileges = { + globally: jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'delete')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: mockBaseClient, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: null, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + const id = Symbol(); + const options = Symbol(); + + const result = await client.delete(type, id, options); + + expect(result).toBe(returnValue); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'delete')]); + expect(mockBaseClient.delete).toHaveBeenCalledWith(type, id, options); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'delete', [type], { + type, + id, + options, + }); + }); + }); + + describe('#find', () => { + describe('type', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + globally: jest.fn(async () => { + throw new Error('An actual error would happen here'); + }) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockActions = createMockActions(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + + await expect(client.find({ type })).rejects.toThrowError(mockErrors.generalError); + + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'find')]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when type's singular and unauthorized`, async () => { + const type = 'foo'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + globally: jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'find')]: false, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + const options = { type }; + + await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError); + + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'find')]); + expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'find', + [type], + [mockActions.getSavedObjectAction(type, 'find')], + { + options + } + ); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when type's an array and unauthorized`, async () => { + const type1 = 'foo'; + const type2 = 'bar'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + globally: jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.getSavedObjectAction(type1, 'find')]: false, + [mockActions.getSavedObjectAction(type2, 'find')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + const options = { type: [type1, type2] }; + + await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError); + + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([ + mockActions.getSavedObjectAction(type1, 'find'), + mockActions.getSavedObjectAction(type2, 'find') + ]); + expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'find', + [type1, type2], + [mockActions.getSavedObjectAction(type1, 'find')], + { + options + } + ); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.find when authorized`, async () => { + const type = 'foo'; + const username = Symbol(); + const returnValue = Symbol(); + const mockActions = createMockActions(); + const mockBaseClient = { + find: jest.fn().mockReturnValue(returnValue) + }; + const mockCheckPrivileges = { + globally: jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'find')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: mockBaseClient, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: null, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + const options = { type }; + + const result = await client.find(options); + + expect(result).toBe(returnValue); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'find')]); + expect(mockBaseClient.find).toHaveBeenCalledWith({ type }); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'find', [type], { + options, + }); + }); + }); + + describe('no type', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type1 = 'foo'; + const type2 = 'bar'; + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + globally: jest.fn(async () => { + throw new Error('An actual error would happen here'); + }) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockActions = createMockActions(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [type1, type2], + spaces: null, + }); + + await expect(client.find()).rejects.toThrowError(mockErrors.generalError); + + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([ + mockActions.getSavedObjectAction(type1, 'find'), + mockActions.getSavedObjectAction(type2, 'find'), + ]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const type = 'foo'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + globally: jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'find')]: false, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [type], + spaces: null, + }); + const options = Symbol(); + + await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError); + + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'find')]); + expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'find', + [type], + [mockActions.getSavedObjectAction(type, 'find')], + { + options + } + ); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.find when authorized`, async () => { + const type = 'foo'; + const spaceType = 'space'; + const username = Symbol(); + const returnValue = Symbol(); + const mockActions = createMockActions(); + const mockBaseClient = { + find: jest.fn().mockReturnValue(returnValue) + }; + const mockCheckPrivileges = { + globally: jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'find')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: mockBaseClient, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: null, + request: mockRequest, + savedObjectTypes: [type, spaceType], + spaces: null, + }); + const options = { + bar: true, + }; + + const result = await client.find(options); + + expect(result).toBe(returnValue); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'find')]); + expect(mockBaseClient.find).toHaveBeenCalledWith({ bar: true, type: ['foo'] }); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'find', [type], { + options: { + type: [type], + ...options, + } + }); + }); + + test(`specifies authorized types when calling repository.find()`, async () => { + const type1 = 'foo'; + const type2 = 'bar'; + const spaceType = 'space'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockBaseClient = { + find: jest.fn(), + }; + const mockCheckPrivileges = { + globally: jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.getSavedObjectAction(type1, 'find')]: false, + [mockActions.getSavedObjectAction(type2, 'find')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: mockBaseClient, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: null, + request: mockRequest, + savedObjectTypes: [type1, type2, spaceType], + spaces: null, + }); + + await client.find(); + + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([ + mockActions.getSavedObjectAction(type1, 'find'), + mockActions.getSavedObjectAction(type2, 'find'), + ]); + expect(mockBaseClient.find).toHaveBeenCalledWith(expect.objectContaining({ + type: [type2] + })); + }); + }); + }); + + describe('#bulkGet', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + globally: jest.fn(async () => { + throw new Error('An actual error would happen here'); + }) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockActions = createMockActions(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + + await expect(client.bulkGet([{ type }])).rejects.toThrowError(mockErrors.generalError); + + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'bulk_get')]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const type1 = 'foo'; + const type2 = 'bar'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + globally: jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.getSavedObjectAction(type1, 'bulk_get')]: false, + [mockActions.getSavedObjectAction(type2, 'bulk_get')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + const objects = [ + { type: type1 }, + { type: type1 }, + { type: type2 }, + ]; + const options = Symbol(); + + await expect(client.bulkGet(objects, options)).rejects.toThrowError(mockErrors.forbiddenError); + + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([ + mockActions.getSavedObjectAction(type1, 'bulk_get'), + mockActions.getSavedObjectAction(type2, 'bulk_get'), + ]); + expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'bulk_get', + [type1, type2], + [mockActions.getSavedObjectAction(type1, 'bulk_get')], + { + objects, + options, + } + ); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.bulkGet when authorized`, async () => { + const type1 = 'foo'; + const type2 = 'bar'; + const username = Symbol(); + const returnValue = Symbol(); + const mockActions = createMockActions(); + const mockBaseClient = { + bulkGet: jest.fn().mockReturnValue(returnValue) + }; + const mockCheckPrivileges = { + globally: jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.getSavedObjectAction(type1, 'bulk_get')]: true, + [mockActions.getSavedObjectAction(type2, 'bulk_get')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: mockBaseClient, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: null, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + const objects = [ + { type: type1, id: 'foo-id' }, + { type: type2, id: 'bar-id' }, + ]; + const options = Symbol(); + + const result = await client.bulkGet(objects, options); + + expect(result).toBe(returnValue); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([ + mockActions.getSavedObjectAction(type1, 'bulk_get'), + mockActions.getSavedObjectAction(type2, 'bulk_get'), + ]); + expect(mockBaseClient.bulkGet).toHaveBeenCalledWith(objects, options); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'bulk_get', [type1, type2], { + objects, + options, + }); + }); + }); + + describe('#get', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + globally: jest.fn(async () => { + throw new Error('An actual error would happen here'); + }) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockActions = createMockActions(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + + await expect(client.get(type)).rejects.toThrowError(mockErrors.generalError); + + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'get')]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const type = 'foo'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + globally: jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'get')]: false, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + const id = Symbol(); + const options = Symbol(); + + await expect(client.get(type, id, options)).rejects.toThrowError(mockErrors.forbiddenError); + + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'get')]); + expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'get', + [type], + [mockActions.getSavedObjectAction(type, 'get')], + { + type, + id, + options, + } + ); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.get when authorized`, async () => { + const type = 'foo'; + const username = Symbol(); + const returnValue = Symbol(); + const mockActions = createMockActions(); + const mockBaseClient = { + get: jest.fn().mockReturnValue(returnValue) + }; + const mockCheckPrivileges = { + globally: jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'get')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: mockBaseClient, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: null, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + const id = Symbol(); + const options = Symbol(); + + const result = await client.get(type, id, options); + + expect(result).toBe(returnValue); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'get')]); + expect(mockBaseClient.get).toHaveBeenCalledWith(type, id, options); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'get', [type], { + type, + id, + options + }); + }); + }); + + describe('#update', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + globally: jest.fn(async () => { + throw new Error('An actual error would happen here'); + }) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockActions = createMockActions(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + + await expect(client.update(type)).rejects.toThrowError(mockErrors.generalError); + + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'update')]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const type = 'foo'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + globally: jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'update')]: false, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + const id = Symbol(); + const attributes = Symbol(); + const options = Symbol(); + + await expect(client.update(type, id, attributes, options)).rejects.toThrowError(mockErrors.forbiddenError); + + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'update')]); + expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'update', + [type], + [mockActions.getSavedObjectAction(type, 'update')], + { + type, + id, + attributes, + options, + } + ); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.update when authorized`, async () => { + const type = 'foo'; + const username = Symbol(); + const returnValue = Symbol(); + const mockActions = createMockActions(); + const mockBaseClient = { + update: jest.fn().mockReturnValue(returnValue) + }; + const mockCheckPrivileges = { + globally: jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'update')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: mockBaseClient, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: null, + request: mockRequest, + savedObjectTypes: [], + spaces: null, + }); + const id = Symbol(); + const attributes = Symbol(); + const options = Symbol(); + + const result = await client.update(type, id, attributes, options); + + expect(result).toBe(returnValue); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'update')]); + expect(mockBaseClient.update).toHaveBeenCalledWith(type, id, attributes, options); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'update', [type], { + type, + id, + attributes, + options, + }); + }); + }); +}); + +describe(`spaces enabled`, () => { + describe('#create', () => { + test(`throws decorated GeneralError when checkPrivileges.globally rejects promise`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + atSpace: jest.fn(async () => { + throw new Error('An actual error would happen here'); + }) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockActions = createMockActions(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + + await expect(client.create(type)).rejects.toThrowError(mockErrors.generalError); + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'create')]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + atSpace: jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'create')]: false, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + const attributes = Symbol(); + const options = Symbol(); + + await expect(client.create(type, attributes, options)).rejects.toThrowError(mockErrors.forbiddenError); + + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'create')]); + expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'create', + [type], + [mockActions.getSavedObjectAction(type, 'create')], + { + type, + attributes, + options, + } + ); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.create when authorized`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const username = Symbol(); + const returnValue = Symbol(); + const mockActions = createMockActions(); + const mockBaseClient = { + create: jest.fn().mockReturnValue(returnValue) + }; + const mockCheckPrivileges = { + atSpace: jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'create')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: mockBaseClient, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: null, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + const attributes = Symbol(); + const options = Symbol(); + + const result = await client.create(type, attributes, options); + + expect(result).toBe(returnValue); + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'create')]); + expect(mockBaseClient.create).toHaveBeenCalledWith(type, attributes, options); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'create', [type], { + type, + attributes, + options, + }); + }); + }); + + describe('#bulkCreate', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + atSpace: jest.fn(async () => { + throw new Error('An actual error would happen here'); + }) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockActions = createMockActions(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + + await expect(client.bulkCreate([{ type }])).rejects.toThrowError(mockErrors.generalError); + + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'bulk_create')]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const spaceId = 'space_1'; + const type1 = 'foo'; + const type2 = 'bar'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + atSpace: jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.getSavedObjectAction(type1, 'bulk_create')]: false, + [mockActions.getSavedObjectAction(type2, 'bulk_create')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + const objects = [ + { type: type1 }, + { type: type1 }, + { type: type2 }, + ]; + const options = Symbol(); + + await expect(client.bulkCreate(objects, options)).rejects.toThrowError(mockErrors.forbiddenError); + + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [ + mockActions.getSavedObjectAction(type1, 'bulk_create'), + mockActions.getSavedObjectAction(type2, 'bulk_create'), + ]); + expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'bulk_create', + [type1, type2], + [mockActions.getSavedObjectAction(type1, 'bulk_create')], + { + objects, + options, + } + ); + }); + + test(`returns result of baseClient.bulkCreate when authorized`, async () => { + const spaceId = 'space_1'; + const username = Symbol(); + const type1 = 'foo'; + const type2 = 'bar'; + const returnValue = Symbol(); + const mockBaseClient = { + bulkCreate: jest.fn().mockReturnValue(returnValue) + }; + const mockActions = createMockActions(); + const mockCheckPrivileges = { + atSpace: jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.getSavedObjectAction(type1, 'bulk_create')]: true, + [mockActions.getSavedObjectAction(type2, 'bulk_create')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: mockBaseClient, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: null, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + const objects = [ + { type: type1, otherThing: 'sup' }, + { type: type2, otherThing: 'everyone' }, + ]; + const options = Symbol(); + + const result = await client.bulkCreate(objects, options); + + expect(result).toBe(returnValue); + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [ + mockActions.getSavedObjectAction(type1, 'bulk_create'), + mockActions.getSavedObjectAction(type2, 'bulk_create'), + ]); + expect(mockBaseClient.bulkCreate).toHaveBeenCalledWith(objects, options); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'bulk_create', [type1, type2], { + objects, + options, + }); + }); + }); + + describe('#delete', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + atSpace: jest.fn(async () => { + throw new Error('An actual error would happen here'); + }) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockActions = createMockActions(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + + await expect(client.delete(type)).rejects.toThrowError(mockErrors.generalError); + + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'delete')]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + atSpace: jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'delete')]: false, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + const id = Symbol(); + + await expect(client.delete(type, id)).rejects.toThrowError(mockErrors.forbiddenError); + + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'delete')]); + expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'delete', + [type], + [mockActions.getSavedObjectAction(type, 'delete')], + { + type, + id, + } + ); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of internalRepository.delete when authorized`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const username = Symbol(); + const returnValue = Symbol(); + const mockActions = createMockActions(); + const mockBaseClient = { + delete: jest.fn().mockReturnValue(returnValue) + }; + const mockCheckPrivileges = { + atSpace: jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'delete')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: mockBaseClient, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: null, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + const id = Symbol(); + const options = Symbol(); + + const result = await client.delete(type, id, options); + + expect(result).toBe(returnValue); + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'delete')]); + expect(mockBaseClient.delete).toHaveBeenCalledWith(type, id, options); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'delete', [type], { + type, + id, + options, + }); + }); + }); + + describe('#find', () => { + describe('type', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + atSpace: jest.fn(async () => { + throw new Error('An actual error would happen here'); + }) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockActions = createMockActions(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + + await expect(client.find({ type })).rejects.toThrowError(mockErrors.generalError); + + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'find')]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when type's singular and unauthorized`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + atSpace: jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'find')]: false, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + const options = { type }; + + await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError); + + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'find')]); + expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'find', + [type], + [mockActions.getSavedObjectAction(type, 'find')], + { + options + } + ); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when type's an array and unauthorized`, async () => { + const spaceId = 'space_1'; + const type1 = 'foo'; + const type2 = 'bar'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + atSpace: jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.getSavedObjectAction(type1, 'find')]: false, + [mockActions.getSavedObjectAction(type2, 'find')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + + const mockAuditLogger = createMockAuditLogger(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + const options = { type: [type1, type2] }; + + await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError); + + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [ + mockActions.getSavedObjectAction(type1, 'find'), + mockActions.getSavedObjectAction(type2, 'find') + ]); + expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'find', + [type1, type2], + [mockActions.getSavedObjectAction(type1, 'find')], + { + options + } + ); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.find when authorized`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const username = Symbol(); + const returnValue = Symbol(); + const mockActions = createMockActions(); + const mockBaseClient = { + find: jest.fn().mockReturnValue(returnValue) + }; + const mockCheckPrivileges = { + atSpace: jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'find')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: mockBaseClient, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: null, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + const options = { type }; + + const result = await client.find(options); + + expect(result).toBe(returnValue); + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'find')]); + expect(mockBaseClient.find).toHaveBeenCalledWith({ type }); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'find', [type], { + options, + }); + }); + }); + + describe('no type', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const spaceId = 'space_1'; + const type1 = 'foo'; + const type2 = 'bar'; + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + atSpace: jest.fn(async () => { + throw new Error('An actual error would happen here'); + }) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockActions = createMockActions(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [type1, type2], + spaces: mockSpaces, + }); + + await expect(client.find()).rejects.toThrowError(mockErrors.generalError); + + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [ + mockActions.getSavedObjectAction(type1, 'find'), + mockActions.getSavedObjectAction(type2, 'find'), + ]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + atSpace: jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'find')]: false, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [type], + spaces: mockSpaces, + }); + const options = Symbol(); + + await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError); + + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'find')]); + expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'find', + [type], + [mockActions.getSavedObjectAction(type, 'find')], + { + options + } + ); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.find when authorized`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const username = Symbol(); + const returnValue = Symbol(); + const mockActions = createMockActions(); + const mockBaseClient = { + find: jest.fn().mockReturnValue(returnValue) + }; + const mockCheckPrivileges = { + atSpace: jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'find')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: mockBaseClient, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: null, + request: mockRequest, + savedObjectTypes: [type], + spaces: mockSpaces, + }); + const options = { + bar: true, + }; + + const result = await client.find(options); + + expect(result).toBe(returnValue); + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'find')]); + expect(mockBaseClient.find).toHaveBeenCalledWith({ bar: true, type: ['foo'] }); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'find', [type], { + options: { + type: [type], + ...options, + } + }); + }); + + test(`specifies authorized types when calling repository.find()`, async () => { + const spaceId = 'space_1'; + const type1 = 'foo'; + const type2 = 'bar'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockBaseClient = { + find: jest.fn(), + }; + const mockCheckPrivileges = { + atSpace: jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.getSavedObjectAction(type1, 'find')]: false, + [mockActions.getSavedObjectAction(type2, 'find')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: mockBaseClient, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: null, + request: mockRequest, + savedObjectTypes: [type1, type2], + spaces: mockSpaces, + }); + + await client.find(); + + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [ + mockActions.getSavedObjectAction(type1, 'find'), + mockActions.getSavedObjectAction(type2, 'find'), + ]); + expect(mockBaseClient.find).toHaveBeenCalledWith(expect.objectContaining({ + type: [type2] + })); + }); + }); + }); + + describe('#bulkGet', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + atSpace: jest.fn(async () => { + throw new Error('An actual error would happen here'); + }) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockActions = createMockActions(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + + await expect(client.bulkGet([{ type }])).rejects.toThrowError(mockErrors.generalError); + + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'bulk_get')]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const spaceId = 'space_1'; + const type1 = 'foo'; + const type2 = 'bar'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + atSpace: jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.getSavedObjectAction(type1, 'bulk_get')]: false, + [mockActions.getSavedObjectAction(type2, 'bulk_get')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + const objects = [ + { type: type1 }, + { type: type1 }, + { type: type2 }, + ]; + const options = Symbol(); + + await expect(client.bulkGet(objects, options)).rejects.toThrowError(mockErrors.forbiddenError); + + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [ + mockActions.getSavedObjectAction(type1, 'bulk_get'), + mockActions.getSavedObjectAction(type2, 'bulk_get'), + ]); + expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'bulk_get', + [type1, type2], + [mockActions.getSavedObjectAction(type1, 'bulk_get')], + { + objects, + options, + } + ); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.bulkGet when authorized`, async () => { + const spaceId = 'space_1'; + const type1 = 'foo'; + const type2 = 'bar'; + const username = Symbol(); + const returnValue = Symbol(); + const mockActions = createMockActions(); + const mockBaseClient = { + bulkGet: jest.fn().mockReturnValue(returnValue) + }; + const mockCheckPrivileges = { + atSpace: jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.getSavedObjectAction(type1, 'bulk_get')]: true, + [mockActions.getSavedObjectAction(type2, 'bulk_get')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: mockBaseClient, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: null, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + const objects = [ + { type: type1, id: 'foo-id' }, + { type: type2, id: 'bar-id' }, + ]; + const options = Symbol(); + + const result = await client.bulkGet(objects, options); + + expect(result).toBe(returnValue); + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [ + mockActions.getSavedObjectAction(type1, 'bulk_get'), + mockActions.getSavedObjectAction(type2, 'bulk_get'), + ]); + expect(mockBaseClient.bulkGet).toHaveBeenCalledWith(objects, options); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'bulk_get', [type1, type2], { + objects, + options, + }); + }); + }); + + describe('#get', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + atSpace: jest.fn(async () => { + throw new Error('An actual error would happen here'); + }) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockActions = createMockActions(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + + await expect(client.get(type)).rejects.toThrowError(mockErrors.generalError); + + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'get')]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + atSpace: jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'get')]: false, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + const id = Symbol(); + const options = Symbol(); + + await expect(client.get(type, id, options)).rejects.toThrowError(mockErrors.forbiddenError); + + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'get')]); + expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'get', + [type], + [mockActions.getSavedObjectAction(type, 'get')], + { + type, + id, + options, + } + ); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.get when authorized`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const username = Symbol(); + const returnValue = Symbol(); + const mockActions = createMockActions(); + const mockBaseClient = { + get: jest.fn().mockReturnValue(returnValue) + }; + const mockCheckPrivileges = { + atSpace: jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'get')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: mockBaseClient, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: null, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + const id = Symbol(); + const options = Symbol(); + + const result = await client.get(type, id, options); + + expect(result).toBe(returnValue); + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'get')]); + expect(mockBaseClient.get).toHaveBeenCalledWith(type, id, options); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'get', [type], { + type, + id, + options + }); + }); + }); + + describe('#update', () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + atSpace: jest.fn(async () => { + throw new Error('An actual error would happen here'); + }) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockActions = createMockActions(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + + await expect(client.update(type)).rejects.toThrowError(mockErrors.generalError); + + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'update')]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const username = Symbol(); + const mockActions = createMockActions(); + const mockErrors = createMockErrors(); + const mockCheckPrivileges = { + atSpace: jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'update')]: false, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: null, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: mockErrors, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + const id = Symbol(); + const attributes = Symbol(); + const options = Symbol(); + + await expect(client.update(type, id, attributes, options)).rejects.toThrowError(mockErrors.forbiddenError); + + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'update')]); + expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + username, + 'update', + [type], + [mockActions.getSavedObjectAction(type, 'update')], + { + type, + id, + attributes, + options, + } + ); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.update when authorized`, async () => { + const spaceId = 'space_1'; + const type = 'foo'; + const username = Symbol(); + const returnValue = Symbol(); + const mockActions = createMockActions(); + const mockBaseClient = { + update: jest.fn().mockReturnValue(returnValue) + }; + const mockCheckPrivileges = { + atSpace: jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.getSavedObjectAction(type, 'update')]: true, + } + })) + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockRequest = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const mockSpaces = { + getSpaceId: jest.fn().mockReturnValue(spaceId) + }; + const client = new SecureSavedObjectsClientWrapper({ + actions: mockActions, + auditLogger: mockAuditLogger, + baseClient: mockBaseClient, + checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + errors: null, + request: mockRequest, + savedObjectTypes: [], + spaces: mockSpaces, + }); + const id = Symbol(); + const attributes = Symbol(); + const options = Symbol(); + + const result = await client.update(type, id, attributes, options); + + expect(result).toBe(returnValue); + expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.getSavedObjectAction(type, 'update')]); + expect(mockBaseClient.update).toHaveBeenCalledWith(type, id, attributes, options); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'update', [type], { + type, + id, + attributes, + options, + }); + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/api/public/roles/get.js b/x-pack/plugins/security/server/routes/api/public/roles/get.js index a0c7fd0ec4f39..797a76110d21a 100644 --- a/x-pack/plugins/security/server/routes/api/public/roles/get.js +++ b/x-pack/plugins/security/server/routes/api/public/roles/get.js @@ -5,8 +5,9 @@ */ import _ from 'lodash'; import Boom from 'boom'; +import { GLOBAL_RESOURCE } from '../../../../../common/constants'; import { wrapError } from '../../../../lib/errors'; -import { ALL_RESOURCE } from '../../../../../common/constants'; +import { spaceApplicationPrivilegesSerializer } from '../../../../lib/authorization'; export function initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, application) { @@ -19,19 +20,17 @@ export function initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, ); return resourcePrivileges.reduce((result, { resource, privileges }) => { - if (resource === ALL_RESOURCE) { + if (resource === GLOBAL_RESOURCE) { result.global = _.uniq([...result.global, ...privileges]); return result; } - const spacePrefix = 'space:'; - if (resource.startsWith(spacePrefix)) { - const spaceId = resource.slice(spacePrefix.length); - result.space[spaceId] = _.uniq([...result.space[spaceId] || [], ...privileges]); - return result; - } - - throw new Error(`Unknown application privilege resource: ${resource}`); + const spaceId = spaceApplicationPrivilegesSerializer.resource.deserialize(resource); + result.space[spaceId] = _.uniq([ + ...result.space[spaceId] || [], + ...privileges.map(privilege => spaceApplicationPrivilegesSerializer.privilege.deserialize(privilege)) + ]); + return result; }, { global: [], space: {}, diff --git a/x-pack/plugins/security/server/routes/api/public/roles/get.test.js b/x-pack/plugins/security/server/routes/api/public/roles/get.test.js index f0f3511e32497..28f754248d829 100644 --- a/x-pack/plugins/security/server/routes/api/public/roles/get.test.js +++ b/x-pack/plugins/security/server/routes/api/public/roles/get.test.js @@ -233,17 +233,17 @@ describe('GET roles', () => { applications: [ { application, - privileges: ['read'], + privileges: ['space_read'], resources: ['space:marketing'], }, { application, - privileges: ['all'], + privileges: ['space_all'], resources: ['space:marketing'], }, { application, - privileges: ['read'], + privileges: ['space_read'], resources: ['space:engineering'], }, ], @@ -602,17 +602,17 @@ describe('GET role', () => { applications: [ { application, - privileges: ['read'], + privileges: ['space_read'], resources: ['space:marketing'], }, { application, - privileges: ['all'], + privileges: ['space_all'], resources: ['space:marketing'], }, { application, - privileges: ['read'], + privileges: ['space_read'], resources: ['space:engineering'], }, ], diff --git a/x-pack/plugins/security/server/routes/api/public/roles/index.js b/x-pack/plugins/security/server/routes/api/public/roles/index.js index 5425af0a1202d..8bdde88123ee4 100644 --- a/x-pack/plugins/security/server/routes/api/public/roles/index.js +++ b/x-pack/plugins/security/server/routes/api/public/roles/index.js @@ -17,7 +17,7 @@ export function initPublicRolesApi(server) { const { application, actions } = server.plugins.security.authorization; const savedObjectTypes = server.savedObjects.types; - const privilegeMap = buildPrivilegeMap(savedObjectTypes, application, actions); + const privilegeMap = buildPrivilegeMap(savedObjectTypes, actions); initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, application); initPutRolesApi(server, callWithRequest, routePreCheckLicenseFn, privilegeMap, application); diff --git a/x-pack/plugins/security/server/routes/api/public/roles/put.js b/x-pack/plugins/security/server/routes/api/public/roles/put.js index 262e2259cbea3..0152453a10a26 100644 --- a/x-pack/plugins/security/server/routes/api/public/roles/put.js +++ b/x-pack/plugins/security/server/routes/api/public/roles/put.js @@ -6,61 +6,61 @@ import { pick, identity } from 'lodash'; import Joi from 'joi'; +import { GLOBAL_RESOURCE } from '../../../../../common/constants'; import { wrapError } from '../../../../lib/errors'; -import { ALL_RESOURCE } from '../../../../../common/constants'; +import { spaceApplicationPrivilegesSerializer } from '../../../../lib/authorization'; -const transformKibanaPrivilegesToEs = (application, kibanaPrivileges) => { - const kibanaApplicationPrivileges = []; - if (kibanaPrivileges.global && kibanaPrivileges.global.length) { - kibanaApplicationPrivileges.push({ - privileges: kibanaPrivileges.global, - application, - resources: [ALL_RESOURCE], - }); - } +export function initPutRolesApi( + server, + callWithRequest, + routePreCheckLicenseFn, + privilegeMap, + application +) { - if (kibanaPrivileges.space) { - for (const [spaceId, privileges] of Object.entries(kibanaPrivileges.space)) { + const transformKibanaPrivilegesToEs = (kibanaPrivileges) => { + const kibanaApplicationPrivileges = []; + if (kibanaPrivileges.global && kibanaPrivileges.global.length) { kibanaApplicationPrivileges.push({ - privileges: privileges, + privileges: kibanaPrivileges.global, application, - resources: [`space:${spaceId}`] + resources: [GLOBAL_RESOURCE], }); } - } - return kibanaApplicationPrivileges; -}; + if (kibanaPrivileges.space) { + for(const [spaceId, privileges] of Object.entries(kibanaPrivileges.space)) { + kibanaApplicationPrivileges.push({ + privileges: privileges.map(privilege => spaceApplicationPrivilegesSerializer.privilege.serialize(privilege)), + application, + resources: [spaceApplicationPrivilegesSerializer.resource.serialize(spaceId)] + }); + } + } -const transformRolesToEs = ( - application, - payload, - existingApplications = [] -) => { - const { elasticsearch = {}, kibana = {} } = payload; - const otherApplications = existingApplications.filter( - roleApplication => roleApplication.application !== application - ); + return kibanaApplicationPrivileges; + }; - return pick({ - metadata: payload.metadata, - cluster: elasticsearch.cluster || [], - indices: elasticsearch.indices || [], - run_as: elasticsearch.run_as || [], - applications: [ - ...transformKibanaPrivilegesToEs(application, kibana), - ...otherApplications, - ], - }, identity); -}; + const transformRolesToEs = ( + payload, + existingApplications = [] + ) => { + const { elasticsearch = {}, kibana = {} } = payload; + const otherApplications = existingApplications.filter( + roleApplication => roleApplication.application !== application + ); -export function initPutRolesApi( - server, - callWithRequest, - routePreCheckLicenseFn, - privilegeMap, - application -) { + return pick({ + metadata: payload.metadata, + cluster: elasticsearch.cluster || [], + indices: elasticsearch.indices || [], + run_as: elasticsearch.run_as || [], + applications: [ + ...transformKibanaPrivilegesToEs(kibana), + ...otherApplications, + ], + }, identity); + }; const schema = Joi.object().keys({ metadata: Joi.object().optional(), @@ -78,8 +78,8 @@ export function initPutRolesApi( run_as: Joi.array().items(Joi.string()), }), kibana: Joi.object().keys({ - global: Joi.array().items(Joi.string().valid(Object.keys(privilegeMap))), - space: Joi.object().pattern(/^/, Joi.array().items(Joi.string().valid(Object.keys(privilegeMap)))) + global: Joi.array().items(Joi.string().valid(Object.keys(privilegeMap.global))), + space: Joi.object().pattern(/^[a-z0-9_-]+$/, Joi.array().items(Joi.string().valid(Object.keys(privilegeMap.space)))) }) }); @@ -95,7 +95,6 @@ export function initPutRolesApi( }); const body = transformRolesToEs( - application, request.payload, existingRoleResponse[name] ? existingRoleResponse[name].applications : [] ); diff --git a/x-pack/plugins/security/server/routes/api/public/roles/put.test.js b/x-pack/plugins/security/server/routes/api/public/roles/put.test.js index d159c98c59eb4..ffa7b247f99d2 100644 --- a/x-pack/plugins/security/server/routes/api/public/roles/put.test.js +++ b/x-pack/plugins/security/server/routes/api/public/roles/put.test.js @@ -7,7 +7,7 @@ import Hapi from 'hapi'; import Boom from 'boom'; import { initPutRolesApi } from './put'; -import { ALL_RESOURCE } from '../../../../../common/constants'; +import { GLOBAL_RESOURCE } from '../../../../../common/constants'; const application = 'kibana-.kibana'; @@ -20,9 +20,16 @@ const createMockServer = () => { const defaultPreCheckLicenseImpl = (request, reply) => reply(); const privilegeMap = { - 'test-kibana-privilege-1': {}, - 'test-kibana-privilege-2': {}, - 'test-kibana-privilege-3': {}, + global: { + 'test-global-privilege-1': [], + 'test-global-privilege-2': [], + 'test-global-privilege-3': [], + }, + space: { + 'test-space-privilege-1': [], + 'test-space-privilege-2': [], + 'test-space-privilege-3': [], + } }; const putRoleTest = ( @@ -112,7 +119,7 @@ describe('PUT role', () => { }, }); - putRoleTest(`only allows known Kibana privileges`, { + putRoleTest(`only allows known Kibana global privileges`, { name: 'foo-role', payload: { kibana: { @@ -124,7 +131,7 @@ describe('PUT role', () => { result: { error: 'Bad Request', //eslint-disable-next-line max-len - message: `child \"kibana\" fails because [child \"global\" fails because [\"global\" at position 0 fails because [\"0\" must be one of [test-kibana-privilege-1, test-kibana-privilege-2, test-kibana-privilege-3]]]]`, + message: `child \"kibana\" fails because [child \"global\" fails because [\"global\" at position 0 fails because [\"0\" must be one of [test-global-privilege-1, test-global-privilege-2, test-global-privilege-3]]]]`, statusCode: 400, validation: { keys: ['kibana.global.0'], @@ -134,6 +141,76 @@ describe('PUT role', () => { }, }); + putRoleTest(`only allows known Kibana space privileges`, { + name: 'foo-role', + payload: { + kibana: { + space: { + quz: ['foo'] + } + } + }, + asserts: { + statusCode: 400, + result: { + error: 'Bad Request', + //eslint-disable-next-line max-len + message: `child \"kibana\" fails because [child \"space\" fails because [child \"quz\" fails because [\"quz\" at position 0 fails because [\"0\" must be one of [test-space-privilege-1, test-space-privilege-2, test-space-privilege-3]]]]]`, + statusCode: 400, + validation: { + keys: ['kibana.space.quz.0'], + source: 'payload', + }, + }, + }, + }); + + putRoleTest(`doesn't allow * space ID`, { + name: 'foo-role', + payload: { + kibana: { + space: { + '*': ['test-space-privilege-1'] + } + } + }, + asserts: { + statusCode: 400, + result: { + error: 'Bad Request', + message: `child \"kibana\" fails because [child \"space\" fails because [\"*\" is not allowed]]`, + statusCode: 400, + validation: { + keys: ['kibana.space.*'], + source: 'payload', + }, + }, + }, + }); + + putRoleTest(`doesn't allow * in a space ID`, { + name: 'foo-role', + payload: { + kibana: { + space: { + 'foo-*': ['test-space-privilege-1'] + } + } + }, + asserts: { + statusCode: 400, + result: { + error: 'Bad Request', + message: `child \"kibana\" fails because [child \"space\" fails because [\"foo-*\" is not allowed]]`, + statusCode: 400, + validation: { + keys: ['kibana.space.foo-*'], + source: 'payload', + }, + }, + }, + }); + putRoleTest(`returns result of routePreCheckLicense`, { name: 'foo-role', payload: {}, @@ -199,10 +276,10 @@ describe('PUT role', () => { run_as: ['test-run-as-1', 'test-run-as-2'], }, kibana: { - global: ['test-kibana-privilege-1', 'test-kibana-privilege-2', 'test-kibana-privilege-3'], + global: ['test-global-privilege-1', 'test-global-privilege-2', 'test-global-privilege-3'], space: { - 'test-space-1': ['test-kibana-privilege-1', 'test-kibana-privilege-2'], - 'test-space-2': ['test-kibana-privilege-3'], + 'test-space-1': ['test-space-privilege-1', 'test-space-privilege-2'], + 'test-space-2': ['test-space-privilege-3'], } }, }, @@ -220,24 +297,24 @@ describe('PUT role', () => { { application, privileges: [ - 'test-kibana-privilege-1', - 'test-kibana-privilege-2', - 'test-kibana-privilege-3' + 'test-global-privilege-1', + 'test-global-privilege-2', + 'test-global-privilege-3' ], - resources: [ALL_RESOURCE], + resources: [GLOBAL_RESOURCE], }, { application, privileges: [ - 'test-kibana-privilege-1', - 'test-kibana-privilege-2' + 'space_test-space-privilege-1', + 'space_test-space-privilege-2' ], resources: ['space:test-space-1'] }, { application, privileges: [ - 'test-kibana-privilege-3', + 'space_test-space-privilege-3', ], resources: ['space:test-space-2'] }, @@ -290,10 +367,10 @@ describe('PUT role', () => { run_as: ['test-run-as-1', 'test-run-as-2'], }, kibana: { - global: ['test-kibana-privilege-1', 'test-kibana-privilege-2', 'test-kibana-privilege-3'], + global: ['test-global-privilege-1', 'test-global-privilege-2', 'test-global-privilege-3'], space: { - 'test-space-1': ['test-kibana-privilege-1', 'test-kibana-privilege-2'], - 'test-space-2': ['test-kibana-privilege-3'], + 'test-space-1': ['test-space-privilege-1', 'test-space-privilege-2'], + 'test-space-2': ['test-space-privilege-3'], } }, }, @@ -343,24 +420,24 @@ describe('PUT role', () => { { application, privileges: [ - 'test-kibana-privilege-1', - 'test-kibana-privilege-2', - 'test-kibana-privilege-3' + 'test-global-privilege-1', + 'test-global-privilege-2', + 'test-global-privilege-3' ], - resources: [ALL_RESOURCE], + resources: [GLOBAL_RESOURCE], }, { application, privileges: [ - 'test-kibana-privilege-1', - 'test-kibana-privilege-2' + 'space_test-space-privilege-1', + 'space_test-space-privilege-2' ], resources: ['space:test-space-1'] }, { application, privileges: [ - 'test-kibana-privilege-3', + 'space_test-space-privilege-3', ], resources: ['space:test-space-2'] }, @@ -414,9 +491,9 @@ describe('PUT role', () => { }, kibana: { global: [ - 'test-kibana-privilege-1', - 'test-kibana-privilege-2', - 'test-kibana-privilege-3' + 'test-global-privilege-1', + 'test-global-privilege-2', + 'test-global-privilege-3' ], }, }, @@ -471,11 +548,11 @@ describe('PUT role', () => { { application, privileges: [ - 'test-kibana-privilege-1', - 'test-kibana-privilege-2', - 'test-kibana-privilege-3' + 'test-global-privilege-1', + 'test-global-privilege-2', + 'test-global-privilege-3' ], - resources: [ALL_RESOURCE], + resources: [GLOBAL_RESOURCE], }, { application: 'logstash-foo', diff --git a/x-pack/plugins/security/server/routes/api/v1/__tests__/authenticate.js b/x-pack/plugins/security/server/routes/api/v1/__tests__/authenticate.js index 4139569e51b34..155fc041f24a5 100644 --- a/x-pack/plugins/security/server/routes/api/v1/__tests__/authenticate.js +++ b/x-pack/plugins/security/server/routes/api/v1/__tests__/authenticate.js @@ -15,7 +15,6 @@ import { AuthenticationResult } from '../../../../../server/lib/authentication/a import { BasicCredentials } from '../../../../../server/lib/authentication/providers/basic'; import { initAuthenticateApi } from '../authenticate'; import { DeauthenticationResult } from '../../../../lib/authentication/deauthentication_result'; -import { CHECK_PRIVILEGES_RESULT } from '../../../../lib/authorization'; describe('Authentication routes', () => { let serverStub; @@ -34,7 +33,7 @@ describe('Authentication routes', () => { let loginRoute; let request; let authenticateStub; - let checkPrivilegesWithRequestStub; + let authorizationModeStub; beforeEach(() => { loginRoute = serverStub.route @@ -50,7 +49,7 @@ describe('Authentication routes', () => { authenticateStub = serverStub.plugins.security.authenticate.withArgs( sinon.match(BasicCredentials.decorateRequest({ headers: {} }, 'user', 'password')) ); - checkPrivilegesWithRequestStub = serverStub.plugins.security.authorization.checkPrivilegesWithRequest; + authorizationModeStub = serverStub.plugins.security.authorization.mode; }); it('correctly defines route.', async () => { @@ -134,59 +133,37 @@ describe('Authentication routes', () => { const getDeprecationMessage = username => `${username} relies on index privileges on the Kibana index. This is deprecated and will be removed in Kibana 7.0`; - it(`returns user data and doesn't log deprecation warning if checkPrivileges result is authorized.`, async () => { + it(`returns user data and doesn't log deprecation warning if authorization.mode.useRbacForRequest returns true.`, async () => { const user = { username: 'user' }; authenticateStub.returns( Promise.resolve(AuthenticationResult.succeeded(user)) ); - const checkPrivilegesStub = sinon.stub().returns({ result: CHECK_PRIVILEGES_RESULT.AUTHORIZED }); - checkPrivilegesWithRequestStub.returns(checkPrivilegesStub); + authorizationModeStub.useRbacForRequest.returns(true); await loginRoute.handler(request, replyStub); - sinon.assert.calledWithExactly(checkPrivilegesWithRequestStub, request); - sinon.assert.calledWithExactly(checkPrivilegesStub, [serverStub.plugins.security.authorization.actions.login]); + sinon.assert.calledWithExactly(authorizationModeStub.useRbacForRequest, request); sinon.assert.neverCalledWith(serverStub.log, ['warning', 'deprecated', 'security'], getDeprecationMessage(user.username)); sinon.assert.notCalled(replyStub); sinon.assert.calledOnce(replyStub.continue); sinon.assert.calledWithExactly(replyStub.continue, { credentials: user }); }); - it(`returns user data and logs deprecation warning if checkPrivileges result is legacy.`, async () => { + it(`returns user data and logs deprecation warning if authorization.mode.useRbacForRequest returns false.`, async () => { const user = { username: 'user' }; authenticateStub.returns( Promise.resolve(AuthenticationResult.succeeded(user)) ); - const checkPrivilegesStub = sinon.stub().returns({ result: CHECK_PRIVILEGES_RESULT.LEGACY }); - checkPrivilegesWithRequestStub.returns(checkPrivilegesStub); + authorizationModeStub.useRbacForRequest.returns(false); await loginRoute.handler(request, replyStub); - sinon.assert.calledWithExactly(checkPrivilegesWithRequestStub, request); - sinon.assert.calledWithExactly(checkPrivilegesStub, [serverStub.plugins.security.authorization.actions.login]); + sinon.assert.calledWithExactly(authorizationModeStub.useRbacForRequest, request); sinon.assert.calledWith(serverStub.log, ['warning', 'deprecated', 'security'], getDeprecationMessage(user.username)); sinon.assert.notCalled(replyStub); sinon.assert.calledOnce(replyStub.continue); sinon.assert.calledWithExactly(replyStub.continue, { credentials: user }); }); - - it(`returns user data and doesn't log deprecation warning if checkPrivileges result is unauthorized.`, async () => { - const user = { username: 'user' }; - authenticateStub.returns( - Promise.resolve(AuthenticationResult.succeeded(user)) - ); - const checkPrivilegesStub = sinon.stub().returns({ result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED }); - checkPrivilegesWithRequestStub.returns(checkPrivilegesStub); - - await loginRoute.handler(request, replyStub); - - sinon.assert.calledWithExactly(checkPrivilegesWithRequestStub, request); - sinon.assert.calledWithExactly(checkPrivilegesStub, [serverStub.plugins.security.authorization.actions.login]); - sinon.assert.neverCalledWith(serverStub.log, ['warning', 'deprecated', 'security'], getDeprecationMessage(user.username)); - sinon.assert.notCalled(replyStub); - sinon.assert.calledOnce(replyStub.continue); - sinon.assert.calledWithExactly(replyStub.continue, { credentials: user }); - }); }); }); diff --git a/x-pack/plugins/security/server/routes/api/v1/authenticate.js b/x-pack/plugins/security/server/routes/api/v1/authenticate.js index e92a0a2a9536c..c791def49e9d2 100644 --- a/x-pack/plugins/security/server/routes/api/v1/authenticate.js +++ b/x-pack/plugins/security/server/routes/api/v1/authenticate.js @@ -9,7 +9,6 @@ import Joi from 'joi'; import { wrapError } from '../../../lib/errors'; import { BasicCredentials } from '../../../../server/lib/authentication/providers/basic'; import { canRedirectRequest } from '../../../lib/can_redirect_request'; -import { CHECK_PRIVILEGES_RESULT } from '../../../../server/lib/authorization'; export function initAuthenticateApi(server) { @@ -41,9 +40,7 @@ export function initAuthenticateApi(server) { } const { authorization } = server.plugins.security; - const checkPrivileges = authorization.checkPrivilegesWithRequest(request); - const privilegeCheck = await checkPrivileges([authorization.actions.login]); - if (privilegeCheck.result === CHECK_PRIVILEGES_RESULT.LEGACY) { + if (!authorization.mode.useRbacForRequest(request)) { const msg = `${username} relies on index privileges on the Kibana index. This is deprecated and will be removed in Kibana 7.0`; server.log(['warning', 'deprecated', 'security'], msg); } diff --git a/x-pack/plugins/security/server/routes/api/v1/privileges.js b/x-pack/plugins/security/server/routes/api/v1/privileges.js deleted file mode 100644 index 4bf1b2c5cc7a5..0000000000000 --- a/x-pack/plugins/security/server/routes/api/v1/privileges.js +++ /dev/null @@ -1,25 +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 { buildPrivilegeMap } from '../../../lib/authorization'; - -export function initPrivilegesApi(server) { - const { authorization } = server.plugins.security; - const savedObjectTypes = server.savedObjects.types; - - server.route({ - method: 'GET', - path: '/api/security/v1/privileges', - handler(request, reply) { - // we're returning our representation of the privileges, as opposed to the ones that are stored - // in Elasticsearch because our current thinking is that we'll associate additional structure/metadata - // with our view of them to allow users to more efficiently edit privileges for roles, and serialize it - // into a different structure for enforcement within Elasticsearch - const privileges = buildPrivilegeMap(savedObjectTypes, authorization.application, authorization.actions); - reply(Object.values(privileges)); - } - }); -} diff --git a/x-pack/plugins/spaces/index.js b/x-pack/plugins/spaces/index.js index 9992dfd148301..b7cfeb94f958e 100644 --- a/x-pack/plugins/spaces/index.js +++ b/x-pack/plugins/spaces/index.js @@ -19,6 +19,9 @@ import { wrapError } from './server/lib/errors'; import mappings from './mappings.json'; import { spacesSavedObjectsClientWrapperFactory } from './server/lib/saved_objects_client/saved_objects_client_wrapper_factory'; import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize'; +import { SpacesClient } from './server/lib/spaces_client'; +import { SpacesAuditLogger } from './server/lib/audit_logger'; +import { AuditLogger } from '../../server/lib/audit_logger'; export const spaces = (kibana) => new kibana.Plugin({ id: 'spaces', @@ -58,10 +61,11 @@ export const spaces = (kibana) => new kibana.Plugin({ }; }, replaceInjectedVars: async function (vars, request, server) { + const spacesClient = server.plugins.spaces.spacesClient.getScopedClient(request); try { vars.activeSpace = { valid: true, - space: await getActiveSpace(request.getSavedObjectsClient(), request.getBasePath(), server.config().get('server.basePath')) + space: await getActiveSpace(spacesClient, request.getBasePath(), server.config().get('server.basePath')) }; } catch (e) { vars.activeSpace = { @@ -89,11 +93,26 @@ export const spaces = (kibana) => new kibana.Plugin({ validateConfig(config, message => server.log(['spaces', 'warning'], message)); const spacesService = createSpacesService(server); - server.decorate('server', 'spaces', spacesService); + server.expose('getSpaceId', (request) => spacesService.getSpaceId(request)); - const { addScopedSavedObjectsClientWrapperFactory } = server.savedObjects; - addScopedSavedObjectsClientWrapperFactory( - spacesSavedObjectsClientWrapperFactory(spacesService) + const spacesAuditLogger = new SpacesAuditLogger(config, new AuditLogger(server, 'spaces')); + + server.expose('spacesClient', { + getScopedClient: (request) => { + const adminCluster = server.plugins.elasticsearch.getCluster('admin'); + const { callWithRequest, callWithInternalUser } = adminCluster; + const callCluster = (...args) => callWithRequest(request, ...args); + const { savedObjects } = server; + const internalRepository = savedObjects.getSavedObjectsRepository(callWithInternalUser); + const callWithRequestRepository = savedObjects.getSavedObjectsRepository(callCluster); + const authorization = server.plugins.security ? server.plugins.security.authorization : null; + return new SpacesClient(spacesAuditLogger, authorization, callWithRequestRepository, internalRepository, request); + } + }); + + const { addScopedSavedObjectsClientWrapperFactory, types } = server.savedObjects; + addScopedSavedObjectsClientWrapperFactory(Number.MAX_VALUE, + spacesSavedObjectsClientWrapperFactory(spacesService, types) ); server.addScopedTutorialContextFactory( diff --git a/x-pack/plugins/spaces/server/lib/__snapshots__/spaces_client.test.ts.snap b/x-pack/plugins/spaces/server/lib/__snapshots__/spaces_client.test.ts.snap new file mode 100644 index 0000000000000..4b0c0274cedf9 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/__snapshots__/spaces_client.test.ts.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#create useRbacForRequest is true throws Boom.forbidden if the user isn't authorized at space 1`] = `"Unauthorized to create spaces"`; + +exports[`#delete authorization is null throws Boom.badRequest when the space is reserved 1`] = `"This Space cannot be deleted because it is reserved."`; + +exports[`#delete authorization.mode.useRbacForRequest returns false throws Boom.badRequest when the space is reserved 1`] = `"This Space cannot be deleted because it is reserved."`; + +exports[`#delete authorization.mode.useRbacForRequest returns true throws Boom.badRequest if the user is authorized but the space is reserved 1`] = `"This Space cannot be deleted because it is reserved."`; + +exports[`#delete authorization.mode.useRbacForRequest returns true throws Boom.forbidden if the user isn't authorized 1`] = `"Unauthorized to delete spaces"`; + +exports[`#get useRbacForRequest is true throws Boom.forbidden if the user isn't authorized at space 1`] = `"Unauthorized to get foo-space space"`; + +exports[`#getAll useRbacForRequest is true throws Boom.forbidden when user isn't authorized for any spaces 1`] = `"Forbidden"`; + +exports[`#update useRbacForRequest is true throws Boom.forbidden when user isn't authorized at space 1`] = `"Unauthorized to update spaces"`; diff --git a/x-pack/plugins/spaces/server/lib/audit_logger.test.ts b/x-pack/plugins/spaces/server/lib/audit_logger.test.ts new file mode 100644 index 0000000000000..53d0befd01380 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/audit_logger.test.ts @@ -0,0 +1,140 @@ +/* + * 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 { SpacesAuditLogger } from './audit_logger'; + +const createMockConfig = (settings: any) => { + const mockConfig = { + get: jest.fn(), + }; + + mockConfig.get.mockImplementation(key => { + return settings[key]; + }); + + return mockConfig; +}; + +const createMockAuditLogger = () => { + return { + log: jest.fn(), + }; +}; + +describe(`#savedObjectsAuthorizationFailure`, () => { + test(`doesn't log anything when xpack.security.audit.enabled is false`, () => { + const config = createMockConfig({ + 'xpack.security.audit.enabled': false, + }); + const auditLogger = createMockAuditLogger(); + + const securityAuditLogger = new SpacesAuditLogger(config, auditLogger); + securityAuditLogger.spacesAuthorizationFailure('foo-user', 'foo-action'); + + expect(auditLogger.log).toHaveBeenCalledTimes(0); + }); + + test('logs with spaceIds via auditLogger when xpack.security.audit.enabled is true', () => { + const config = createMockConfig({ + 'xpack.security.audit.enabled': true, + }); + const auditLogger = createMockAuditLogger(); + const securityAuditLogger = new SpacesAuditLogger(config, auditLogger); + const username = 'foo-user'; + const action = 'foo-action'; + const spaceIds = ['foo-space-1', 'foo-space-2']; + + securityAuditLogger.spacesAuthorizationFailure(username, action, spaceIds); + + expect(auditLogger.log).toHaveBeenCalledWith( + 'spaces_authorization_failure', + expect.stringContaining(`${username} unauthorized to ${action} ${spaceIds.join(',')} spaces`), + { + username, + action, + spaceIds, + } + ); + }); + + test('logs without spaceIds via auditLogger when xpack.security.audit.enabled is true', () => { + const config = createMockConfig({ + 'xpack.security.audit.enabled': true, + }); + const auditLogger = createMockAuditLogger(); + const securityAuditLogger = new SpacesAuditLogger(config, auditLogger); + const username = 'foo-user'; + const action = 'foo-action'; + + securityAuditLogger.spacesAuthorizationFailure(username, action); + + expect(auditLogger.log).toHaveBeenCalledWith( + 'spaces_authorization_failure', + expect.stringContaining(`${username} unauthorized to ${action} spaces`), + { + username, + action, + } + ); + }); +}); + +describe(`#savedObjectsAuthorizationSuccess`, () => { + test(`doesn't log anything when xpack.security.audit.enabled is false`, () => { + const config = createMockConfig({ + 'xpack.security.audit.enabled': false, + }); + const auditLogger = createMockAuditLogger(); + + const securityAuditLogger = new SpacesAuditLogger(config, auditLogger); + securityAuditLogger.spacesAuthorizationSuccess('foo-user', 'foo-action'); + + expect(auditLogger.log).toHaveBeenCalledTimes(0); + }); + + test('logs with spaceIds via auditLogger when xpack.security.audit.enabled is true', () => { + const config = createMockConfig({ + 'xpack.security.audit.enabled': true, + }); + const auditLogger = createMockAuditLogger(); + const securityAuditLogger = new SpacesAuditLogger(config, auditLogger); + const username = 'foo-user'; + const action = 'foo-action'; + const spaceIds = ['foo-space-1', 'foo-space-2']; + + securityAuditLogger.spacesAuthorizationSuccess(username, action, spaceIds); + + expect(auditLogger.log).toHaveBeenCalledWith( + 'spaces_authorization_success', + expect.stringContaining(`${username} authorized to ${action} ${spaceIds.join(',')} spaces`), + { + username, + action, + spaceIds, + } + ); + }); + + test('logs without spaceIds via auditLogger when xpack.security.audit.enabled is true', () => { + const config = createMockConfig({ + 'xpack.security.audit.enabled': true, + }); + const auditLogger = createMockAuditLogger(); + const securityAuditLogger = new SpacesAuditLogger(config, auditLogger); + const username = 'foo-user'; + const action = 'foo-action'; + + securityAuditLogger.spacesAuthorizationSuccess(username, action); + + expect(auditLogger.log).toHaveBeenCalledWith( + 'spaces_authorization_success', + expect.stringContaining(`${username} authorized to ${action} spaces`), + { + username, + action, + } + ); + }); +}); diff --git a/x-pack/plugins/spaces/server/lib/audit_logger.ts b/x-pack/plugins/spaces/server/lib/audit_logger.ts new file mode 100644 index 0000000000000..b9bd3f5fe0399 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/audit_logger.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export class SpacesAuditLogger { + private readonly enabled: boolean; + private readonly auditLogger: any; + + constructor(config: any, auditLogger: any) { + this.enabled = config.get('xpack.security.audit.enabled'); + this.auditLogger = auditLogger; + } + public spacesAuthorizationFailure(username: string, action: string, spaceIds?: string[]) { + if (!this.enabled) { + return; + } + + this.auditLogger.log( + 'spaces_authorization_failure', + `${username} unauthorized to ${action}${spaceIds ? ' ' + spaceIds.join(',') : ''} spaces`, + { + username, + action, + spaceIds, + } + ); + } + + public spacesAuthorizationSuccess(username: string, action: string, spaceIds?: string[]) { + if (!this.enabled) { + return; + } + + this.auditLogger.log( + 'spaces_authorization_success', + `${username} authorized to ${action}${spaceIds ? ' ' + spaceIds.join(',') : ''} spaces`, + { + username, + action, + spaceIds, + } + ); + } +} diff --git a/x-pack/plugins/spaces/server/lib/get_active_space.js b/x-pack/plugins/spaces/server/lib/get_active_space.js index 5ab9bb744b3c6..143ed1b3e5dda 100644 --- a/x-pack/plugins/spaces/server/lib/get_active_space.js +++ b/x-pack/plugins/spaces/server/lib/get_active_space.js @@ -4,34 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; import { wrapError } from './errors'; import { getSpaceIdFromPath } from './spaces_url_parser'; -export async function getActiveSpace(savedObjectsClient, requestBasePath, serverBasePath) { +export async function getActiveSpace(spacesClient, requestBasePath, serverBasePath) { const spaceId = getSpaceIdFromPath(requestBasePath, serverBasePath); - let space; - try { - space = await getSpaceById(savedObjectsClient, spaceId); + return spacesClient.get(spaceId); } catch (e) { throw wrapError(e); } - - if (!space) { - throw Boom.notFound( - `The Space you requested could not be found. Please select a different Space to continue.` - ); - } - - return { - id: space.id, - ...space.attributes - }; -} - -async function getSpaceById(savedObjectsClient, spaceId) { - return savedObjectsClient.get('space', spaceId); } diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap b/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap index 8b1a258138355..e52af9a98001a 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap @@ -1,29 +1,65 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`default space #bulkCreate throws error if objects type is space 1`] = `"Spaces can not be accessed using the SavedObjectsClient"`; + exports[`default space #bulkCreate throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; +exports[`default space #bulkGet throws error if objects type is space 1`] = `"Spaces can not be accessed using the SavedObjectsClient"`; + exports[`default space #bulkGet throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; exports[`default space #create throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; +exports[`default space #create throws error if type is space 1`] = `"Spaces can not be accessed using the SavedObjectsClient"`; + exports[`default space #delete throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; +exports[`default space #delete throws error if type is space 1`] = `"Spaces can not be accessed using the SavedObjectsClient"`; + +exports[`default space #find if options.type isn't provided specifies options.type based on the types excluding the space 1`] = `"Spaces can not be accessed using the SavedObjectsClient"`; + exports[`default space #find throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; +exports[`default space #find throws error if options.type is array containing space 1`] = `"Spaces can not be accessed using the SavedObjectsClient"`; + +exports[`default space #find throws error if options.type is space 1`] = `"Spaces can not be accessed using the SavedObjectsClient"`; + exports[`default space #get throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; +exports[`default space #get throws error if type is space 1`] = `"Spaces can not be accessed using the SavedObjectsClient"`; + exports[`default space #update throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; +exports[`default space #update throws error if type is space 1`] = `"Spaces can not be accessed using the SavedObjectsClient"`; + +exports[`space_1 space #bulkCreate throws error if objects type is space 1`] = `"Spaces can not be accessed using the SavedObjectsClient"`; + exports[`space_1 space #bulkCreate throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; +exports[`space_1 space #bulkGet throws error if objects type is space 1`] = `"Spaces can not be accessed using the SavedObjectsClient"`; + exports[`space_1 space #bulkGet throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; exports[`space_1 space #create throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; +exports[`space_1 space #create throws error if type is space 1`] = `"Spaces can not be accessed using the SavedObjectsClient"`; + exports[`space_1 space #delete throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; +exports[`space_1 space #delete throws error if type is space 1`] = `"Spaces can not be accessed using the SavedObjectsClient"`; + +exports[`space_1 space #find if options.type isn't provided specifies options.type based on the types excluding the space 1`] = `"Spaces can not be accessed using the SavedObjectsClient"`; + exports[`space_1 space #find throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; +exports[`space_1 space #find throws error if options.type is array containing space 1`] = `"Spaces can not be accessed using the SavedObjectsClient"`; + +exports[`space_1 space #find throws error if options.type is space 1`] = `"Spaces can not be accessed using the SavedObjectsClient"`; + exports[`space_1 space #get throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; +exports[`space_1 space #get throws error if type is space 1`] = `"Spaces can not be accessed using the SavedObjectsClient"`; + exports[`space_1 space #update throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; + +exports[`space_1 space #update throws error if type is space 1`] = `"Spaces can not be accessed using the SavedObjectsClient"`; diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper_factory.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper_factory.js index 3f7c0cbe2124e..da7a448d86d2d 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper_factory.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper_factory.js @@ -6,10 +6,11 @@ import { SpacesSavedObjectsClient } from './spaces_saved_objects_client'; -export function spacesSavedObjectsClientWrapperFactory(spacesService) { +export function spacesSavedObjectsClientWrapperFactory(spacesService, types) { return ({ client, request }) => new SpacesSavedObjectsClient({ baseClient: client, request, spacesService, + types, }); } diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index 7f66b145e593e..92c9385cdda25 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -6,17 +6,53 @@ import { DEFAULT_SPACE_ID } from '../../../common/constants'; +const coerceToArray = (param) => { + if (Array.isArray(param)) { + return param; + } + + return [param]; +}; + +const getNamespace = (spaceId) => { + if (spaceId === DEFAULT_SPACE_ID) { + return undefined; + } + + return spaceId; +}; + +const throwErrorIfNamespaceSpecified = (options) => { + if (options.namespace) { + throw new Error('Spaces currently determines the namespaces'); + } +}; + +const throwErrorIfTypeIsSpace = (type) => { + if (type === 'space') { + throw new Error('Spaces can not be accessed using the SavedObjectsClient'); + } +}; + +const throwErrorIfTypesContainsSpace = (types) => { + for (const type of types) { + throwErrorIfTypeIsSpace(type); + } +}; + export class SpacesSavedObjectsClient { constructor(options) { const { baseClient, request, spacesService, + types, } = options; this.errors = baseClient.errors; this._client = baseClient; this._spaceId = spacesService.getSpaceId(request); + this._types = types; } /** @@ -31,13 +67,12 @@ export class SpacesSavedObjectsClient { * @returns {promise} - { id, type, version, attributes } */ async create(type, attributes = {}, options = {}) { - if (options.namespace) { - throw new Error('Spaces currently determines the namespaces'); - } + throwErrorIfTypeIsSpace(type); + throwErrorIfNamespaceSpecified(options); return await this._client.create(type, attributes, { ...options, - namespace: this._getNamespace(this._spaceId) + namespace: getNamespace(this._spaceId) }); } @@ -51,13 +86,12 @@ export class SpacesSavedObjectsClient { * @returns {promise} - { saved_objects: [{ id, type, version, attributes, error: { message } }]} */ async bulkCreate(objects, options = {}) { - if (options.namespace) { - throw new Error('Spaces currently determines the namespaces'); - } + throwErrorIfTypesContainsSpace(objects.map(object => object.type)); + throwErrorIfNamespaceSpecified(options); return await this._client.bulkCreate(objects, { ...options, - namespace: this._getNamespace(this._spaceId) + namespace: getNamespace(this._spaceId) }); } @@ -71,13 +105,12 @@ export class SpacesSavedObjectsClient { * @returns {promise} */ async delete(type, id, options = {}) { - if (options.namespace) { - throw new Error('Spaces currently determines the namespaces'); - } + throwErrorIfTypeIsSpace(type); + throwErrorIfNamespaceSpecified(options); return await this._client.delete(type, id, { ...options, - namespace: this._getNamespace(this._spaceId) + namespace: getNamespace(this._spaceId) }); } @@ -96,13 +129,13 @@ export class SpacesSavedObjectsClient { * @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page } */ async find(options = {}) { - if (options.namespace) { - throw new Error('Spaces currently determines the namespaces'); - } + throwErrorIfTypesContainsSpace(coerceToArray(options.type)); + throwErrorIfNamespaceSpecified(options); return await this._client.find({ ...options, - namespace: this._getNamespace(this._spaceId) + type: (options.type ? coerceToArray(options.type) : this._types).filter(type => type !== 'space'), + namespace: getNamespace(this._spaceId) }); } @@ -121,13 +154,12 @@ export class SpacesSavedObjectsClient { * ]) */ async bulkGet(objects = [], options = {}) { - if (options.namespace) { - throw new Error('Spaces currently determines the namespaces'); - } + throwErrorIfTypesContainsSpace(objects.map(object => object.type)); + throwErrorIfNamespaceSpecified(options); return await this._client.bulkGet(objects, { ...options, - namespace: this._getNamespace(this._spaceId) + namespace: getNamespace(this._spaceId) }); } @@ -141,13 +173,12 @@ export class SpacesSavedObjectsClient { * @returns {promise} - { id, type, version, attributes } */ async get(type, id, options = {}) { - if (options.namespace) { - throw new Error('Spaces currently determines the namespaces'); - } + throwErrorIfTypeIsSpace(type); + throwErrorIfNamespaceSpecified(options); return await this._client.get(type, id, { ...options, - namespace: this._getNamespace(this._spaceId) + namespace: getNamespace(this._spaceId) }); } @@ -162,21 +193,12 @@ export class SpacesSavedObjectsClient { * @returns {promise} */ async update(type, id, attributes, options = {}) { - if (options.namespace) { - throw new Error('Spaces currently determines the namespaces'); - } + throwErrorIfTypeIsSpace(type); + throwErrorIfNamespaceSpecified(options); return await this._client.update(type, id, attributes, { ...options, - namespace: this._getNamespace(this._spaceId) + namespace: getNamespace(this._spaceId) }); } - - _getNamespace(spaceId) { - if (spaceId === DEFAULT_SPACE_ID) { - return undefined; - } - - return spaceId; - } } diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js index a25ccc2136de9..fe9e363b01ce3 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js @@ -57,7 +57,21 @@ const createMockClient = () => { spacesService, }); - await expect(client.get(null, null, { namespace: 'bar' })).rejects.toThrowErrorMatchingSnapshot(); + await expect(client.get('foo', null, { namespace: 'bar' })).rejects.toThrowErrorMatchingSnapshot(); + }); + + test(`throws error if type is space`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); + + await expect(client.get('space', null)).rejects.toThrowErrorMatchingSnapshot(); }); test(`supplements options with undefined namespace`, async () => { @@ -94,7 +108,21 @@ const createMockClient = () => { spacesService, }); - await expect(client.bulkGet(null, { namespace: 'bar' })).rejects.toThrowErrorMatchingSnapshot(); + await expect(client.bulkGet([{ type: 'foo' }], { namespace: 'bar' })).rejects.toThrowErrorMatchingSnapshot(); + }); + + test(`throws error if objects type is space`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); + + await expect(client.bulkGet([{ type: 'foo' }, { type: 'space' }], { namespace: 'bar' })).rejects.toThrowErrorMatchingSnapshot(); }); test(`supplements options with undefined namespace`, async () => { @@ -110,7 +138,7 @@ const createMockClient = () => { spacesService, }); - const objects = Symbol(); + const objects = [{ type: 'foo' }]; const options = Object.freeze({ foo: 'bar' }); const actualReturnValue = await client.bulkGet(objects, options); @@ -134,6 +162,76 @@ const createMockClient = () => { await expect(client.find({ namespace: 'bar' })).rejects.toThrowErrorMatchingSnapshot(); }); + test(`throws error if options.type is space`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const expectedReturnValue = Symbol(); + baseClient.find.mockReturnValue(expectedReturnValue); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); + + await expect(client.find({ type: 'space' })).rejects.toThrowErrorMatchingSnapshot(); + }); + + test(`passes options.type to baseClient if valid singular type specified`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const expectedReturnValue = Symbol(); + baseClient.find.mockReturnValue(expectedReturnValue); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); + const options = Object.freeze({ type: 'foo' }); + + const actualReturnValue = await client.find(options); + + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.find).toHaveBeenCalledWith({ type: ['foo'], namespace: currentSpace.expectedNamespace }); + }); + + test(`throws error if options.type is array containing space`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const expectedReturnValue = Symbol(); + baseClient.find.mockReturnValue(expectedReturnValue); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); + + await expect(client.find({ type: ['space', 'foo'] })).rejects.toThrowErrorMatchingSnapshot(); + }); + + test(`if options.type isn't provided specifies options.type based on the types excluding the space`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const expectedReturnValue = Symbol(); + baseClient.find.mockReturnValue(expectedReturnValue); + const spacesService = createSpacesService(server); + const types = ['foo', 'bar', 'space']; + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types, + }); + + await expect(client.find({ type: ['space', 'foo'] })).rejects.toThrowErrorMatchingSnapshot(); + }); + test(`supplements options with undefined namespace`, async () => { const request = createMockRequest({ id: currentSpace.id }); const baseClient = createMockClient(); @@ -147,11 +245,11 @@ const createMockClient = () => { spacesService, }); - const options = Object.freeze({ foo: 'bar' }); + const options = Object.freeze({ type: ['foo', 'bar'] }); const actualReturnValue = await client.find(options); expect(actualReturnValue).toBe(expectedReturnValue); - expect(baseClient.find).toHaveBeenCalledWith({ foo: 'bar', namespace: currentSpace.expectedNamespace }); + expect(baseClient.find).toHaveBeenCalledWith({ type: ['foo', 'bar'], namespace: currentSpace.expectedNamespace }); }); }); @@ -171,6 +269,20 @@ const createMockClient = () => { await expect(client.create('foo', {}, { namespace: 'bar' })).rejects.toThrowErrorMatchingSnapshot(); }); + test(`throws error if type is space`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); + + await expect(client.create('space', {})).rejects.toThrowErrorMatchingSnapshot(); + }); + test(`supplements options with undefined namespace`, async () => { const request = createMockRequest({ id: currentSpace.id }); const baseClient = createMockClient(); @@ -206,7 +318,21 @@ const createMockClient = () => { spacesService, }); - await expect(client.bulkCreate(null, { namespace: 'bar' })).rejects.toThrowErrorMatchingSnapshot(); + await expect(client.bulkCreate([{ type: 'foo' }], { namespace: 'bar' })).rejects.toThrowErrorMatchingSnapshot(); + }); + + test(`throws error if objects type is space`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); + + await expect(client.bulkCreate([{ type: 'foo' }, { type: 'space' }])).rejects.toThrowErrorMatchingSnapshot(); }); test(`supplements options with undefined namespace`, async () => { @@ -222,7 +348,7 @@ const createMockClient = () => { spacesService, }); - const objects = Symbol(); + const objects = [{ type: 'foo' }]; const options = Object.freeze({ foo: 'bar' }); const actualReturnValue = await client.bulkCreate(objects, options); @@ -246,6 +372,20 @@ const createMockClient = () => { await expect(client.update(null, null, null, { namespace: 'bar' })).rejects.toThrowErrorMatchingSnapshot(); }); + test(`throws error if type is space`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); + + await expect(client.update('space', null)).rejects.toThrowErrorMatchingSnapshot(); + }); + test(`supplements options with undefined namespace`, async () => { const request = createMockRequest({ id: currentSpace.id }); const baseClient = createMockClient(); @@ -285,6 +425,20 @@ const createMockClient = () => { await expect(client.delete(null, null, { namespace: 'bar' })).rejects.toThrowErrorMatchingSnapshot(); }); + test(`throws error if type is space`, async () => { + const request = createMockRequest({ id: currentSpace.id }); + const baseClient = createMockClient(); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + }); + + await expect(client.delete('space', 'foo')).rejects.toThrowErrorMatchingSnapshot(); + }); + test(`supplements options with undefined namespace`, async () => { const request = createMockRequest({ id: currentSpace.id }); const baseClient = createMockClient(); diff --git a/x-pack/plugins/spaces/server/lib/space_request_interceptors.js b/x-pack/plugins/spaces/server/lib/space_request_interceptors.js index 5f0612f821b65..2f461d5fdf7b4 100644 --- a/x-pack/plugins/spaces/server/lib/space_request_interceptors.js +++ b/x-pack/plugins/spaces/server/lib/space_request_interceptors.js @@ -48,29 +48,27 @@ export function initSpacesRequestInterceptors(server) { // which is not available at the time of "onRequest". if (isRequestingKibanaRoot) { try { - const client = request.getSavedObjectsClient(); - const { total, saved_objects: spaceObjects } = await client.find({ - type: 'space' - }); + const spacesClient = server.plugins.spaces.spacesClient.getScopedClient(request); + const spaces = await spacesClient.getAll(); const config = server.config(); const basePath = config.get('server.basePath'); const defaultRoute = config.get('server.defaultRoute'); - if (total === 1) { + if (spaces.length === 1) { // If only one space is available, then send user there directly. // No need for an interstitial screen where there is only one possible outcome. - const space = spaceObjects[0]; + const space = spaces[0]; const destination = addSpaceIdToPath(basePath, space.id, defaultRoute); return reply.redirect(destination); } - if (total > 0) { + if (spaces.length > 0) { // render spaces selector instead of home page const app = server.getHiddenUiAppById('space_selector'); return reply.renderApp(app, { - spaces: spaceObjects.map(so => ({ ...so.attributes, id: so.id })) + spaces }); } diff --git a/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.js b/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.js index feb6bebeef5f1..0e7d47d2578cd 100644 --- a/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.js +++ b/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.js @@ -6,18 +6,21 @@ import sinon from 'sinon'; import { Server } from 'hapi'; import { initSpacesRequestInterceptors } from './space_request_interceptors'; -import { createSpacesService } from './create_spaces_service'; describe('interceptors', () => { const sandbox = sinon.sandbox.create(); const teardowns = []; + const headers = { + authorization: 'foo', + }; + let server; let request; beforeEach(() => { teardowns.push(() => sandbox.restore()); request = async (path, setupFn = () => { }, testConfig = {}) => { - const server = new Server(); + server = new Server(); server.connection({ port: 0 }); @@ -34,8 +37,13 @@ describe('interceptors', () => { }; })); - const spacesService = createSpacesService(server); - server.decorate('server', 'spaces', spacesService); + server.plugins = { + spaces: { + spacesClient: { + getScopedClient: jest.fn(), + } + } + }; initSpacesRequestInterceptors(server); @@ -57,6 +65,7 @@ describe('interceptors', () => { return await server.inject({ method: 'GET', url: path, + headers, }); }; }); @@ -132,16 +141,10 @@ describe('interceptors', () => { }; const setupTest = (server, spaces, testHandler) => { - // Mock server.getSavedObjectsClient() - server.decorate('request', 'getSavedObjectsClient', () => { - return { - find: jest.fn(() => { - return { - total: spaces.length, - saved_objects: spaces - }; - }) - }; + server.plugins.spaces.spacesClient.getScopedClient.mockReturnValue({ + getAll() { + return spaces; + } }); // Register test inspector @@ -175,6 +178,11 @@ describe('interceptors', () => { }, config); expect(testHandler).toHaveBeenCalledTimes(1); + expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith(expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization + }) + })); }); test('it redirects to the defaultRoute within the context of the Default Space when navigating to Kibana root', async () => { @@ -207,6 +215,11 @@ describe('interceptors', () => { }, config); expect(testHandler).toHaveBeenCalledTimes(1); + expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith(expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization + }) + })); }); }); @@ -252,6 +265,11 @@ describe('interceptors', () => { expect(getHiddenUiAppHandler).toHaveBeenCalledTimes(1); expect(getHiddenUiAppHandler).toHaveBeenCalledWith('space_selector'); expect(testHandler).toHaveBeenCalledTimes(1); + expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith(expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization + }) + })); }); }); }); diff --git a/x-pack/plugins/spaces/server/lib/spaces_client.test.ts b/x-pack/plugins/spaces/server/lib/spaces_client.test.ts new file mode 100644 index 0000000000000..6aaed0bb9506c --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/spaces_client.test.ts @@ -0,0 +1,925 @@ +/* + * 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 { SpacesClient } from './spaces_client'; + +const createMockAuditLogger = () => { + return { + spacesAuthorizationFailure: jest.fn(), + spacesAuthorizationSuccess: jest.fn(), + }; +}; + +const createMockAuthorization = () => { + const mockCheckPrivilegesAtSpace = jest.fn(); + const mockCheckPrivilegesAtSpaces = jest.fn(); + const mockCheckPrivilegesGlobally = jest.fn(); + + const mockAuthorization = { + actions: { + login: 'action:login', + manageSpaces: 'action:manageSpaces', + }, + checkPrivilegesWithRequest: jest.fn(() => ({ + atSpaces: mockCheckPrivilegesAtSpaces, + atSpace: mockCheckPrivilegesAtSpace, + globally: mockCheckPrivilegesGlobally, + })), + mode: { + useRbacForRequest: jest.fn(), + }, + }; + + return { + mockCheckPrivilegesAtSpaces, + mockCheckPrivilegesAtSpace, + mockCheckPrivilegesGlobally, + mockAuthorization, + }; +}; + +describe('#getAll', () => { + const savedObjects = [ + { + id: 'foo', + attributes: { + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + }, + }, + { + id: 'bar', + attributes: { + name: 'bar-name', + description: 'bar-description', + bar: 'bar-bar', + }, + }, + ]; + + const expectedSpaces = [ + { + id: 'foo', + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + }, + { + id: 'bar', + name: 'bar-name', + description: 'bar-description', + bar: 'bar-bar', + }, + ]; + + describe('authorization is null', () => { + test(`finds spaces using callWithRequestRepository`, async () => { + const mockAuditLogger = createMockAuditLogger(); + const authorization = null; + const mockCallWithRequestRepository = { + find: jest.fn(), + }; + mockCallWithRequestRepository.find.mockReturnValue({ + saved_objects: savedObjects, + }); + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + authorization, + mockCallWithRequestRepository, + null, + request + ); + const actualSpaces = await client.getAll(); + + expect(actualSpaces).toEqual(expectedSpaces); + expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({ + type: 'space', + page: 1, + perPage: 1000, + }); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); + }); + }); + + describe(`authorization.mode.useRbacForRequest returns false`, () => { + test(`finds spaces using callWithRequestRepository`, async () => { + const mockAuditLogger = createMockAuditLogger(); + const { mockAuthorization } = createMockAuthorization(); + mockAuthorization.mode.useRbacForRequest.mockReturnValue(false); + const mockCallWithRequestRepository = { + find: jest.fn().mockReturnValue({ + saved_objects: savedObjects, + }), + }; + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + mockAuthorization, + mockCallWithRequestRepository, + null, + request + ); + const actualSpaces = await client.getAll(); + + expect(actualSpaces).toEqual(expectedSpaces); + expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({ + type: 'space', + page: 1, + perPage: 1000, + }); + expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); + }); + }); + + describe('useRbacForRequest is true', () => { + test(`throws Boom.forbidden when user isn't authorized for any spaces`, async () => { + const username = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const { mockAuthorization, mockCheckPrivilegesAtSpaces } = createMockAuthorization(); + mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); + mockCheckPrivilegesAtSpaces.mockReturnValue({ + username, + spacePrivileges: { + [savedObjects[0].id]: { + [mockAuthorization.actions.login]: false, + }, + [savedObjects[1].id]: { + [mockAuthorization.actions.login]: false, + }, + }, + }); + const mockInternalRepository = { + find: jest.fn().mockReturnValue({ + saved_objects: savedObjects, + }), + }; + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + mockAuthorization, + null, + mockInternalRepository, + request + ); + await expect(client.getAll()).rejects.toThrowErrorMatchingSnapshot(); + + expect(mockInternalRepository.find).toHaveBeenCalledWith({ + type: 'space', + page: 1, + perPage: 1000, + }); + expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledWith( + savedObjects.map(savedObject => savedObject.id), + mockAuthorization.actions.login + ); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'getAll'); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); + }); + + test(`returns spaces that the user is authorized for`, async () => { + const username = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const { mockAuthorization, mockCheckPrivilegesAtSpaces } = createMockAuthorization(); + mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); + mockCheckPrivilegesAtSpaces.mockReturnValue({ + username, + spacePrivileges: { + [savedObjects[0].id]: { + [mockAuthorization.actions.login]: true, + }, + [savedObjects[1].id]: { + [mockAuthorization.actions.login]: false, + }, + }, + }); + const mockInternalRepository = { + find: jest.fn().mockReturnValue({ + saved_objects: savedObjects, + }), + }; + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + mockAuthorization, + null, + mockInternalRepository, + request + ); + const actualSpaces = await client.getAll(); + + expect(actualSpaces).toEqual([expectedSpaces[0]]); + expect(mockInternalRepository.find).toHaveBeenCalledWith({ + type: 'space', + page: 1, + perPage: 1000, + }); + expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledWith( + savedObjects.map(savedObject => savedObject.id), + mockAuthorization.actions.login + ); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'getAll', [ + savedObjects[0].id, + ]); + }); + }); +}); + +describe('#get', () => { + const savedObject = { + id: 'foo', + attributes: { + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + }, + }; + + const expectedSpace = { + id: 'foo', + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + }; + + describe(`authorization is null`, () => { + test(`gets space using callWithRequestRepository`, async () => { + const mockAuditLogger = createMockAuditLogger(); + const authorization = null; + const mockCallWithRequestRepository = { + get: jest.fn().mockReturnValue(savedObject), + }; + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + authorization, + mockCallWithRequestRepository, + null, + request + ); + const id = savedObject.id; + const actualSpace = await client.get(id); + + expect(actualSpace).toEqual(expectedSpace); + expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); + }); + }); + + describe(`authorization.mode.useRbacForRequest returns false`, () => { + test(`gets space using callWithRequestRepository`, async () => { + const mockAuditLogger = createMockAuditLogger(); + const { mockAuthorization } = createMockAuthorization(); + mockAuthorization.mode.useRbacForRequest.mockReturnValue(false); + const mockCallWithRequestRepository = { + get: jest.fn().mockReturnValue(savedObject), + }; + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + mockAuthorization, + mockCallWithRequestRepository, + null, + request + ); + const id = savedObject.id; + const actualSpace = await client.get(id); + + expect(actualSpace).toEqual(expectedSpace); + expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); + }); + }); + + describe('useRbacForRequest is true', () => { + test(`throws Boom.forbidden if the user isn't authorized at space`, async () => { + const username = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const { mockAuthorization, mockCheckPrivilegesAtSpace } = createMockAuthorization(); + mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); + mockCheckPrivilegesAtSpace.mockReturnValue({ + username, + hasAllRequested: false, + }); + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + mockAuthorization, + null, + null, + request + ); + const id = 'foo-space'; + + await expect(client.get(id)).rejects.toThrowErrorMatchingSnapshot(); + + expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivilegesAtSpace).toHaveBeenCalledWith(id, mockAuthorization.actions.login); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'get', [ + id, + ]); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); + }); + + test(`returns space using internalRepository if the user is authorized at space`, async () => { + const username = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const { mockAuthorization, mockCheckPrivilegesAtSpace } = createMockAuthorization(); + mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); + mockCheckPrivilegesAtSpace.mockReturnValue({ + username, + hasAllRequested: true, + }); + const request = Symbol(); + const mockInternalRepository = { + get: jest.fn().mockReturnValue(savedObject), + }; + + const client = new SpacesClient( + mockAuditLogger as any, + mockAuthorization, + null, + mockInternalRepository, + request + ); + const id = savedObject.id; + + const space = await client.get(id); + + expect(space).toEqual(expectedSpace); + expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivilegesAtSpace).toHaveBeenCalledWith(id, mockAuthorization.actions.login); + expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'get', [ + id, + ]); + }); + }); +}); + +describe('#create', () => { + const id = 'foo'; + + const spaceToCreate = { + id, + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + _reserved: true, + }; + + const attributes = { + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + }; + + const savedObject = { + id, + attributes: { + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + }, + }; + + const expectedReturnedSpace = { + id, + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + }; + + describe(`authorization is null`, () => { + test(`creates space using callWithRequestRepository`, async () => { + const mockAuditLogger = createMockAuditLogger(); + const authorization = null; + const mockCallWithRequestRepository = { + create: jest.fn().mockReturnValue(savedObject), + }; + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + authorization, + mockCallWithRequestRepository, + null, + request + ); + + const actualSpace = await client.create(spaceToCreate); + + expect(actualSpace).toEqual(expectedReturnedSpace); + expect(mockCallWithRequestRepository.create).toHaveBeenCalledWith('space', attributes, { + id, + }); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); + }); + }); + + describe(`authorization.mode.useRbacForRequest returns false`, () => { + test(`creates space using callWithRequestRepository`, async () => { + const mockAuditLogger = createMockAuditLogger(); + const { mockAuthorization } = createMockAuthorization(); + mockAuthorization.mode.useRbacForRequest.mockReturnValue(false); + const mockCallWithRequestRepository = { + create: jest.fn().mockReturnValue(savedObject), + }; + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + mockAuthorization, + mockCallWithRequestRepository, + null, + request + ); + + const actualSpace = await client.create(spaceToCreate); + + expect(actualSpace).toEqual(expectedReturnedSpace); + expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(mockCallWithRequestRepository.create).toHaveBeenCalledWith('space', attributes, { + id, + }); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); + }); + }); + + describe('useRbacForRequest is true', () => { + test(`throws Boom.forbidden if the user isn't authorized at space`, async () => { + const username = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const { mockAuthorization, mockCheckPrivilegesGlobally } = createMockAuthorization(); + mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); + mockCheckPrivilegesGlobally.mockReturnValue({ + username, + hasAllRequested: false, + }); + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + mockAuthorization, + null, + null, + request + ); + + await expect(client.create(spaceToCreate)).rejects.toThrowErrorMatchingSnapshot(); + + expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( + mockAuthorization.actions.manageSpaces + ); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'create'); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); + }); + + test(`creates space using internalRepository if the user is authorized`, async () => { + const username = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const { mockAuthorization, mockCheckPrivilegesGlobally } = createMockAuthorization(); + mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); + mockCheckPrivilegesGlobally.mockReturnValue({ + username, + hasAllRequested: true, + }); + const mockInternalRepository = { + create: jest.fn().mockReturnValue(savedObject), + }; + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + mockAuthorization, + null, + mockInternalRepository, + request + ); + + const actualSpace = await client.create(spaceToCreate); + + expect(actualSpace).toEqual(expectedReturnedSpace); + expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( + mockAuthorization.actions.manageSpaces + ); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'create'); + }); + }); +}); + +describe('#update', () => { + const spaceToUpdate = { + id: 'foo', + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + _reserved: false, + }; + + const attributes = { + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + }; + + const savedObject = { + id: 'foo', + attributes: { + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + _reserved: true, + }, + }; + + const expectedReturnedSpace = { + id: 'foo', + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + _reserved: true, + }; + + describe(`authorization is null`, () => { + test(`updates space using callWithRequestRepository`, async () => { + const mockAuditLogger = createMockAuditLogger(); + const authorization = null; + const mockCallWithRequestRepository = { + update: jest.fn(), + get: jest.fn().mockReturnValue(savedObject), + }; + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + authorization, + mockCallWithRequestRepository, + null, + request + ); + const id = savedObject.id; + const actualSpace = await client.update(id, spaceToUpdate); + + expect(actualSpace).toEqual(expectedReturnedSpace); + expect(mockCallWithRequestRepository.update).toHaveBeenCalledWith('space', id, attributes); + expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); + }); + }); + describe(`authorization.mode.useRbacForRequest returns false`, () => { + test(`updates space using callWithRequestRepository`, async () => { + const mockAuditLogger = createMockAuditLogger(); + const { mockAuthorization } = createMockAuthorization(); + mockAuthorization.mode.useRbacForRequest.mockReturnValue(false); + const mockCallWithRequestRepository = { + update: jest.fn(), + get: jest.fn().mockReturnValue(savedObject), + }; + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + mockAuthorization, + mockCallWithRequestRepository, + null, + request + ); + const id = savedObject.id; + const actualSpace = await client.update(id, spaceToUpdate); + + expect(actualSpace).toEqual(expectedReturnedSpace); + expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(mockCallWithRequestRepository.update).toHaveBeenCalledWith('space', id, attributes); + expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); + }); + }); + + describe('useRbacForRequest is true', () => { + test(`throws Boom.forbidden when user isn't authorized at space`, async () => { + const username = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const { mockAuthorization, mockCheckPrivilegesGlobally } = createMockAuthorization(); + mockCheckPrivilegesGlobally.mockReturnValue({ + hasAllRequested: false, + username, + }); + mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + mockAuthorization, + null, + null, + request + ); + const id = savedObject.id; + await expect(client.update(id, spaceToUpdate)).rejects.toThrowErrorMatchingSnapshot(); + + expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( + mockAuthorization.actions.manageSpaces + ); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'update'); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); + }); + + test(`updates space using internalRepository if user is authorized`, async () => { + const username = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const { mockAuthorization, mockCheckPrivilegesGlobally } = createMockAuthorization(); + mockCheckPrivilegesGlobally.mockReturnValue({ + hasAllRequested: true, + username, + }); + mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); + const mockInternalRepository = { + update: jest.fn(), + get: jest.fn().mockReturnValue(savedObject), + }; + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + mockAuthorization, + null, + mockInternalRepository, + request + ); + const id = savedObject.id; + const actualSpace = await client.update(id, spaceToUpdate); + + expect(actualSpace).toEqual(expectedReturnedSpace); + expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( + mockAuthorization.actions.manageSpaces + ); + expect(mockInternalRepository.update).toHaveBeenCalledWith('space', id, attributes); + expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'update'); + }); + }); +}); + +describe('#delete', () => { + const id = 'foo'; + + const reservedSavedObject = { + id, + attributes: { + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + _reserved: true, + }, + }; + + const notReservedSavedObject = { + id, + attributes: { + name: 'foo-name', + description: 'foo-description', + bar: 'foo-bar', + }, + }; + + describe(`authorization is null`, () => { + test(`throws Boom.badRequest when the space is reserved`, async () => { + const mockAuditLogger = createMockAuditLogger(); + const authorization = null; + const mockCallWithRequestRepository = { + get: jest.fn().mockReturnValue(reservedSavedObject), + }; + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + authorization, + mockCallWithRequestRepository, + null, + request + ); + + await expect(client.delete(id)).rejects.toThrowErrorMatchingSnapshot(); + + expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); + }); + + test(`deletes space using callWithRequestRepository when space isn't reserved`, async () => { + const mockAuditLogger = createMockAuditLogger(); + const authorization = null; + const mockCallWithRequestRepository = { + get: jest.fn().mockReturnValue(notReservedSavedObject), + delete: jest.fn(), + }; + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + authorization, + mockCallWithRequestRepository, + null, + request + ); + + await client.delete(id); + + expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); + expect(mockCallWithRequestRepository.delete).toHaveBeenCalledWith('space', id); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); + }); + }); + + describe(`authorization.mode.useRbacForRequest returns false`, () => { + test(`throws Boom.badRequest when the space is reserved`, async () => { + const mockAuditLogger = createMockAuditLogger(); + const { mockAuthorization } = createMockAuthorization(); + mockAuthorization.mode.useRbacForRequest.mockReturnValue(false); + const mockCallWithRequestRepository = { + get: jest.fn().mockReturnValue(reservedSavedObject), + }; + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + mockAuthorization, + mockCallWithRequestRepository, + null, + request + ); + + await expect(client.delete(id)).rejects.toThrowErrorMatchingSnapshot(); + + expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); + }); + + test(`deletes space using callWithRequestRepository when space isn't reserved`, async () => { + const mockAuditLogger = createMockAuditLogger(); + const { mockAuthorization } = createMockAuthorization(); + mockAuthorization.mode.useRbacForRequest.mockReturnValue(false); + const mockCallWithRequestRepository = { + get: jest.fn().mockReturnValue(notReservedSavedObject), + delete: jest.fn(), + }; + const request = Symbol(); + + const client = new SpacesClient( + mockAuditLogger as any, + mockAuthorization, + mockCallWithRequestRepository, + null, + request + ); + + await client.delete(id); + + expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); + expect(mockCallWithRequestRepository.delete).toHaveBeenCalledWith('space', id); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); + }); + }); + + describe('authorization.mode.useRbacForRequest returns true', () => { + test(`throws Boom.forbidden if the user isn't authorized`, async () => { + const username = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const { mockAuthorization, mockCheckPrivilegesGlobally } = createMockAuthorization(); + mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); + mockCheckPrivilegesGlobally.mockReturnValue({ + username, + hasAllRequested: false, + }); + const request = Symbol(); + const client = new SpacesClient( + mockAuditLogger as any, + mockAuthorization, + null, + null, + request + ); + + await expect(client.delete(id)).rejects.toThrowErrorMatchingSnapshot(); + + expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( + mockAuthorization.actions.manageSpaces + ); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'delete'); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); + }); + + test(`throws Boom.badRequest if the user is authorized but the space is reserved`, async () => { + const username = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const { mockAuthorization, mockCheckPrivilegesGlobally } = createMockAuthorization(); + mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); + mockCheckPrivilegesGlobally.mockReturnValue({ + username, + hasAllRequested: true, + }); + const mockInternalRepository = { + get: jest.fn().mockReturnValue(reservedSavedObject), + }; + const request = Symbol(); + const client = new SpacesClient( + mockAuditLogger as any, + mockAuthorization, + null, + mockInternalRepository, + request + ); + + await expect(client.delete(id)).rejects.toThrowErrorMatchingSnapshot(); + + expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( + mockAuthorization.actions.manageSpaces + ); + expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'delete'); + }); + + test(`deletes space using internalRepository if the user is authorized and the space isn't reserved`, async () => { + const username = Symbol(); + const mockAuditLogger = createMockAuditLogger(); + const { mockAuthorization, mockCheckPrivilegesGlobally } = createMockAuthorization(); + mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); + mockCheckPrivilegesGlobally.mockReturnValue({ + username, + hasAllRequested: true, + }); + const mockInternalRepository = { + get: jest.fn().mockReturnValue(notReservedSavedObject), + delete: jest.fn(), + }; + const request = Symbol(); + const client = new SpacesClient( + mockAuditLogger as any, + mockAuthorization, + null, + mockInternalRepository, + request + ); + + await client.delete(id); + + expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( + mockAuthorization.actions.manageSpaces + ); + expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); + expect(mockInternalRepository.delete).toHaveBeenCalledWith('space', id); + expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); + expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'delete'); + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/lib/spaces_client.ts b/x-pack/plugins/spaces/server/lib/spaces_client.ts new file mode 100644 index 0000000000000..2942b5a019fcb --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/spaces_client.ts @@ -0,0 +1,187 @@ +/* + * 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 { omit } from 'lodash'; +import { isReservedSpace } from '../../common/is_reserved_space'; +import { Space } from '../../common/model/space'; +import { SpacesAuditLogger } from './audit_logger'; + +export class SpacesClient { + private readonly auditLogger: SpacesAuditLogger; + private readonly authorization: any; + private readonly callWithRequestSavedObjectRepository: any; + private readonly internalSavedObjectRepository: any; + private readonly request: any; + + constructor( + auditLogger: SpacesAuditLogger, + authorization: any, + callWithRequestSavedObjectRepository: any, + internalSavedObjectRepository: any, + request: any + ) { + this.auditLogger = auditLogger; + this.authorization = authorization; + this.callWithRequestSavedObjectRepository = callWithRequestSavedObjectRepository; + this.internalSavedObjectRepository = internalSavedObjectRepository; + this.request = request; + } + + public async getAll(): Promise<[Space]> { + if (this.useRbac()) { + const { saved_objects } = await this.internalSavedObjectRepository.find({ + type: 'space', + page: 1, + perPage: 1000, + }); + + const spaces = saved_objects.map(this.transformSavedObjectToSpace); + + const spaceIds = spaces.map((space: Space) => space.id); + const checkPrivileges = this.authorization.checkPrivilegesWithRequest(this.request); + const { username, spacePrivileges } = await checkPrivileges.atSpaces( + spaceIds, + this.authorization.actions.login + ); + + const authorized = Object.keys(spacePrivileges).filter(spaceId => { + return spacePrivileges[spaceId][this.authorization.actions.login]; + }); + + if (authorized.length === 0) { + this.auditLogger.spacesAuthorizationFailure(username, 'getAll'); + throw Boom.forbidden(); + } + + this.auditLogger.spacesAuthorizationSuccess(username, 'getAll', authorized); + return spaces.filter((space: any) => authorized.includes(space.id)); + } else { + const { saved_objects } = await this.callWithRequestSavedObjectRepository.find({ + type: 'space', + page: 1, + perPage: 1000, + }); + + return saved_objects.map(this.transformSavedObjectToSpace); + } + } + + public async get(id: string): Promise { + if (this.useRbac()) { + await this.ensureAuthorizedAtSpace( + id, + this.authorization.actions.login, + 'get', + `Unauthorized to get ${id} space` + ); + } + const repository = this.useRbac() + ? this.internalSavedObjectRepository + : this.callWithRequestSavedObjectRepository; + + const savedObject = await repository.get('space', id); + return this.transformSavedObjectToSpace(savedObject); + } + + public async create(space: Space) { + if (this.useRbac()) { + await this.ensureAuthorizedGlobally( + this.authorization.actions.manageSpaces, + 'create', + 'Unauthorized to create spaces' + ); + } + const repository = this.useRbac() + ? this.internalSavedObjectRepository + : this.callWithRequestSavedObjectRepository; + + const attributes = omit(space, ['id', '_reserved']); + const id = space.id; + const createdSavedObject = await repository.create('space', attributes, { id }); + return this.transformSavedObjectToSpace(createdSavedObject); + } + + public async update(id: string, space: Space) { + if (this.useRbac()) { + await this.ensureAuthorizedGlobally( + this.authorization.actions.manageSpaces, + 'update', + 'Unauthorized to update spaces' + ); + } + const repository = this.useRbac() + ? this.internalSavedObjectRepository + : this.callWithRequestSavedObjectRepository; + + const attributes = omit(space, 'id', '_reserved'); + await repository.update('space', id, attributes); + const updatedSavedObject = await repository.get('space', id); + return this.transformSavedObjectToSpace(updatedSavedObject); + } + + public async delete(id: string) { + if (this.useRbac()) { + await this.ensureAuthorizedGlobally( + this.authorization.actions.manageSpaces, + 'delete', + 'Unauthorized to delete spaces' + ); + } + + const repository = this.useRbac() + ? this.internalSavedObjectRepository + : this.callWithRequestSavedObjectRepository; + + const existingSavedObject = await repository.get('space', id); + if (isReservedSpace(this.transformSavedObjectToSpace(existingSavedObject))) { + throw Boom.badRequest('This Space cannot be deleted because it is reserved.'); + } + + await repository.delete('space', id); + } + + private useRbac(): boolean { + return this.authorization && this.authorization.mode.useRbacForRequest(this.request); + } + + private async ensureAuthorizedGlobally(action: string, method: string, forbiddenMessage: string) { + const checkPrivileges = this.authorization.checkPrivilegesWithRequest(this.request); + const { username, hasAllRequested } = await checkPrivileges.globally(action); + + if (hasAllRequested) { + this.auditLogger.spacesAuthorizationSuccess(username, method); + return; + } else { + this.auditLogger.spacesAuthorizationFailure(username, method); + throw Boom.forbidden(forbiddenMessage); + } + } + + private async ensureAuthorizedAtSpace( + spaceId: string, + action: string, + method: string, + forbiddenMessage: string + ) { + const checkPrivileges = this.authorization.checkPrivilegesWithRequest(this.request); + const { username, hasAllRequested } = await checkPrivileges.atSpace(spaceId, action); + + if (hasAllRequested) { + this.auditLogger.spacesAuthorizationSuccess(username, method, [spaceId]); + return; + } else { + this.auditLogger.spacesAuthorizationFailure(username, method, [spaceId]); + throw Boom.forbidden(forbiddenMessage); + } + } + + private transformSavedObjectToSpace(savedObject: any): Space { + return { + id: savedObject.id, + ...savedObject.attributes, + } as Space; + } +} diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts index a184f21076d4f..44c7f9a2e6500 100644 --- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts @@ -6,6 +6,7 @@ // @ts-ignore import { Server } from 'hapi'; +import { SpacesClient } from '../../../lib/spaces_client'; import { createSpaces } from './create_spaces'; export interface TestConfig { @@ -17,13 +18,14 @@ export interface TestOptions { testConfig?: TestConfig; payload?: any; preCheckLicenseImpl?: (req: any, reply: any) => any; + expectSpacesClientCall?: boolean; } export type TeardownFn = () => void; export interface RequestRunnerResult { server: any; - mockSavedObjectsClient: any; + mockSavedObjectsRepository: any; response: any; } @@ -56,6 +58,7 @@ export function createTestHandler(initApiFn: (server: any, preCheckLicenseImpl: testConfig = {}, payload, preCheckLicenseImpl = defaultPreCheckLicenseImpl, + expectSpacesClientCall = true, } = options; let pre = jest.fn(); @@ -89,10 +92,13 @@ export function createTestHandler(initApiFn: (server: any, preCheckLicenseImpl: server.decorate('request', 'getBasePath', jest.fn()); server.decorate('request', 'setBasePath', jest.fn()); - // Mock server.getSavedObjectsClient() - const mockSavedObjectsClient = { + const mockSavedObjectsRepository = { get: jest.fn((type, id) => { - return spaces.filter(s => s.id === id)[0]; + const result = spaces.filter(s => s.id === id); + if (!result.length) { + throw new Error(`not found: [${type}:${id}]`); + } + return result[0]; }), find: jest.fn(() => { return { @@ -100,22 +106,57 @@ export function createTestHandler(initApiFn: (server: any, preCheckLicenseImpl: saved_objects: spaces, }; }), - create: jest.fn(() => ({})), - update: jest.fn(() => ({})), - delete: jest.fn(), - errors: { - isNotFoundError: jest.fn(() => true), + create: jest.fn((type, attributes, { id }) => { + if (spaces.find(s => s.id === id)) { + throw new Error('conflict'); + } + return {}; + }), + update: jest.fn((type, id) => { + if (!spaces.find(s => s.id === id)) { + throw new Error('not found: during update'); + } + return {}; + }), + delete: jest.fn((type: string, id: string) => { + return {}; + }), + }; + + server.savedObjects = { + SavedObjectsClient: { + errors: { + isNotFoundError: jest.fn((e: any) => e.message.startsWith('not found:')), + isConflictError: jest.fn((e: any) => e.message.startsWith('conflict')), + }, }, }; - server.decorate('request', 'getSavedObjectsClient', () => mockSavedObjectsClient); + server.plugins.spaces = { + spacesClient: { + getScopedClient: jest.fn((req: any) => { + return new SpacesClient( + null as any, + null, + mockSavedObjectsRepository, + mockSavedObjectsRepository, + req + ); + }), + }, + }; teardowns.push(() => server.stop()); + const headers = { + authorization: 'foo', + }; + const testRun = async () => { const response = await server.inject({ method, url: path, + headers, payload, }); @@ -125,12 +166,25 @@ export function createTestHandler(initApiFn: (server: any, preCheckLicenseImpl: expect(pre).not.toHaveBeenCalled(); } + if (expectSpacesClientCall) { + expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization, + }), + }) + ); + } else { + expect(server.plugins.spaces.spacesClient.getScopedClient).not.toHaveBeenCalled(); + } + return response; }; return { server, - mockSavedObjectsClient, + headers, + mockSavedObjectsRepository, response: await testRun(), }; }; diff --git a/x-pack/plugins/spaces/server/routes/api/public/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/public/delete.test.ts index 523b5a2cb2e7b..21948e28c56d6 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/delete.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/delete.test.ts @@ -52,6 +52,7 @@ describe('Spaces Public API', () => { const { response } = await request('DELETE', '/api/spaces/space/a-space', { preCheckLicenseImpl: (req: any, reply: any) => reply(Boom.forbidden('test forbidden message')), + expectSpacesClientCall: false, }); const { statusCode, payload } = response; @@ -62,12 +63,12 @@ describe('Spaces Public API', () => { }); }); - test('DELETE spaces/{id} pretends to delete a non-existent space', async () => { + test('DELETE spaces/{id} throws when deleting a non-existent space', async () => { const { response } = await request('DELETE', '/api/spaces/space/not-a-space'); const { statusCode } = response; - expect(statusCode).toEqual(204); + expect(statusCode).toEqual(404); }); test(`'DELETE spaces/{id}' cannot delete reserved spaces`, async () => { diff --git a/x-pack/plugins/spaces/server/routes/api/public/delete.ts b/x-pack/plugins/spaces/server/routes/api/public/delete.ts index 9937b786ccfa7..080c765dd4a44 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/delete.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/delete.ts @@ -5,31 +5,29 @@ */ import Boom from 'boom'; -import { isReservedSpace } from '../../../../common/is_reserved_space'; import { wrapError } from '../../../lib/errors'; -import { getSpaceById } from '../../lib'; +import { SpacesClient } from '../../../lib/spaces_client'; export function initDeleteSpacesApi(server: any, routePreCheckLicenseFn: any) { server.route({ method: 'DELETE', path: '/api/spaces/space/{id}', async handler(request: any, reply: any) { - const client = request.getSavedObjectsClient(); + const { SavedObjectsClient } = server.savedObjects; + const spacesClient: SpacesClient = server.plugins.spaces.spacesClient.getScopedClient( + request + ); const id = request.params.id; let result; try { - const existingSpace = await getSpaceById(client, id); - if (isReservedSpace(existingSpace)) { - return reply( - wrapError(Boom.badRequest('This Space cannot be deleted because it is reserved.')) - ); - } - - result = await client.delete('space', id); + result = await spacesClient.delete(id); } catch (error) { + if (SavedObjectsClient.errors.isNotFoundError(error)) { + return reply(Boom.notFound()); + } return reply(wrapError(error)); } diff --git a/x-pack/plugins/spaces/server/routes/api/public/get.test.ts b/x-pack/plugins/spaces/server/routes/api/public/get.test.ts index 4d04759b283a8..ad3e758853e01 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/get.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/get.test.ts @@ -56,6 +56,7 @@ describe('GET spaces', () => { const { response } = await request('GET', '/api/spaces/space', { preCheckLicenseImpl: (req: any, reply: any) => reply(Boom.forbidden('test forbidden message')), + expectSpacesClientCall: false, }); const { statusCode, payload } = response; diff --git a/x-pack/plugins/spaces/server/routes/api/public/get.ts b/x-pack/plugins/spaces/server/routes/api/public/get.ts index 95f1d273a8f6b..ae3a083c50123 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/get.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/get.ts @@ -5,25 +5,23 @@ */ import Boom from 'boom'; +import { Space } from '../../../../common/model/space'; import { wrapError } from '../../../lib/errors'; -import { convertSavedObjectToSpace } from '../../lib'; +import { SpacesClient } from '../../../lib/spaces_client'; export function initGetSpacesApi(server: any, routePreCheckLicenseFn: any) { server.route({ method: 'GET', path: '/api/spaces/space', async handler(request: any, reply: any) { - const client = request.getSavedObjectsClient(); + const spacesClient: SpacesClient = server.plugins.spaces.spacesClient.getScopedClient( + request + ); - let spaces; + let spaces: Space[]; try { - const result = await client.find({ - type: 'space', - sortField: 'name.keyword', - }); - - spaces = result.saved_objects.map(convertSavedObjectToSpace); + spaces = await spacesClient.getAll(); } catch (error) { return reply(wrapError(error)); } @@ -41,14 +39,15 @@ export function initGetSpacesApi(server: any, routePreCheckLicenseFn: any) { async handler(request: any, reply: any) { const spaceId = request.params.id; - const client = request.getSavedObjectsClient(); + const { SavedObjectsClient } = server.savedObjects; + const spacesClient: SpacesClient = server.plugins.spaces.spacesClient.getScopedClient( + request + ); try { - const response = await client.get('space', spaceId); - - return reply(convertSavedObjectToSpace(response)); + return reply(await spacesClient.get(spaceId)); } catch (error) { - if (client.errors.isNotFoundError(error)) { + if (SavedObjectsClient.errors.isNotFoundError(error)) { return reply(Boom.notFound()); } return reply(wrapError(error)); diff --git a/x-pack/plugins/spaces/server/routes/api/public/post.test.ts b/x-pack/plugins/spaces/server/routes/api/public/post.test.ts index f97931d36ed66..b554d5fc67354 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/post.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/post.test.ts @@ -48,18 +48,18 @@ describe('Spaces Public API', () => { description: 'with a description', }; - const { mockSavedObjectsClient, response } = await request('POST', '/api/spaces/space', { + const { mockSavedObjectsRepository, response } = await request('POST', '/api/spaces/space', { payload, }); const { statusCode } = response; expect(statusCode).toEqual(200); - expect(mockSavedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(mockSavedObjectsClient.create).toHaveBeenCalledWith( + expect(mockSavedObjectsRepository.create).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsRepository.create).toHaveBeenCalledWith( 'space', { name: 'my new space', description: 'with a description' }, - { id: 'my-space-id', overwrite: false } + { id: 'my-space-id' } ); }); @@ -73,6 +73,7 @@ describe('Spaces Public API', () => { const { response } = await request('POST', '/api/spaces/space', { preCheckLicenseImpl: (req: any, reply: any) => reply(Boom.forbidden('test forbidden message')), + expectSpacesClientCall: false, payload, }); @@ -98,8 +99,7 @@ describe('Spaces Public API', () => { expect(statusCode).toEqual(409); expect(JSON.parse(responsePayload)).toEqual({ error: 'Conflict', - message: - 'A space with the identifier a-space already exists. Please choose a different identifier', + message: 'A space with the identifier a-space already exists.', statusCode: 409, }); }); diff --git a/x-pack/plugins/spaces/server/routes/api/public/post.ts b/x-pack/plugins/spaces/server/routes/api/public/post.ts index fd51390af5023..a4c1e04a73831 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/post.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/post.ts @@ -5,34 +5,28 @@ */ import Boom from 'boom'; -import { omit } from 'lodash'; import { wrapError } from '../../../lib/errors'; import { spaceSchema } from '../../../lib/space_schema'; -import { getSpaceById } from '../../lib'; +import { SpacesClient } from '../../../lib/spaces_client'; export function initPostSpacesApi(server: any, routePreCheckLicenseFn: any) { server.route({ method: 'POST', path: '/api/spaces/space', async handler(request: any, reply: any) { - const client = request.getSavedObjectsClient(); + const { SavedObjectsClient } = server.savedObjects; + const spacesClient: SpacesClient = server.plugins.spaces.spacesClient.getScopedClient( + request + ); - const space = omit(request.payload, ['id', '_reserved']); - - const id = request.payload.id; - - const existingSpace = await getSpaceById(client, id); - if (existingSpace) { - return reply( - Boom.conflict( - `A space with the identifier ${id} already exists. Please choose a different identifier` - ) - ); - } + const space = request.payload; try { - return reply(await client.create('space', { ...space }, { id, overwrite: false })); + return reply(await spacesClient.create(space)); } catch (error) { + if (SavedObjectsClient.errors.isConflictError(error)) { + return reply(Boom.conflict(`A space with the identifier ${space.id} already exists.`)); + } return reply(wrapError(error)); } }, diff --git a/x-pack/plugins/spaces/server/routes/api/public/put.test.ts b/x-pack/plugins/spaces/server/routes/api/public/put.test.ts index 2af4fc9bbeaf3..e02fb58da1d61 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/put.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/put.test.ts @@ -46,15 +46,19 @@ describe('Spaces Public API', () => { description: 'with a description', }; - const { mockSavedObjectsClient, response } = await request('PUT', '/api/spaces/space/a-space', { - payload, - }); + const { mockSavedObjectsRepository, response } = await request( + 'PUT', + '/api/spaces/space/a-space', + { + payload, + } + ); const { statusCode } = response; expect(statusCode).toEqual(200); - expect(mockSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(mockSavedObjectsClient.update).toHaveBeenCalledWith('space', 'a-space', { + expect(mockSavedObjectsRepository.update).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsRepository.update).toHaveBeenCalledWith('space', 'a-space', { name: 'my updated space', description: 'with a description', }); @@ -70,6 +74,7 @@ describe('Spaces Public API', () => { const { response } = await request('PUT', '/api/spaces/space/a-space', { preCheckLicenseImpl: (req: any, reply: any) => reply(Boom.forbidden('test forbidden message')), + expectSpacesClientCall: false, payload, }); diff --git a/x-pack/plugins/spaces/server/routes/api/public/put.ts b/x-pack/plugins/spaces/server/routes/api/public/put.ts index 093d7c777e786..dea7e3a79d5c0 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/put.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/put.ts @@ -5,39 +5,35 @@ */ import Boom from 'boom'; -import { omit } from 'lodash'; import { Space } from '../../../../common/model/space'; import { wrapError } from '../../../lib/errors'; import { spaceSchema } from '../../../lib/space_schema'; -import { convertSavedObjectToSpace, getSpaceById } from '../../lib'; +import { SpacesClient } from '../../../lib/spaces_client'; export function initPutSpacesApi(server: any, routePreCheckLicenseFn: any) { server.route({ method: 'PUT', path: '/api/spaces/space/{id}', async handler(request: any, reply: any) { - const client = request.getSavedObjectsClient(); + const { SavedObjectsClient } = server.savedObjects; + const spacesClient: SpacesClient = server.plugins.spaces.spacesClient.getScopedClient( + request + ); - const space: Space = omit(request.payload, ['id']); + const space: Space = request.payload; const id = request.params.id; - const existingSpace = await getSpaceById(client, id); - - if (existingSpace) { - space._reserved = existingSpace._reserved; - } else { - return reply(Boom.notFound(`Unable to find space with ID ${id}`)); - } - - let result; + let result: Space; try { - result = await client.update('space', id, { ...space }); + result = await spacesClient.update(id, { ...space }); } catch (error) { + if (SavedObjectsClient.errors.isNotFoundError(error)) { + return reply(Boom.notFound()); + } return reply(wrapError(error)); } - const updatedSpace = convertSavedObjectToSpace(result); - return reply(updatedSpace); + return reply(result); }, config: { validate: { diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.ts b/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.ts index bbdab1be34910..0758ceb32746c 100644 --- a/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.ts @@ -56,6 +56,7 @@ describe('Spaces API', () => { const { response } = await request('POST', '/api/spaces/v1/space/a-space/select', { preCheckLicenseImpl: (req: any, reply: any) => reply(Boom.forbidden('test forbidden message')), + expectSpacesClientCall: false, }); const { statusCode, payload } = response; diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.ts b/x-pack/plugins/spaces/server/routes/api/v1/spaces.ts index 0233cb76b96d8..6f09d1831bff9 100644 --- a/x-pack/plugins/spaces/server/routes/api/v1/spaces.ts +++ b/x-pack/plugins/spaces/server/routes/api/v1/spaces.ts @@ -7,6 +7,7 @@ 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'; @@ -15,12 +16,19 @@ export function initPrivateSpacesApi(server: any, routePreCheckLicenseFn: any) { method: 'POST', path: '/api/spaces/v1/space/{id}/select', async handler(request: any, reply: any) { - const client = request.getSavedObjectsClient(); + const { SavedObjectsClient } = server.savedObjects; + const spacesClient: SpacesClient = server.plugins.spaces.spacesClient.getScopedClient( + request + ); const id = request.params.id; try { - const existingSpace: Space | null = await getSpaceById(client, id); + const existingSpace: Space | null = await getSpaceById( + spacesClient, + id, + SavedObjectsClient.errors + ); if (!existingSpace) { return reply(Boom.notFound()); } diff --git a/x-pack/plugins/spaces/server/routes/lib/get_space_by_id.ts b/x-pack/plugins/spaces/server/routes/lib/get_space_by_id.ts index 4143c09a79a93..eaa789b32c39b 100644 --- a/x-pack/plugins/spaces/server/routes/lib/get_space_by_id.ts +++ b/x-pack/plugins/spaces/server/routes/lib/get_space_by_id.ts @@ -4,14 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ import { Space } from '../../../common/model/space'; +import { SpacesClient } from '../../lib/spaces_client'; import { convertSavedObjectToSpace } from './convert_saved_object_to_space'; -export async function getSpaceById(client: any, spaceId: string): Promise { +export async function getSpaceById( + client: SpacesClient, + spaceId: string, + errors: any +): Promise { try { - const existingSpace = await client.get('space', spaceId); + const existingSpace = await client.get(spaceId); return convertSavedObjectToSpace(existingSpace); } catch (error) { - if (client.errors.isNotFoundError(error)) { + if (errors.isNotFoundError(error)) { return null; } throw error; diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 1b05203e4dd09..65fa2787c09af 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -13,6 +13,9 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/functional/config.js'), require.resolve('../test/api_integration/config.js'), require.resolve('../test/saml_api_integration/config.js'), - require.resolve('../test/rbac_api_integration/config.js'), - require.resolve('../test/spaces_api_integration/config.js'), + require.resolve('../test/spaces_api_integration/spaces_only/config'), + require.resolve('../test/spaces_api_integration/security_and_spaces/config'), + require.resolve('../test/saved_object_api_integration/security_and_spaces/config'), + require.resolve('../test/saved_object_api_integration/security_only/config'), + require.resolve('../test/saved_object_api_integration/spaces_only/config'), ]); diff --git a/x-pack/test/rbac_api_integration/apis/es/has_privileges.js b/x-pack/test/api_integration/apis/es/has_privileges.js similarity index 100% rename from x-pack/test/rbac_api_integration/apis/es/has_privileges.js rename to x-pack/test/api_integration/apis/es/has_privileges.js diff --git a/x-pack/test/rbac_api_integration/apis/es/index.js b/x-pack/test/api_integration/apis/es/index.js similarity index 100% rename from x-pack/test/rbac_api_integration/apis/es/index.js rename to x-pack/test/api_integration/apis/es/index.js diff --git a/x-pack/test/rbac_api_integration/apis/es/post_privileges.js b/x-pack/test/api_integration/apis/es/post_privileges.js similarity index 100% rename from x-pack/test/rbac_api_integration/apis/es/post_privileges.js rename to x-pack/test/api_integration/apis/es/post_privileges.js diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index 7f105650141d9..85b11bb9ef71e 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -6,6 +6,7 @@ export default function ({ loadTestFile }) { describe('apis', () => { + loadTestFile(require.resolve('./es')); loadTestFile(require.resolve('./security')); loadTestFile(require.resolve('./monitoring')); loadTestFile(require.resolve('./xpack_main')); diff --git a/x-pack/test/rbac_api_integration/apis/privileges/index.js b/x-pack/test/rbac_api_integration/apis/privileges/index.js deleted file mode 100644 index 6f62720a227cf..0000000000000 --- a/x-pack/test/rbac_api_integration/apis/privileges/index.js +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import expect from 'expect.js'; - -export default function ({ getService }) { - describe('privileges', () => { - it(`get should return privileges`, async () => { - const supertest = getService('supertest'); - const kibanaServer = getService('kibanaServer'); - const version = await kibanaServer.version.get(); - - await supertest - .get(`/api/security/v1/privileges`) - .expect(200) - .then(resp => { - expect(resp.body).to.eql([ - { - application: 'kibana-.kibana', - name: 'all', - actions: [`version:${version}`, 'action:*'], - metadata: {}, - }, - { - application: 'kibana-.kibana', - name: 'read', - actions: [ - `version:${version}`, - 'action:login', - 'action:saved_objects/config/get', - 'action:saved_objects/config/bulk_get', - 'action:saved_objects/config/find', - 'action:saved_objects/timelion-sheet/get', - 'action:saved_objects/timelion-sheet/bulk_get', - 'action:saved_objects/timelion-sheet/find', - 'action:saved_objects/telemetry/get', - 'action:saved_objects/telemetry/bulk_get', - 'action:saved_objects/telemetry/find', - 'action:saved_objects/graph-workspace/get', - 'action:saved_objects/graph-workspace/bulk_get', - 'action:saved_objects/graph-workspace/find', - 'action:saved_objects/space/get', - 'action:saved_objects/space/bulk_get', - 'action:saved_objects/space/find', - 'action:saved_objects/index-pattern/get', - 'action:saved_objects/index-pattern/bulk_get', - 'action:saved_objects/index-pattern/find', - 'action:saved_objects/visualization/get', - 'action:saved_objects/visualization/bulk_get', - 'action:saved_objects/visualization/find', - 'action:saved_objects/search/get', - 'action:saved_objects/search/bulk_get', - 'action:saved_objects/search/find', - 'action:saved_objects/dashboard/get', - 'action:saved_objects/dashboard/bulk_get', - 'action:saved_objects/dashboard/find', - 'action:saved_objects/url/get', - 'action:saved_objects/url/bulk_get', - 'action:saved_objects/url/find', - 'action:saved_objects/server/get', - 'action:saved_objects/server/bulk_get', - 'action:saved_objects/server/find', - ], - metadata: {}, - }, - ]); - }); - }); - }); -} diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/bulk_get.js b/x-pack/test/rbac_api_integration/apis/saved_objects/bulk_get.js deleted file mode 100644 index 6785859e42fbf..0000000000000 --- a/x-pack/test/rbac_api_integration/apis/saved_objects/bulk_get.js +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from 'expect.js'; -import { AUTHENTICATION } from './lib/authentication'; - -export default function ({ getService }) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const BULK_REQUESTS = [ - { - type: 'visualization', - id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - }, - { - type: 'dashboard', - id: 'does not exist', - }, - { - type: 'config', - id: '7.0.0-alpha1', - }, - ]; - - describe('_bulk_get', () => { - const expectResults = resp => { - expect(resp.body).to.eql({ - saved_objects: [ - { - id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - type: 'visualization', - updated_at: '2017-09-21T18:51:23.794Z', - version: resp.body.saved_objects[0].version, - attributes: { - title: 'Count of requests', - description: '', - version: 1, - // cheat for some of the more complex attributes - visState: resp.body.saved_objects[0].attributes.visState, - uiStateJSON: resp.body.saved_objects[0].attributes.uiStateJSON, - kibanaSavedObjectMeta: - resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta, - }, - }, - { - id: 'does not exist', - type: 'dashboard', - error: { - statusCode: 404, - message: 'Not found', - }, - }, - { - id: '7.0.0-alpha1', - type: 'config', - updated_at: '2017-09-21T18:49:16.302Z', - version: resp.body.saved_objects[2].version, - attributes: { - buildNum: 8467, - defaultIndex: '91200a00-9efd-11e7-acb3-3dab96693fab', - }, - }, - ], - }); - }; - - const expectRbacForbidden = resp => { - //eslint-disable-next-line max-len - const missingActions = `action:saved_objects/config/bulk_get,action:saved_objects/dashboard/bulk_get,action:saved_objects/visualization/bulk_get`; - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to bulk_get config,dashboard,visualization, missing ${missingActions}` - }); - }; - - const bulkGetTest = (description, { auth, tests }) => { - describe(description, () => { - before(() => esArchiver.load('saved_objects/basic')); - after(() => esArchiver.unload('saved_objects/basic')); - - it(`should return ${tests.default.statusCode}`, async () => { - await supertest - .post(`/api/saved_objects/_bulk_get`) - .auth(auth.username, auth.password) - .send(BULK_REQUESTS) - .expect(tests.default.statusCode) - .then(tests.default.response); - }); - }); - }; - - bulkGetTest(`not a kibana user`, { - auth: { - username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, - password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - } - } - }); - - bulkGetTest(`superuser`, { - auth: { - username: AUTHENTICATION.SUPERUSER.USERNAME, - password: AUTHENTICATION.SUPERUSER.PASSWORD, - }, - tests: { - default: { - statusCode: 200, - response: expectResults, - }, - } - }); - - bulkGetTest(`kibana legacy user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 200, - response: expectResults, - }, - } - }); - - bulkGetTest(`kibana legacy dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 200, - response: expectResults, - }, - } - }); - - bulkGetTest(`kibana dual-privileges user`, { - auth: { - username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME, - password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 200, - response: expectResults, - }, - } - }); - - bulkGetTest(`kibana dual-privileges dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 200, - response: expectResults, - }, - } - }); - - bulkGetTest(`kibana rbac user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 200, - response: expectResults, - }, - } - }); - - bulkGetTest(`kibana rbac dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 200, - response: expectResults, - }, - } - }); - }); -} diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/create.js b/x-pack/test/rbac_api_integration/apis/saved_objects/create.js deleted file mode 100644 index 6a949004371f8..0000000000000 --- a/x-pack/test/rbac_api_integration/apis/saved_objects/create.js +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from 'expect.js'; -import { AUTHENTICATION } from './lib/authentication'; - -export default function ({ getService }) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - describe('create', () => { - const expectResults = (resp) => { - expect(resp.body).to.have.property('id').match(/^[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/); - - expect(resp.body).to.eql({ - id: resp.body.id, - type: 'visualization', - updated_at: resp.body.updated_at, - version: 1, - attributes: { - title: 'My favorite vis' - } - }); - }; - - const expectRbacForbidden = resp => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to create visualization, missing action:saved_objects/visualization/create` - }); - }; - - const createExpectLegacyForbidden = username => resp => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - //eslint-disable-next-line max-len - message: `action [indices:data/write/index] is unauthorized for user [${username}]: [security_exception] action [indices:data/write/index] is unauthorized for user [${username}]` - }); - }; - - const createTest = (description, { auth, tests }) => { - describe(description, () => { - before(() => esArchiver.load('saved_objects/basic')); - after(() => esArchiver.unload('saved_objects/basic')); - it(`should return ${tests.default.statusCode}`, async () => { - await supertest - .post(`/api/saved_objects/visualization`) - .auth(auth.username, auth.password) - .send({ - attributes: { - title: 'My favorite vis' - } - }) - .expect(tests.default.statusCode) - .then(tests.default.response); - }); - }); - }; - - createTest(`not a kibana user`, { - auth: { - username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, - password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - } - }); - - createTest(`superuser`, { - auth: { - username: AUTHENTICATION.SUPERUSER.USERNAME, - password: AUTHENTICATION.SUPERUSER.PASSWORD, - }, - tests: { - default: { - statusCode: 200, - response: expectResults, - }, - } - }); - - createTest(`kibana legacy user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 200, - response: expectResults, - }, - } - }); - - createTest(`kibana legacy dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 403, - response: createExpectLegacyForbidden(AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME), - }, - } - }); - - createTest(`kibana dual-privileges user`, { - auth: { - username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME, - password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 200, - response: expectResults, - }, - } - }); - - createTest(`kibana dual-privileges dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - } - }); - - createTest(`kibana rbac user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 200, - response: expectResults, - }, - } - }); - - createTest(`kibana rbac dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - } - }); - }); -} diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/delete.js b/x-pack/test/rbac_api_integration/apis/saved_objects/delete.js deleted file mode 100644 index 5885eb7919c7b..0000000000000 --- a/x-pack/test/rbac_api_integration/apis/saved_objects/delete.js +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from 'expect.js'; -import { AUTHENTICATION } from './lib/authentication'; - -export default function ({ getService }) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - describe('delete', () => { - - const expectEmpty = (resp) => { - expect(resp.body).to.eql({}); - }; - - const expectNotFound = (resp) => { - expect(resp.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Saved object [dashboard/not-a-real-id] not found' - }); - }; - - const expectRbacForbidden = resp => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to delete dashboard, missing action:saved_objects/dashboard/delete` - }); - }; - - const createExpectLegacyForbidden = username => resp => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - //eslint-disable-next-line max-len - message: `action [indices:data/write/delete] is unauthorized for user [${username}]: [security_exception] action [indices:data/write/delete] is unauthorized for user [${username}]` - }); - }; - - const deleteTest = (description, { auth, tests }) => { - describe(description, () => { - before(() => esArchiver.load('saved_objects/basic')); - after(() => esArchiver.unload('saved_objects/basic')); - - it(`should return ${tests.actualId.statusCode} when deleting a doc`, async () => ( - await supertest - .delete(`/api/saved_objects/dashboard/be3733a0-9efe-11e7-acb3-3dab96693fab`) - .auth(auth.username, auth.password) - .expect(tests.actualId.statusCode) - .then(tests.actualId.response) - )); - - it(`should return ${tests.invalidId.statusCode} when deleting an unknown doc`, async () => ( - await supertest - .delete(`/api/saved_objects/dashboard/not-a-real-id`) - .auth(auth.username, auth.password) - .expect(tests.invalidId.statusCode) - .then(tests.invalidId.response) - )); - }); - }; - - deleteTest(`not a kibana user`, { - auth: { - username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, - password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, - }, - tests: { - actualId: { - statusCode: 403, - response: expectRbacForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacForbidden, - } - } - }); - - deleteTest(`superuser`, { - auth: { - username: AUTHENTICATION.SUPERUSER.USERNAME, - password: AUTHENTICATION.SUPERUSER.PASSWORD, - }, - tests: { - actualId: { - statusCode: 200, - response: expectEmpty, - }, - invalidId: { - statusCode: 404, - response: expectNotFound, - } - } - }); - - deleteTest(`kibana legacy user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, - }, - tests: { - actualId: { - statusCode: 200, - response: expectEmpty, - }, - invalidId: { - statusCode: 404, - response: expectNotFound, - } - } - }); - - deleteTest(`kibana legacy dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - actualId: { - statusCode: 403, - response: createExpectLegacyForbidden(AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME), - }, - invalidId: { - statusCode: 403, - response: createExpectLegacyForbidden(AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME), - } - } - }); - - deleteTest(`kibana dual-privileges user`, { - auth: { - username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME, - password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD, - }, - tests: { - actualId: { - statusCode: 200, - response: expectEmpty, - }, - invalidId: { - statusCode: 404, - response: expectNotFound, - } - } - }); - - deleteTest(`kibana dual-privileges dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - actualId: { - statusCode: 403, - response: expectRbacForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacForbidden, - } - } - }); - - deleteTest(`kibana rbac user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, - }, - tests: { - actualId: { - statusCode: 200, - response: expectEmpty, - }, - invalidId: { - statusCode: 404, - response: expectNotFound, - } - } - }); - - deleteTest(`kibana rbac dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - actualId: { - statusCode: 403, - response: expectRbacForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacForbidden, - } - } - }); - }); -} diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/find.js b/x-pack/test/rbac_api_integration/apis/saved_objects/find.js deleted file mode 100644 index ca9b9bb2ec1c2..0000000000000 --- a/x-pack/test/rbac_api_integration/apis/saved_objects/find.js +++ /dev/null @@ -1,424 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from 'expect.js'; -import { AUTHENTICATION } from './lib/authentication'; - -export default function ({ getService }) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - describe('find', () => { - - const expectVisualizationResults = (resp) => { - expect(resp.body).to.eql({ - page: 1, - per_page: 20, - total: 1, - saved_objects: [ - { - type: 'visualization', - id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - version: 1, - attributes: { - 'title': 'Count of requests' - } - } - ] - }); - }; - - const expectResultsWithValidTypes = (resp) => { - expect(resp.body).to.eql({ - page: 1, - per_page: 20, - total: 4, - saved_objects: [ - { - id: '91200a00-9efd-11e7-acb3-3dab96693fab', - type: 'index-pattern', - updated_at: '2017-09-21T18:49:16.270Z', - version: 1, - attributes: resp.body.saved_objects[0].attributes - }, - { - id: '7.0.0-alpha1', - type: 'config', - updated_at: '2017-09-21T18:49:16.302Z', - version: 1, - attributes: resp.body.saved_objects[1].attributes - }, - { - id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - type: 'visualization', - updated_at: '2017-09-21T18:51:23.794Z', - version: 1, - attributes: resp.body.saved_objects[2].attributes - }, - { - id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', - type: 'dashboard', - updated_at: '2017-09-21T18:57:40.826Z', - version: 1, - attributes: resp.body.saved_objects[3].attributes - }, - ] - }); - }; - - const createExpectEmpty = (page, perPage, total) => (resp) => { - expect(resp.body).to.eql({ - page: page, - per_page: perPage, - total: total, - saved_objects: [] - }); - }; - - const createExpectRbacForbidden = (type) => resp => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to find ${type}, missing action:saved_objects/${type}/find` - }); - }; - - const expectForbiddenCantFindAnyTypes = resp => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Not authorized to find saved_object` - }); - }; - - const findTest = (description, { auth, tests }) => { - describe(description, () => { - before(() => esArchiver.load('saved_objects/basic')); - after(() => esArchiver.unload('saved_objects/basic')); - - it(`should return ${tests.normal.statusCode} with ${tests.normal.description}`, async () => ( - await supertest - .get('/api/saved_objects/_find?type=visualization&fields=title') - .auth(auth.username, auth.password) - .expect(tests.normal.statusCode) - .then(tests.normal.response) - )); - - describe('unknown type', () => { - it(`should return ${tests.unknownType.statusCode} with ${tests.unknownType.description}`, async () => ( - await supertest - .get('/api/saved_objects/_find?type=wigwags') - .auth(auth.username, auth.password) - .expect(tests.unknownType.statusCode) - .then(tests.unknownType.response) - )); - }); - - describe('page beyond total', () => { - it(`should return ${tests.pageBeyondTotal.statusCode} with ${tests.pageBeyondTotal.description}`, async () => ( - await supertest - .get('/api/saved_objects/_find?type=visualization&page=100&per_page=100') - .auth(auth.username, auth.password) - .expect(tests.pageBeyondTotal.statusCode) - .then(tests.pageBeyondTotal.response) - )); - }); - - describe('unknown search field', () => { - it(`should return ${tests.unknownSearchField.statusCode} with ${tests.unknownSearchField.description}`, async () => ( - await supertest - .get('/api/saved_objects/_find?type=wigwags&search_fields=a') - .auth(auth.username, auth.password) - .expect(tests.unknownSearchField.statusCode) - .then(tests.unknownSearchField.response) - )); - }); - - describe('no type', () => { - it(`should return ${tests.noType.statusCode} with ${tests.noType.description}`, async () => ( - await supertest - .get('/api/saved_objects/_find') - .auth(auth.username, auth.password) - .expect(tests.noType.statusCode) - .then(tests.noType.response) - )); - }); - }); - }; - - findTest(`not a kibana user`, { - auth: { - username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, - password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, - }, - tests: { - normal: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownType: { - description: 'forbidden login and find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - noType: { - description: `forbidded can't find any types`, - statusCode: 403, - response: expectForbiddenCantFindAnyTypes, - } - } - }); - - findTest(`superuser`, { - auth: { - username: AUTHENTICATION.SUPERUSER.USERNAME, - password: AUTHENTICATION.SUPERUSER.PASSWORD, - }, - tests: { - normal: { - description: 'only the visualization', - statusCode: 200, - response: expectVisualizationResults, - }, - unknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'all objects', - statusCode: 200, - response: expectResultsWithValidTypes, - }, - }, - }); - - findTest(`kibana legacy user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, - }, - tests: { - normal: { - description: 'only the visualization', - statusCode: 200, - response: expectVisualizationResults, - }, - unknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'all objects', - statusCode: 200, - response: expectResultsWithValidTypes, - }, - }, - }); - - findTest(`kibana legacy dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - normal: { - description: 'only the visualization', - statusCode: 200, - response: expectVisualizationResults, - }, - unknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'all objects', - statusCode: 200, - response: expectResultsWithValidTypes, - }, - } - }); - - findTest(`kibana dual-privileges user`, { - auth: { - username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME, - password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD, - }, - tests: { - normal: { - description: 'only the visualization', - statusCode: 200, - response: expectVisualizationResults, - }, - unknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'all objects', - statusCode: 200, - response: expectResultsWithValidTypes, - }, - }, - }); - - findTest(`kibana dual-privileges dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - normal: { - description: 'only the visualization', - statusCode: 200, - response: expectVisualizationResults, - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - noType: { - description: 'all objects', - statusCode: 200, - response: expectResultsWithValidTypes, - }, - } - }); - - findTest(`kibana rbac user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, - }, - tests: { - normal: { - description: 'only the visualization', - statusCode: 200, - response: expectVisualizationResults, - }, - unknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'all objects', - statusCode: 200, - response: expectResultsWithValidTypes, - }, - }, - }); - - findTest(`kibana rbac dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - normal: { - description: 'only the visualization', - statusCode: 200, - response: expectVisualizationResults, - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - noType: { - description: 'all objects', - statusCode: 200, - response: expectResultsWithValidTypes, - }, - } - }); - }); -} diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/get.js b/x-pack/test/rbac_api_integration/apis/saved_objects/get.js deleted file mode 100644 index b640d12055593..0000000000000 --- a/x-pack/test/rbac_api_integration/apis/saved_objects/get.js +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from 'expect.js'; -import { AUTHENTICATION } from './lib/authentication'; - -export default function ({ getService }) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - describe('get', () => { - - const expectResults = (resp) => { - expect(resp.body).to.eql({ - id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - type: 'visualization', - updated_at: '2017-09-21T18:51:23.794Z', - version: resp.body.version, - attributes: { - title: 'Count of requests', - description: '', - version: 1, - // cheat for some of the more complex attributes - visState: resp.body.attributes.visState, - uiStateJSON: resp.body.attributes.uiStateJSON, - kibanaSavedObjectMeta: resp.body.attributes.kibanaSavedObjectMeta - } - }); - }; - - const expectNotFound = (resp) => { - expect(resp.body).to.eql({ - error: 'Not Found', - message: 'Saved object [visualization/foobar] not found', - statusCode: 404, - }); - }; - - const expectRbacForbidden = resp => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to get visualization, missing action:saved_objects/visualization/get` - }); - }; - - const getTest = (description, { auth, tests }) => { - describe(description, () => { - before(() => esArchiver.load('saved_objects/basic')); - after(() => esArchiver.unload('saved_objects/basic')); - - it(`should return ${tests.exists.statusCode}`, async () => ( - await supertest - .get(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`) - .auth(auth.username, auth.password) - .expect(tests.exists.statusCode) - .then(tests.exists.response) - )); - - describe('document does not exist', () => { - it(`should return ${tests.doesntExist.statusCode}`, async () => ( - await supertest - .get(`/api/saved_objects/visualization/foobar`) - .auth(auth.username, auth.password) - .expect(tests.doesntExist.statusCode) - .then(tests.doesntExist.response) - )); - }); - }); - }; - - getTest(`not a kibana user`, { - auth: { - username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, - password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, - }, - tests: { - exists: { - statusCode: 403, - response: expectRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectRbacForbidden, - }, - } - }); - - getTest(`superuser`, { - auth: { - username: AUTHENTICATION.SUPERUSER.USERNAME, - password: AUTHENTICATION.SUPERUSER.PASSWORD, - }, - tests: { - exists: { - statusCode: 200, - response: expectResults, - }, - doesntExist: { - statusCode: 404, - response: expectNotFound, - }, - } - }); - - getTest(`kibana legacy user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, - }, - tests: { - exists: { - statusCode: 200, - response: expectResults, - }, - doesntExist: { - statusCode: 404, - response: expectNotFound, - }, - } - }); - - getTest(`kibana legacy dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - exists: { - statusCode: 200, - response: expectResults, - }, - doesntExist: { - statusCode: 404, - response: expectNotFound, - }, - } - }); - - getTest(`kibana dual-privileges user`, { - auth: { - username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME, - password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD, - }, - tests: { - exists: { - statusCode: 200, - response: expectResults, - }, - doesntExist: { - statusCode: 404, - response: expectNotFound, - }, - } - }); - - getTest(`kibana dual-privileges dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - exists: { - statusCode: 200, - response: expectResults, - }, - doesntExist: { - statusCode: 404, - response: expectNotFound, - }, - } - }); - - getTest(`kibana rbac user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, - }, - tests: { - exists: { - statusCode: 200, - response: expectResults, - }, - doesntExist: { - statusCode: 404, - response: expectNotFound, - }, - } - }); - - getTest(`kibana rbac dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - exists: { - statusCode: 200, - response: expectResults, - }, - doesntExist: { - statusCode: 404, - response: expectNotFound, - }, - } - }); - }); -} diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/index.js b/x-pack/test/rbac_api_integration/apis/saved_objects/index.js deleted file mode 100644 index f5ec186a2e4b2..0000000000000 --- a/x-pack/test/rbac_api_integration/apis/saved_objects/index.js +++ /dev/null @@ -1,153 +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 "./lib/authentication"; - -export default function ({ loadTestFile, getService }) { - const es = getService('es'); - const supertest = getService('supertest'); - - describe('saved_objects', () => { - before(async () => { - await supertest.put('/api/security/role/kibana_legacy_user') - .send({ - elasticsearch: { - indices: [{ - names: ['.kibana'], - privileges: ['manage', 'read', 'index', 'delete'] - }] - } - }); - - await supertest.put('/api/security/role/kibana_legacy_dashboard_only_user') - .send({ - elasticsearch: { - indices: [{ - names: ['.kibana'], - privileges: ['read', 'view_index_metadata'] - }] - } - }); - - await supertest.put('/api/security/role/kibana_dual_privileges_user') - .send({ - elasticsearch: { - indices: [{ - names: ['.kibana'], - privileges: ['manage', 'read', 'index', 'delete'] - }] - }, - kibana: { - global: ['all'] - } - }); - - await supertest.put('/api/security/role/kibana_dual_privileges_dashboard_only_user') - .send({ - elasticsearch: { - indices: [{ - names: ['.kibana'], - privileges: ['read', 'view_index_metadata'] - }] - }, - kibana: { - global: ['read'] - } - }); - - await supertest.put('/api/security/role/kibana_rbac_user') - .send({ - kibana: { - global: ['all'] - } - - }); - - await supertest.put('/api/security/role/kibana_rbac_dashboard_only_user') - .send({ - kibana: { - global: ['read'] - } - }); - - await es.shield.putUser({ - username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, - body: { - password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, - roles: [], - full_name: 'not a kibana user', - email: 'not_a_kibana_user@elastic.co', - } - }); - - await es.shield.putUser({ - username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, - body: { - password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, - roles: ['kibana_legacy_user'], - full_name: 'a kibana legacy user', - email: 'a_kibana_legacy_user@elastic.co', - } - }); - - await es.shield.putUser({ - username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, - body: { - password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, - roles: ["kibana_legacy_dashboard_only_user"], - full_name: 'a kibana legacy dashboard only user', - email: 'a_kibana_legacy_dashboard_only_user@elastic.co', - } - }); - - await es.shield.putUser({ - username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME, - body: { - password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD, - roles: ['kibana_dual_privileges_user'], - full_name: 'a kibana dual_privileges user', - email: 'a_kibana_dual_privileges_user@elastic.co', - } - }); - - await es.shield.putUser({ - username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME, - body: { - password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD, - roles: ["kibana_dual_privileges_dashboard_only_user"], - full_name: 'a kibana dual_privileges dashboard only user', - email: 'a_kibana_dual_privileges_dashboard_only_user@elastic.co', - } - }); - - await es.shield.putUser({ - username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, - body: { - password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, - roles: ['kibana_rbac_user'], - full_name: 'a kibana user', - email: 'a_kibana_rbac_user@elastic.co', - } - }); - - await es.shield.putUser({ - username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, - body: { - password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, - roles: ["kibana_rbac_dashboard_only_user"], - full_name: 'a kibana dashboard only user', - email: 'a_kibana_rbac_dashboard_only_user@elastic.co', - } - }); - }); - loadTestFile(require.resolve('./bulk_get')); - loadTestFile(require.resolve('./create')); - loadTestFile(require.resolve('./delete')); - loadTestFile(require.resolve('./find')); - loadTestFile(require.resolve('./get')); - loadTestFile(require.resolve('./update')); - }); -} diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/update.js b/x-pack/test/rbac_api_integration/apis/saved_objects/update.js deleted file mode 100644 index a4a17ba67fd5e..0000000000000 --- a/x-pack/test/rbac_api_integration/apis/saved_objects/update.js +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from 'expect.js'; -import { AUTHENTICATION } from './lib/authentication'; - -export default function ({ getService }) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - describe('update', () => { - const expectResults = resp => { - // loose uuid validation - expect(resp.body).to.have.property('id').match(/^[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/); - - expect(resp.body).to.eql({ - id: resp.body.id, - type: 'visualization', - updated_at: resp.body.updated_at, - version: 2, - attributes: { - title: 'My second favorite vis' - } - }); - }; - - const expectNotFound = resp => { - expect(resp.body).eql({ - statusCode: 404, - error: 'Not Found', - message: 'Saved object [visualization/not an id] not found' - }); - }; - - const expectRbacForbidden = resp => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to update visualization, missing action:saved_objects/visualization/update` - }); - }; - - const createExpectLegacyForbidden = username => resp => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - //eslint-disable-next-line max-len - message: `action [indices:data/write/update] is unauthorized for user [${username}]: [security_exception] action [indices:data/write/update] is unauthorized for user [${username}]` - }); - }; - - const updateTest = (description, { auth, tests }) => { - describe(description, () => { - before(() => esArchiver.load('saved_objects/basic')); - after(() => esArchiver.unload('saved_objects/basic')); - it(`should return ${tests.exists.statusCode}`, async () => { - await supertest - .put(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`) - .auth(auth.username, auth.password) - .send({ - attributes: { - title: 'My second favorite vis' - } - }) - .expect(tests.exists.statusCode) - .then(tests.exists.response); - }); - - describe('unknown id', () => { - it(`should return ${tests.doesntExist.statusCode}`, async () => { - await supertest - .put(`/api/saved_objects/visualization/not an id`) - .auth(auth.username, auth.password) - .send({ - attributes: { - title: 'My second favorite vis' - } - }) - .expect(tests.doesntExist.statusCode) - .then(tests.doesntExist.response); - }); - }); - }); - }; - - updateTest(`not a kibana user`, { - auth: { - username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, - password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, - }, - tests: { - exists: { - statusCode: 403, - response: expectRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectRbacForbidden, - }, - } - }); - - updateTest(`superuser`, { - auth: { - username: AUTHENTICATION.SUPERUSER.USERNAME, - password: AUTHENTICATION.SUPERUSER.PASSWORD, - }, - tests: { - exists: { - statusCode: 200, - response: expectResults, - }, - doesntExist: { - statusCode: 404, - response: expectNotFound, - }, - } - }); - - updateTest(`kibana legacy user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, - }, - tests: { - exists: { - statusCode: 200, - response: expectResults, - }, - doesntExist: { - statusCode: 404, - response: expectNotFound, - }, - } - }); - - updateTest(`kibana legacy dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - exists: { - statusCode: 403, - response: createExpectLegacyForbidden(AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME), - }, - doesntExist: { - statusCode: 403, - response: createExpectLegacyForbidden(AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME), - }, - } - }); - - updateTest(`kibana dual-privileges user`, { - auth: { - username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME, - password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD, - }, - tests: { - exists: { - statusCode: 200, - response: expectResults, - }, - doesntExist: { - statusCode: 404, - response: expectNotFound, - }, - } - }); - - updateTest(`kibana dual-privileges dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - exists: { - statusCode: 403, - response: expectRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectRbacForbidden, - }, - } - }); - - updateTest(`kibana rbac user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, - }, - tests: { - exists: { - statusCode: 200, - response: expectResults, - }, - doesntExist: { - statusCode: 404, - response: expectNotFound, - }, - } - }); - - updateTest(`kibana rbac dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - exists: { - statusCode: 403, - response: expectRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectRbacForbidden, - }, - } - }); - - }); -} diff --git a/x-pack/test/rbac_api_integration/config.js b/x-pack/test/rbac_api_integration/config.js deleted file mode 100644 index 3ea5546f09cf1..0000000000000 --- a/x-pack/test/rbac_api_integration/config.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import path from 'path'; -import { resolveKibanaPath } from '@kbn/plugin-helpers'; -import { EsProvider } from './services/es'; - -export default async function ({ readConfigFile }) { - - const config = { - kibana: { - api: await readConfigFile(resolveKibanaPath('test/api_integration/config.js')), - functional: await readConfigFile(require.resolve('../../../test/functional/config.js')) - }, - xpack: { - api: await readConfigFile(require.resolve('../api_integration/config.js')) - } - }; - - return { - testFiles: [require.resolve('./apis')], - servers: config.xpack.api.get('servers'), - services: { - es: EsProvider, - esSupertestWithoutAuth: config.xpack.api.get('services.esSupertestWithoutAuth'), - supertest: config.kibana.api.get('services.supertest'), - supertestWithoutAuth: config.xpack.api.get('services.supertestWithoutAuth'), - esArchiver: config.kibana.functional.get('services.esArchiver'), - kibanaServer: config.kibana.functional.get('services.kibanaServer'), - }, - junit: { - reportName: 'X-Pack RBAC API Integration Tests', - }, - - // The saved_objects/basic archives are almost an exact replica of the ones in OSS - // with the exception of a bogus "not-a-visualization" type that I added to make sure - // the find filtering without a type specified worked correctly. Once we have the ability - // to specify more granular access to the objects via the Kibana privileges, this should - // no longer be necessary, and it's only required as long as we do read/all privileges. - esArchiver: { - directory: path.join(__dirname, 'fixtures', 'es_archiver') - }, - - esTestCluster: { - ...config.xpack.api.get('esTestCluster'), - serverArgs: [ - ...config.xpack.api.get('esTestCluster.serverArgs'), - ], - }, - - kbnTestServer: { - ...config.xpack.api.get('kbnTestServer'), - serverArgs: [ - ...config.xpack.api.get('kbnTestServer.serverArgs'), - '--optimize.enabled=false', - '--server.xsrf.disableProtection=true', - ], - }, - }; -} diff --git a/x-pack/test/rbac_api_integration/fixtures/es_archiver/saved_objects/basic/data.json.gz b/x-pack/test/rbac_api_integration/fixtures/es_archiver/saved_objects/basic/data.json.gz deleted file mode 100644 index 910382479979d..0000000000000 Binary files a/x-pack/test/rbac_api_integration/fixtures/es_archiver/saved_objects/basic/data.json.gz and /dev/null differ diff --git a/x-pack/test/saved_object_api_integration/common/config.ts b/x-pack/test/saved_object_api_integration/common/config.ts new file mode 100644 index 0000000000000..de1eb29ac63ab --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/config.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-ignore +import { resolveKibanaPath } from '@kbn/plugin-helpers'; +import path from 'path'; +import { TestInvoker } from './lib/types'; +// @ts-ignore +import { EsProvider } from './services/es'; + +interface CreateTestConfigOptions { + license: string; + disabledPlugins?: string[]; +} + +export function createTestConfig(name: string, options: CreateTestConfigOptions) { + const { license = 'trial', disabledPlugins = [] } = options; + + return async ({ readConfigFile }: TestInvoker) => { + const config = { + kibana: { + api: await readConfigFile(resolveKibanaPath('test/api_integration/config.js')), + functional: await readConfigFile(require.resolve('../../../../test/functional/config.js')), + }, + xpack: { + api: await readConfigFile(require.resolve('../../api_integration/config.js')), + }, + }; + + return { + testFiles: [require.resolve(`../${name}/apis/`)], + servers: config.xpack.api.get('servers'), + services: { + es: EsProvider, + esSupertestWithoutAuth: config.xpack.api.get('services.esSupertestWithoutAuth'), + supertest: config.kibana.api.get('services.supertest'), + supertestWithoutAuth: config.xpack.api.get('services.supertestWithoutAuth'), + esArchiver: config.kibana.functional.get('services.esArchiver'), + kibanaServer: config.kibana.functional.get('services.kibanaServer'), + }, + junit: { + reportName: 'X-Pack Saved Object API Integration Tests -- ' + name, + }, + + esArchiver: { + directory: path.join(__dirname, 'fixtures', 'es_archiver'), + }, + + esTestCluster: { + ...config.xpack.api.get('esTestCluster'), + license, + serverArgs: [ + `xpack.license.self_generated.type=${license}`, + `xpack.security.enabled=${!disabledPlugins.includes('security') && license === 'trial'}`, + ], + }, + + kbnTestServer: { + ...config.xpack.api.get('kbnTestServer'), + serverArgs: [ + ...config.xpack.api.get('kbnTestServer.serverArgs'), + '--optimize.enabled=false', + '--server.xsrf.disableProtection=true', + `--plugin-path=${path.join(__dirname, 'fixtures', 'namespace_agnostic_type_plugin')}`, + ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), + ], + }, + }; + }; +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json new file mode 100644 index 0000000000000..5da6fb43ff1d4 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -0,0 +1,349 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space:default", + "source": { + "type": "space", + "updated_at": "2017-09-21T18:49:16.270Z", + "space": { + "name": "Default Space", + "description": "This is the default space", + "_reserved": true + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space:space_1", + "source": { + "type": "space", + "updated_at": "2017-09-21T18:49:16.270Z", + "space": { + "name": "Space 1", + "description": "This is the first test space" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space:space_2", + "source": { + "type": "space", + "updated_at": "2017-09-21T18:49:16.270Z", + "space": { + "name": "Space 2", + "description": "This is the second test space" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "index-pattern:91200a00-9efd-11e7-acb3-3dab96693fab", + "source": { + "type": "index-pattern", + "updated_at": "2017-09-21T18:49:16.270Z", + "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": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "config:7.0.0-alpha1", + "source": { + "type": "config", + "updated_at": "2017-09-21T18:49:16.302Z", + "config": { + "buildNum": 8467, + "defaultIndex": "91200a00-9efd-11e7-acb3-3dab96693fab" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "visualization:dd7caf20-9efd-11e7-acb3-3dab96693fab", + "source": { + "type": "visualization", + "updated_at": "2017-09-21T18:51:23.794Z", + "visualization": { + "title": "Count of requests", + "visState": "{\"title\":\"Count of requests\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}]}", + "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"91200a00-9efd-11e7-acb3-3dab96693fab\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "dashboard:be3733a0-9efe-11e7-acb3-3dab96693fab", + "source": { + "type": "dashboard", + "updated_at": "2017-09-21T18:57:40.826Z", + "dashboard": { + "title": "Requests", + "hits": 0, + "description": "", + "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"visualization\",\"id\":\"dd7caf20-9efd-11e7-acb3-3dab96693fab\",\"col\":1,\"row\":1}]", + "optionsJSON": "{\"darkTheme\":false}", + "uiStateJSON": "{}", + "version": 1, + "timeRestore": true, + "timeTo": "Fri Sep 18 2015 12:24:38 GMT-0700", + "timeFrom": "Wed Sep 16 2015 22:52:17 GMT-0700", + "refreshInterval": { + "display": "Off", + "pause": false, + "value": 0 + }, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space_1:index-pattern:space_1-91200a00-9efd-11e7-acb3-3dab96693fab", + "source": { + "type": "index-pattern", + "updated_at": "2017-09-21T18:49:16.270Z", + "namespace": "space_1", + "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": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space_1:config:7.0.0-alpha1", + "source": { + "type": "config", + "updated_at": "2017-09-21T18:49:16.302Z", + "namespace": "space_1", + "config": { + "buildNum": 8467, + "defaultIndex": "91200a00-9efd-11e7-acb3-3dab96693fab" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space_1:visualization:space_1-dd7caf20-9efd-11e7-acb3-3dab96693fab", + "source": { + "type": "visualization", + "updated_at": "2017-09-21T18:51:23.794Z", + "namespace": "space_1", + "visualization": { + "title": "Count of requests", + "visState": "{\"title\":\"Count of requests\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}]}", + "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"91200a00-9efd-11e7-acb3-3dab96693fab\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space_1:dashboard:space_1-be3733a0-9efe-11e7-acb3-3dab96693fab", + "source": { + "type": "dashboard", + "updated_at": "2017-09-21T18:57:40.826Z", + "namespace": "space_1", + "dashboard": { + "title": "Requests", + "hits": 0, + "description": "", + "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"visualization\",\"id\":\"dd7caf20-9efd-11e7-acb3-3dab96693fab\",\"col\":1,\"row\":1}]", + "optionsJSON": "{\"darkTheme\":false}", + "uiStateJSON": "{}", + "version": 1, + "timeRestore": true, + "timeTo": "Fri Sep 18 2015 12:24:38 GMT-0700", + "timeFrom": "Wed Sep 16 2015 22:52:17 GMT-0700", + "refreshInterval": { + "display": "Off", + "pause": false, + "value": 0 + }, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + } + } + } + } +} + + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space_2:index-pattern:space_2-91200a00-9efd-11e7-acb3-3dab96693fab", + "source": { + "type": "index-pattern", + "updated_at": "2017-09-21T18:49:16.270Z", + "namespace": "space_2", + "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": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space_2:config:7.0.0-alpha1", + "source": { + "type": "config", + "updated_at": "2017-09-21T18:49:16.302Z", + "namespace": "space_2", + "config": { + "buildNum": 8467, + "defaultIndex": "91200a00-9efd-11e7-acb3-3dab96693fab" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space_2:visualization:space_2-dd7caf20-9efd-11e7-acb3-3dab96693fab", + "source": { + "type": "visualization", + "updated_at": "2017-09-21T18:51:23.794Z", + "namespace": "space_2", + "visualization": { + "title": "Count of requests", + "visState": "{\"title\":\"Count of requests\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}]}", + "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"91200a00-9efd-11e7-acb3-3dab96693fab\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space_2:dashboard:space_2-be3733a0-9efe-11e7-acb3-3dab96693fab", + "source": { + "type": "dashboard", + "updated_at": "2017-09-21T18:57:40.826Z", + "namespace": "space_2", + "dashboard": { + "title": "Requests", + "hits": 0, + "description": "", + "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"visualization\",\"id\":\"dd7caf20-9efd-11e7-acb3-3dab96693fab\",\"col\":1,\"row\":1}]", + "optionsJSON": "{\"darkTheme\":false}", + "uiStateJSON": "{}", + "version": 1, + "timeRestore": true, + "timeTo": "Fri Sep 18 2015 12:24:38 GMT-0700", + "timeFrom": "Wed Sep 16 2015 22:52:17 GMT-0700", + "refreshInterval": { + "display": "Off", + "pause": false, + "value": 0 + }, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "globaltype:8121a00-8efd-21e7-1cb3-34ab966434445", + "source": { + "type": "globaltype", + "updated_at": "2017-09-21T18:59:16.270Z", + "globaltype": { + "name": "My favorite global object" + } + } + } +} diff --git a/x-pack/test/rbac_api_integration/fixtures/es_archiver/saved_objects/basic/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json similarity index 87% rename from x-pack/test/rbac_api_integration/fixtures/es_archiver/saved_objects/basic/mappings.json rename to x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json index 107a45fab187b..6cd530559c8f2 100644 --- a/x-pack/test/rbac_api_integration/fixtures/es_archiver/saved_objects/basic/mappings.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json @@ -30,6 +30,34 @@ } } }, + "namespace": { + "type": "keyword" + }, + "space": { + "properties": { + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + }, + "description": { + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "_reserved": { + "type": "boolean" + } + } + }, "dashboard": { "properties": { "description": { @@ -274,10 +302,23 @@ "type": "text" } } + }, + "globaltype": { + "properties": { + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } } } } }, "aliases": {} } -} \ No newline at end of file +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/index.js b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/index.js new file mode 100644 index 0000000000000..1417444df0661 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/index.js @@ -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 mappings from './mappings.json'; + +export default function (kibana) { + return new kibana.Plugin({ + require: [], + name: 'namespace_agnostic_type_plugin', + uiExports: { + savedObjectsSchema: { + globaltype: { + isNamespaceAgnostic: true + } + }, + mappings, + }, + + config() {}, + }); +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json new file mode 100644 index 0000000000000..b30a2c3877b88 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json @@ -0,0 +1,15 @@ +{ + "globaltype": { + "properties": { + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + } +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/package.json b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/package.json new file mode 100644 index 0000000000000..1a0afbc6bfcb3 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/package.json @@ -0,0 +1,7 @@ +{ + "name": "namespace_agnostic_type_plugin", + "version": "0.0.0", + "kibana": { + "version": "kibana" + } +} diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/lib/authentication.js b/x-pack/test/saved_object_api_integration/common/lib/authentication.ts similarity index 56% rename from x-pack/test/rbac_api_integration/apis/saved_objects/lib/authentication.js rename to x-pack/test/saved_object_api_integration/common/lib/authentication.ts index 5b158a6c8bf37..92888f3263ed9 100644 --- a/x-pack/test/rbac_api_integration/apis/saved_objects/lib/authentication.js +++ b/x-pack/test/saved_object_api_integration/common/lib/authentication.ts @@ -7,34 +7,50 @@ export const AUTHENTICATION = { NOT_A_KIBANA_USER: { USERNAME: 'not_a_kibana_user', - PASSWORD: 'password' + PASSWORD: 'password', }, SUPERUSER: { USERNAME: 'elastic', - PASSWORD: 'changeme' + PASSWORD: 'changeme', }, KIBANA_LEGACY_USER: { USERNAME: 'a_kibana_legacy_user', - PASSWORD: 'password' + PASSWORD: 'password', }, KIBANA_LEGACY_DASHBOARD_ONLY_USER: { USERNAME: 'a_kibana_legacy_dashboard_only_user', - PASSWORD: 'password' + PASSWORD: 'password', }, KIBANA_DUAL_PRIVILEGES_USER: { USERNAME: 'a_kibana_dual_privileges_user', - PASSWORD: 'password' + PASSWORD: 'password', }, KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER: { USERNAME: 'a_kibana_dual_privileges_dashboard_only_user', - PASSWORD: 'password' + PASSWORD: 'password', }, KIBANA_RBAC_USER: { USERNAME: 'a_kibana_rbac_user', - PASSWORD: 'password' + PASSWORD: 'password', }, KIBANA_RBAC_DASHBOARD_ONLY_USER: { USERNAME: 'a_kibana_rbac_dashboard_only_user', - PASSWORD: 'password' - } + PASSWORD: 'password', + }, + KIBANA_RBAC_DEFAULT_SPACE_ALL_USER: { + USERNAME: 'a_kibana_rbac_default_space_all_user', + PASSWORD: 'password', + }, + KIBANA_RBAC_DEFAULT_SPACE_READ_USER: { + USERNAME: 'a_kibana_rbac_default_space_read_user', + PASSWORD: 'password', + }, + KIBANA_RBAC_SPACE_1_ALL_USER: { + USERNAME: 'a_kibana_rbac_space_1_all_user', + PASSWORD: 'password', + }, + KIBANA_RBAC_SPACE_1_READ_USER: { + USERNAME: 'a_kibana_rbac_space_1_read_user', + PASSWORD: 'password', + }, }; diff --git a/x-pack/test/saved_object_api_integration/common/lib/create_users_and_roles.ts b/x-pack/test/saved_object_api_integration/common/lib/create_users_and_roles.ts new file mode 100644 index 0000000000000..4dd2131f83ee4 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/lib/create_users_and_roles.ts @@ -0,0 +1,213 @@ +/* + * 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 { SuperTest } from 'supertest'; +import { AUTHENTICATION } from './authentication'; + +export const createUsersAndRoles = async (es: any, supertest: SuperTest) => { + await supertest.put('/api/security/role/kibana_legacy_user').send({ + elasticsearch: { + indices: [ + { + names: ['.kibana'], + privileges: ['manage', 'read', 'index', 'delete'], + }, + ], + }, + }); + + await supertest.put('/api/security/role/kibana_legacy_dashboard_only_user').send({ + elasticsearch: { + indices: [ + { + names: ['.kibana'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + }); + + await supertest.put('/api/security/role/kibana_dual_privileges_user').send({ + elasticsearch: { + indices: [ + { + names: ['.kibana'], + privileges: ['manage', 'read', 'index', 'delete'], + }, + ], + }, + kibana: { + global: ['all'], + }, + }); + + await supertest.put('/api/security/role/kibana_dual_privileges_dashboard_only_user').send({ + elasticsearch: { + indices: [ + { + names: ['.kibana'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: { + global: ['read'], + }, + }); + + await supertest.put('/api/security/role/kibana_rbac_user').send({ + kibana: { + global: ['all'], + }, + }); + + await supertest.put('/api/security/role/kibana_rbac_dashboard_only_user').send({ + kibana: { + global: ['read'], + }, + }); + + await supertest.put('/api/security/role/kibana_rbac_default_space_all_user').send({ + kibana: { + space: { + default: ['all'], + }, + }, + }); + + await supertest.put('/api/security/role/kibana_rbac_default_space_read_user').send({ + kibana: { + space: { + default: ['read'], + }, + }, + }); + + await supertest.put('/api/security/role/kibana_rbac_space_1_all_user').send({ + kibana: { + space: { + space_1: ['all'], + }, + }, + }); + + await supertest.put('/api/security/role/kibana_rbac_space_1_read_user').send({ + kibana: { + space: { + space_1: ['read'], + }, + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + body: { + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + roles: [], + full_name: 'not a kibana user', + email: 'not_a_kibana_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, + roles: ['kibana_legacy_user'], + full_name: 'a kibana legacy user', + email: 'a_kibana_legacy_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, + roles: ['kibana_legacy_dashboard_only_user'], + full_name: 'a kibana legacy dashboard only user', + email: 'a_kibana_legacy_dashboard_only_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD, + roles: ['kibana_dual_privileges_user'], + full_name: 'a kibana dual_privileges user', + email: 'a_kibana_dual_privileges_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD, + roles: ['kibana_dual_privileges_dashboard_only_user'], + full_name: 'a kibana dual_privileges dashboard only user', + email: 'a_kibana_dual_privileges_dashboard_only_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + roles: ['kibana_rbac_user'], + full_name: 'a kibana user', + email: 'a_kibana_rbac_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + roles: ['kibana_rbac_dashboard_only_user'], + full_name: 'a kibana dashboard only user', + email: 'a_kibana_rbac_dashboard_only_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.PASSWORD, + roles: ['kibana_rbac_default_space_all_user'], + full_name: 'a kibana default space all user', + email: 'a_kibana_rbac_default_space_all_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.PASSWORD, + roles: ['kibana_rbac_default_space_read_user'], + full_name: 'a kibana default space read-only user', + email: 'a_kibana_rbac_default_space_read_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.PASSWORD, + roles: ['kibana_rbac_space_1_all_user'], + full_name: 'a kibana rbac space 1 all user', + email: 'a_kibana_rbac_space_1_all_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.PASSWORD, + roles: ['kibana_rbac_space_1_read_user'], + full_name: 'a kibana rbac space 1 read-only user', + email: 'a_kibana_rbac_space_1_readonly_user@elastic.co', + }, + }); +}; diff --git a/x-pack/test/saved_object_api_integration/common/lib/space_test_utils.ts b/x-pack/test/saved_object_api_integration/common/lib/space_test_utils.ts new file mode 100644 index 0000000000000..1619d77761c84 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/lib/space_test_utils.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 { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; + +export function getUrlPrefix(spaceId: string) { + return spaceId && spaceId !== DEFAULT_SPACE_ID ? `/s/${spaceId}` : ``; +} + +export function getIdPrefix(spaceId: string) { + return spaceId === DEFAULT_SPACE_ID ? '' : `${spaceId}-`; +} + +export function getExpectedSpaceIdProperty(spaceId: string) { + if (spaceId === DEFAULT_SPACE_ID) { + return {}; + } + return { + spaceId, + }; +} diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/spaces.js b/x-pack/test/saved_object_api_integration/common/lib/spaces.ts similarity index 98% rename from x-pack/test/spaces_api_integration/apis/saved_objects/lib/spaces.js rename to x-pack/test/saved_object_api_integration/common/lib/spaces.ts index 905b360c99949..a9c552d4ccd78 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/spaces.js +++ b/x-pack/test/saved_object_api_integration/common/lib/spaces.ts @@ -13,5 +13,5 @@ export const SPACES = { }, DEFAULT: { spaceId: 'default', - } + }, }; diff --git a/x-pack/test/saved_object_api_integration/common/lib/types.ts b/x-pack/test/saved_object_api_integration/common/lib/types.ts new file mode 100644 index 0000000000000..fc6d3d8745fb9 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/lib/types.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. + */ + +export type DescribeFn = (text: string, fn: () => void) => void; + +export interface TestDefinitionAuthentication { + username?: string; + password?: string; +} + +export type LoadTestFileFn = (path: string) => string; + +export type GetServiceFn = (service: string) => any; + +export type ReadConfigFileFn = (path: string) => any; + +export interface TestInvoker { + getService: GetServiceFn; + loadTestFile: LoadTestFileFn; + readConfigFile: ReadConfigFileFn; +} diff --git a/x-pack/test/saved_object_api_integration/common/services/es.js b/x-pack/test/saved_object_api_integration/common/services/es.js new file mode 100644 index 0000000000000..f5ef3be4b4bde --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/services/es.js @@ -0,0 +1,21 @@ +/* + * 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 { format as formatUrl } from 'url'; + +import elasticsearch from 'elasticsearch'; +import shieldPlugin from '../../../../server/lib/esjs_shield_plugin'; +import { TestInvoker } from '../lib/types'; + +export function EsProvider({ getService }: TestInvoker) { + const config = getService('config'); + + return new elasticsearch.Client({ + host: formatUrl(config.get('servers.elasticsearch')), + requestTimeout: config.get('timeouts.esRequestTimeout'), + plugins: [shieldPlugin], + }); +} diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts new file mode 100644 index 0000000000000..a45a378c1784b --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from 'expect.js'; +import { SuperTest } from 'supertest'; +import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; +import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; +import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; + +interface BulkCreateTest { + statusCode: number; + response: (resp: any) => void; +} + +interface BulkCreateTests { + default: BulkCreateTest; +} + +interface BulkCreateTestDefinition { + auth?: TestDefinitionAuthentication; + spaceId?: string; + tests: BulkCreateTests; +} + +export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: SuperTest) { + const isGlobalType = (type: string) => type === 'globaltype'; + + const createBulkRequests = (spaceId: string) => [ + { + type: 'visualization', + id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, + attributes: { + title: 'An existing visualization', + }, + }, + { + type: 'dashboard', + id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, + attributes: { + title: 'A great new dashboard', + }, + }, + { + type: 'globaltype', + id: '05976c65-1145-4858-bbf0-d225cc78a06e', + attributes: { + name: 'A new globaltype object', + }, + }, + { + type: 'globaltype', + id: '8121a00-8efd-21e7-1cb3-34ab966434445', + attributes: { + name: 'An existing globaltype', + }, + }, + ]; + + const makeBulkCreateTest = (describeFn: DescribeFn) => ( + description: string, + definition: BulkCreateTestDefinition + ) => { + const { auth = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition; + + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + it(`should return ${tests.default.statusCode}`, async () => { + await supertest + .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_create`) + .auth(auth.username, auth.password) + .send(createBulkRequests(spaceId)) + .expect(tests.default.statusCode) + .then(tests.default.response); + }); + }); + }; + + const createExpectLegacyForbidden = (username: string) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + // eslint-disable-next-line max-len + message: `action [indices:data/write/bulk] is unauthorized for user [${username}]: [security_exception] action [indices:data/write/bulk] is unauthorized for user [${username}]`, + }); + }; + + const expectRbacForbidden = (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `Unable to bulk_create dashboard,globaltype,visualization, missing action:saved_objects/dashboard/bulk_create,action:saved_objects/globaltype/bulk_create,action:saved_objects/visualization/bulk_create`, + }); + }; + + const createExpectResults = (spaceId = DEFAULT_SPACE_ID) => async (resp: any) => { + expect(resp.body).to.eql({ + saved_objects: [ + { + type: 'visualization', + id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, + error: { + message: 'version conflict, document already exists', + statusCode: 409, + }, + }, + { + type: 'dashboard', + id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, + updated_at: resp.body.saved_objects[1].updated_at, + version: 1, + attributes: { + title: 'A great new dashboard', + }, + }, + { + type: 'globaltype', + id: `05976c65-1145-4858-bbf0-d225cc78a06e`, + updated_at: resp.body.saved_objects[2].updated_at, + version: 1, + attributes: { + name: 'A new globaltype object', + }, + }, + { + type: 'globaltype', + id: '8121a00-8efd-21e7-1cb3-34ab966434445', + error: { + message: 'version conflict, document already exists', + statusCode: 409, + }, + }, + ], + }); + + for (const savedObject of createBulkRequests(spaceId)) { + const expectedSpacePrefix = + spaceId === DEFAULT_SPACE_ID || isGlobalType(savedObject.type) ? '' : `${spaceId}:`; + + // query ES directory to ensure namespace was or wasn't specified + const { _source } = await es.get({ + id: `${expectedSpacePrefix}${savedObject.type}:${savedObject.id}`, + type: 'doc', + index: '.kibana', + }); + + const { namespace: actualNamespace } = _source; + + if (spaceId === DEFAULT_SPACE_ID || isGlobalType(savedObject.type)) { + expect(actualNamespace).to.eql(undefined); + } else { + expect(actualNamespace).to.eql(spaceId); + } + } + }; + + const bulkCreateTest = makeBulkCreateTest(describe); + // @ts-ignore + bulkCreateTest.only = makeBulkCreateTest(describe.only); + + return { + bulkCreateTest, + createExpectLegacyForbidden, + createExpectResults, + expectRbacForbidden, + }; +} diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts new file mode 100644 index 0000000000000..028889d76769d --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from 'expect.js'; +import { SuperTest } from 'supertest'; +import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; +import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; +import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; + +interface BulkGetTest { + statusCode: number; + response: (resp: any) => void; +} + +interface BulkGetTests { + default: BulkGetTest; +} + +interface BulkGetTestDefinition { + auth?: TestDefinitionAuthentication; + spaceId?: string; + otherSpaceId?: string; + tests: BulkGetTests; +} + +export function bulkGetTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const createBulkRequests = (spaceId: string) => [ + { + type: 'visualization', + id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, + }, + { + type: 'dashboard', + id: `${getIdPrefix(spaceId)}does not exist`, + }, + { + type: 'globaltype', + id: '8121a00-8efd-21e7-1cb3-34ab966434445', + }, + ]; + + const makeBulkGetTest = (describeFn: DescribeFn) => ( + description: string, + definition: BulkGetTestDefinition + ) => { + const { auth = {}, spaceId = DEFAULT_SPACE_ID, otherSpaceId, tests } = definition; + + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + it(`should return ${tests.default.statusCode}`, async () => { + await supertest + .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_get`) + .auth(auth.username, auth.password) + .send(createBulkRequests(otherSpaceId || spaceId)) + .expect(tests.default.statusCode) + .then(tests.default.response); + }); + }); + }; + + const createExpectLegacyForbidden = (username: string) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + // eslint-disable-next-line max-len + message: `action [indices:data/read/mget] is unauthorized for user [${username}]: [security_exception] action [indices:data/read/mget] is unauthorized for user [${username}]`, + }); + }; + + const expectRbacForbidden = (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `Unable to bulk_get dashboard,globaltype,visualization, missing action:saved_objects/dashboard/bulk_get,action:saved_objects/globaltype/bulk_get,action:saved_objects/visualization/bulk_get`, + }); + }; + + const createExpectNotFoundResults = (spaceId: string) => (resp: any) => { + expect(resp.body).to.eql({ + saved_objects: [ + { + id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, + type: 'visualization', + error: { + statusCode: 404, + message: 'Not found', + }, + }, + { + id: `${getIdPrefix(spaceId)}does not exist`, + type: 'dashboard', + error: { + statusCode: 404, + message: 'Not found', + }, + }, + { + id: `8121a00-8efd-21e7-1cb3-34ab966434445`, + type: 'globaltype', + updated_at: '2017-09-21T18:59:16.270Z', + version: resp.body.saved_objects[2].version, + attributes: { + name: 'My favorite global object', + }, + }, + ], + }); + }; + + const createExpectResults = (spaceId = DEFAULT_SPACE_ID) => (resp: any) => { + expect(resp.body).to.eql({ + saved_objects: [ + { + id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, + type: 'visualization', + updated_at: '2017-09-21T18:51:23.794Z', + version: resp.body.saved_objects[0].version, + attributes: { + title: 'Count of requests', + description: '', + version: 1, + // cheat for some of the more complex attributes + visState: resp.body.saved_objects[0].attributes.visState, + uiStateJSON: resp.body.saved_objects[0].attributes.uiStateJSON, + kibanaSavedObjectMeta: resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta, + }, + }, + { + id: `${getIdPrefix(spaceId)}does not exist`, + type: 'dashboard', + error: { + statusCode: 404, + message: 'Not found', + }, + }, + { + id: `8121a00-8efd-21e7-1cb3-34ab966434445`, + type: 'globaltype', + updated_at: '2017-09-21T18:59:16.270Z', + version: resp.body.saved_objects[2].version, + attributes: { + name: 'My favorite global object', + }, + }, + ], + }); + }; + + const bulkGetTest = makeBulkGetTest(describe); + // @ts-ignore + bulkGetTest.only = makeBulkGetTest(describe.only); + + return { + bulkGetTest, + createExpectLegacyForbidden, + createExpectNotFoundResults, + createExpectResults, + expectRbacForbidden, + }; +} diff --git a/x-pack/test/saved_object_api_integration/common/suites/create.ts b/x-pack/test/saved_object_api_integration/common/suites/create.ts new file mode 100644 index 0000000000000..bab9b6e069c4c --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/suites/create.ts @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from 'expect.js'; +import { SuperTest } from 'supertest'; +import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; +import { getUrlPrefix } from '../lib/space_test_utils'; +import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; + +interface CreateTest { + statusCode: number; + response: (resp: any) => void; +} + +interface CreateTests { + spaceAware: CreateTest; + notSpaceAware: CreateTest; +} + +interface CreateTestDefinition { + auth?: TestDefinitionAuthentication; + spaceId?: string; + tests: CreateTests; +} + +export function createTestSuiteFactory(es: any, esArchiver: any, supertest: SuperTest) { + const spaceAwareType = 'visualization'; + const notSpaceAwareType = 'globaltype'; + + const makeCreateTest = (describeFn: DescribeFn) => ( + description: string, + definition: CreateTestDefinition + ) => { + const { auth = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition; + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + it(`should return ${tests.spaceAware.statusCode} for a space-aware type`, async () => { + await supertest + .post(`${getUrlPrefix(spaceId)}/api/saved_objects/${spaceAwareType}`) + .auth(auth.username, auth.password) + .send({ + attributes: { + title: 'My favorite vis', + }, + }) + .expect(tests.spaceAware.statusCode) + .then(tests.spaceAware.response); + }); + + it(`should return ${tests.notSpaceAware.statusCode} for a non space-aware type`, async () => { + await supertest + .post(`${getUrlPrefix(spaceId)}/api/saved_objects/${notSpaceAwareType}`) + .auth(auth.username, auth.password) + .send({ + attributes: { + name: `Can't be contained to a space`, + }, + }) + .expect(tests.notSpaceAware.statusCode) + .then(tests.notSpaceAware.response); + }); + }); + }; + + const createTest = makeCreateTest(describe); + // @ts-ignore + createTest.only = makeCreateTest(describe.only); + + const createExpectLegacyForbidden = (username: string) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + // eslint-disable-next-line max-len + message: `action [indices:data/write/index] is unauthorized for user [${username}]: [security_exception] action [indices:data/write/index] is unauthorized for user [${username}]`, + }); + }; + + const createExpectRbacForbidden = (type: string) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `Unable to create ${type}, missing action:saved_objects/${type}/create`, + }); + }; + + const createExpectSpaceAwareResults = (spaceId = DEFAULT_SPACE_ID) => async (resp: any) => { + expect(resp.body) + .to.have.property('id') + .match(/^[0-9a-f-]{36}$/); + + // loose ISO8601 UTC time with milliseconds validation + expect(resp.body) + .to.have.property('updated_at') + .match(/^[\d-]{10}T[\d:\.]{12}Z$/); + + expect(resp.body).to.eql({ + id: resp.body.id, + type: spaceAwareType, + updated_at: resp.body.updated_at, + version: 1, + attributes: { + title: 'My favorite vis', + }, + }); + + const expectedSpacePrefix = spaceId === DEFAULT_SPACE_ID ? '' : `${spaceId}:`; + + // query ES directory to ensure namespace was or wasn't specified + const { _source } = await es.get({ + id: `${expectedSpacePrefix}${spaceAwareType}:${resp.body.id}`, + type: 'doc', + index: '.kibana', + }); + + const { namespace: actualNamespace } = _source; + + if (spaceId === DEFAULT_SPACE_ID) { + expect(actualNamespace).to.eql(undefined); + } else { + expect(actualNamespace).to.eql(spaceId); + } + }; + + const expectNotSpaceAwareResults = async (resp: any) => { + expect(resp.body) + .to.have.property('id') + .match(/^[0-9a-f-]{36}$/); + + // loose ISO8601 UTC time with milliseconds validation + expect(resp.body) + .to.have.property('updated_at') + .match(/^[\d-]{10}T[\d:\.]{12}Z$/); + + expect(resp.body).to.eql({ + id: resp.body.id, + type: notSpaceAwareType, + updated_at: resp.body.updated_at, + version: 1, + attributes: { + name: `Can't be contained to a space`, + }, + }); + + // query ES directory to ensure namespace wasn't specified + const { _source } = await es.get({ + id: `${notSpaceAwareType}:${resp.body.id}`, + type: 'doc', + index: '.kibana', + }); + + const { namespace: actualNamespace } = _source; + + expect(actualNamespace).to.eql(undefined); + }; + + return { + createTest, + createExpectLegacyForbidden, + createExpectSpaceAwareResults, + expectNotSpaceAwareResults, + expectNotSpaceAwareRbacForbidden: createExpectRbacForbidden(notSpaceAwareType), + expectSpaceAwareRbacForbidden: createExpectRbacForbidden(spaceAwareType), + }; +} diff --git a/x-pack/test/saved_object_api_integration/common/suites/delete.ts b/x-pack/test/saved_object_api_integration/common/suites/delete.ts new file mode 100644 index 0000000000000..6ab45db4d8cd7 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/suites/delete.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from 'expect.js'; +import { SuperTest } from 'supertest'; +import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; +import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; +import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; + +interface DeleteTest { + statusCode: number; + response: (resp: any) => void; +} + +interface DeleteTests { + spaceAware: DeleteTest; + notSpaceAware: DeleteTest; + invalidId: DeleteTest; +} + +interface DeleteTestDefinition { + auth?: TestDefinitionAuthentication; + spaceId?: string; + tests: DeleteTests; +} + +export function deleteTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const makeDeleteTest = (describeFn: DescribeFn) => ( + description: string, + definition: DeleteTestDefinition + ) => { + const { auth = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition; + + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + it(`should return ${tests.spaceAware.statusCode} when deleting a space-aware doc`, async () => + await supertest + .delete( + `${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/${getIdPrefix( + spaceId + )}be3733a0-9efe-11e7-acb3-3dab96693fab` + ) + .auth(auth.username, auth.password) + .expect(tests.spaceAware.statusCode) + .then(tests.spaceAware.response)); + + it(`should return ${ + tests.notSpaceAware.statusCode + } when deleting a non-space-aware doc`, async () => + await supertest + .delete( + `${getUrlPrefix( + spaceId + )}/api/saved_objects/globaltype/8121a00-8efd-21e7-1cb3-34ab966434445` + ) + .auth(auth.username, auth.password) + .expect(tests.notSpaceAware.statusCode) + .then(tests.notSpaceAware.response)); + + it(`should return ${tests.invalidId.statusCode} when deleting an unknown doc`, async () => + await supertest + .delete( + `${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/${getIdPrefix( + spaceId + )}not-a-real-id` + ) + .auth(auth.username, auth.password) + .expect(tests.invalidId.statusCode) + .then(tests.invalidId.response)); + }); + }; + + const deleteTest = makeDeleteTest(describe); + // @ts-ignore + deleteTest.only = makeDeleteTest(describe.only); + + const createExpectLegacyForbidden = (username: string) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + // eslint-disable-next-line max-len + message: `action [indices:data/write/delete] is unauthorized for user [${username}]: [security_exception] action [indices:data/write/delete] is unauthorized for user [${username}]`, + }); + }; + + const expectEmpty = (resp: any) => { + expect(resp.body).to.eql({}); + }; + + const createExpectNotFound = (spaceId: string, type: string, id: string) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: `Saved object [${type}/${getIdPrefix(spaceId)}${id}] not found`, + }); + }; + + const createExpectUnknownDocNotFound = (spaceId: string = DEFAULT_SPACE_ID) => (resp: any) => { + createExpectNotFound(spaceId, 'dashboard', `not-a-real-id`)(resp); + }; + + const createExpectRbacForbidden = (type: string) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `Unable to delete ${type}, missing action:saved_objects/${type}/delete`, + }); + }; + + return { + createExpectLegacyForbidden, + createExpectUnknownDocNotFound, + deleteTest, + expectEmpty, + expectRbacSpaceAwareForbidden: createExpectRbacForbidden('dashboard'), + expectRbacNotSpaceAwareForbidden: createExpectRbacForbidden('globaltype'), + expectRbacInvalidIdForbidden: createExpectRbacForbidden('dashboard'), + }; +} diff --git a/x-pack/test/saved_object_api_integration/common/suites/find.ts b/x-pack/test/saved_object_api_integration/common/suites/find.ts new file mode 100644 index 0000000000000..387834a4fa999 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/suites/find.ts @@ -0,0 +1,237 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from 'expect.js'; +import { SuperTest } from 'supertest'; +import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; +import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; +import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; + +interface FindTest { + statusCode: number; + description: string; + response: (resp: any) => void; +} + +interface FindTests { + spaceAwareType: FindTest; + notSpaceAwareType: FindTest; + unknownType: FindTest; + pageBeyondTotal: FindTest; + unknownSearchField: FindTest; + noType: FindTest; +} + +interface FindTestDefinition { + auth?: TestDefinitionAuthentication; + spaceId?: string; + tests: FindTests; +} + +// TODO: add space unaware type +export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const makeFindTest = (describeFn: DescribeFn) => ( + description: string, + definition: FindTestDefinition + ) => { + const { auth = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition; + + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + it(`space aware type should return ${tests.spaceAwareType.statusCode} with ${ + tests.spaceAwareType.description + }`, async () => + await supertest + .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=visualization&fields=title`) + .auth(auth.username, auth.password) + .expect(tests.spaceAwareType.statusCode) + .then(tests.spaceAwareType.response)); + + it(`not space aware type should return ${tests.spaceAwareType.statusCode} with ${ + tests.notSpaceAwareType.description + }`, async () => + await supertest + .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=globaltype&fields=name`) + .auth(auth.username, auth.password) + .expect(tests.notSpaceAwareType.statusCode) + .then(tests.notSpaceAwareType.response)); + + describe('unknown type', () => { + it(`should return ${tests.unknownType.statusCode} with ${ + tests.unknownType.description + }`, async () => + await supertest + .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=wigwags`) + .auth(auth.username, auth.password) + .expect(tests.unknownType.statusCode) + .then(tests.unknownType.response)); + }); + + describe('page beyond total', () => { + it(`should return ${tests.pageBeyondTotal.statusCode} with ${ + tests.pageBeyondTotal.description + }`, async () => + await supertest + .get( + `${getUrlPrefix( + spaceId + )}/api/saved_objects/_find?type=visualization&page=100&per_page=100` + ) + .auth(auth.username, auth.password) + .expect(tests.pageBeyondTotal.statusCode) + .then(tests.pageBeyondTotal.response)); + }); + + describe('unknown search field', () => { + it(`should return ${tests.unknownSearchField.statusCode} with ${ + tests.unknownSearchField.description + }`, async () => + await supertest + .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=wigwags&search_fields=a`) + .auth(auth.username, auth.password) + .expect(tests.unknownSearchField.statusCode) + .then(tests.unknownSearchField.response)); + }); + + describe('no type', () => { + it(`should return ${tests.noType.statusCode} with ${tests.noType.description}`, async () => + await supertest + .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find`) + .auth(auth.username, auth.password) + .expect(tests.noType.statusCode) + .then(tests.noType.response)); + }); + }); + }; + + const findTest = makeFindTest(describe); + // @ts-ignore + findTest.only = makeFindTest(describe.only); + + const createExpectEmpty = (page: number, perPage: number, total: number) => (resp: any) => { + expect(resp.body).to.eql({ + page, + per_page: perPage, + total, + saved_objects: [], + }); + }; + + const createExpectRbacForbidden = (type?: string) => (resp: any) => { + const message = type + ? `Unable to find ${type}, missing action:saved_objects/${type}/find` + : `Not authorized to find saved_object`; + + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message, + }); + }; + + const createExpectLegacyForbidden = (username: string) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + // eslint-disable-next-line max-len + message: `action [indices:data/read/search] is unauthorized for user [${username}]: [security_exception] action [indices:data/read/search] is unauthorized for user [${username}]`, + }); + }; + + const createExpectResults = (spaceId = DEFAULT_SPACE_ID) => (resp: any) => { + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 5, + saved_objects: [ + { + id: `${getIdPrefix(spaceId)}91200a00-9efd-11e7-acb3-3dab96693fab`, + type: 'index-pattern', + updated_at: '2017-09-21T18:49:16.270Z', + version: 1, + attributes: resp.body.saved_objects[0].attributes, + }, + { + id: '7.0.0-alpha1', + type: 'config', + updated_at: '2017-09-21T18:49:16.302Z', + version: 1, + attributes: resp.body.saved_objects[1].attributes, + }, + { + id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, + type: 'visualization', + updated_at: '2017-09-21T18:51:23.794Z', + version: 1, + attributes: resp.body.saved_objects[2].attributes, + }, + { + id: `${getIdPrefix(spaceId)}be3733a0-9efe-11e7-acb3-3dab96693fab`, + type: 'dashboard', + updated_at: '2017-09-21T18:57:40.826Z', + version: 1, + attributes: resp.body.saved_objects[3].attributes, + }, + { + id: `8121a00-8efd-21e7-1cb3-34ab966434445`, + type: 'globaltype', + updated_at: '2017-09-21T18:59:16.270Z', + version: 1, + attributes: { + name: 'My favorite global object', + }, + }, + ], + }); + }; + + const expectNotSpaceAwareResults = (resp: any) => { + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 1, + saved_objects: [ + { + type: 'globaltype', + id: `8121a00-8efd-21e7-1cb3-34ab966434445`, + version: 1, + attributes: { + name: 'My favorite global object', + }, + }, + ], + }); + }; + + const createExpectVisualizationResults = (spaceId = DEFAULT_SPACE_ID) => (resp: any) => { + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 1, + saved_objects: [ + { + type: 'visualization', + id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, + version: 1, + attributes: { + title: 'Count of requests', + }, + }, + ], + }); + }; + + return { + createExpectEmpty, + createExpectRbacForbidden, + createExpectResults, + createExpectLegacyForbidden, + createExpectVisualizationResults, + expectNotSpaceAwareResults, + findTest, + }; +} diff --git a/x-pack/test/saved_object_api_integration/common/suites/get.ts b/x-pack/test/saved_object_api_integration/common/suites/get.ts new file mode 100644 index 0000000000000..7753015033572 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/suites/get.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from 'expect.js'; +import { SuperTest } from 'supertest'; +import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; +import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; +import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; + +interface GetTest { + statusCode: number; + response: (resp: any) => void; +} + +interface GetTests { + spaceAware: GetTest; + notSpaceAware: GetTest; + doesntExist: GetTest; +} + +interface GetTestDefinition { + auth?: TestDefinitionAuthentication; + spaceId?: string; + tests: GetTests; +} + +// TODO: add space unaware type +export function getTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const spaceAwareId = 'dd7caf20-9efd-11e7-acb3-3dab96693fab'; + const notSpaceAwareId = '8121a00-8efd-21e7-1cb3-34ab966434445'; + const doesntExistId = 'foobar'; + const makeGetTest = (describeFn: DescribeFn) => ( + description: string, + definition: GetTestDefinition + ) => { + const { auth = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition; + + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + it(`should return ${ + tests.spaceAware.statusCode + } when getting a space aware doc`, async () => { + await supertest + .get( + `${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${getIdPrefix( + spaceId + )}${spaceAwareId}` + ) + .auth(auth.username, auth.password) + .expect(tests.spaceAware.statusCode) + .then(tests.spaceAware.response); + }); + + it(`should return ${ + tests.notSpaceAware.statusCode + } when deleting a non-space-aware doc`, async () => { + await supertest + .get(`${getUrlPrefix(spaceId)}/api/saved_objects/globaltype/${notSpaceAwareId}`) + .auth(auth.username, auth.password) + .expect(tests.notSpaceAware.statusCode) + .then(tests.notSpaceAware.response); + }); + + describe('document does not exist', () => { + it(`should return ${tests.doesntExist.statusCode}`, async () => { + await supertest + .get( + `${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${getIdPrefix( + spaceId + )}${doesntExistId}` + ) + .auth(auth.username, auth.password) + .expect(tests.doesntExist.statusCode) + .then(tests.doesntExist.response); + }); + }); + }); + }; + + const getTest = makeGetTest(describe); + // @ts-ignore + getTest.only = makeGetTest(describe.only); + + const createExpectLegacyForbidden = (username: string) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + // eslint-disable-next-line max-len + message: `action [indices:data/read/get] is unauthorized for user [${username}]: [security_exception] action [indices:data/read/get] is unauthorized for user [${username}]`, + }); + }; + + const createExpectNotFound = (id: string, spaceId = DEFAULT_SPACE_ID) => (resp: any) => { + expect(resp.body).to.eql({ + error: 'Not Found', + message: `Saved object [visualization/${getIdPrefix(spaceId)}${id}] not found`, + statusCode: 404, + }); + }; + + const createExpectDoesntExistNotFound = (spaceId = DEFAULT_SPACE_ID) => { + return createExpectNotFound(doesntExistId, spaceId); + }; + + const createExpectSpaceAwareNotFound = (spaceId = DEFAULT_SPACE_ID) => { + return createExpectNotFound(spaceAwareId, spaceId); + }; + + const createExpectNotSpaceAwareNotFound = (spaceId = DEFAULT_SPACE_ID) => { + return createExpectNotFound(spaceAwareId, spaceId); + }; + + const createExpectSpaceAwareRbacForbidden = () => (resp: any) => { + expect(resp.body).to.eql({ + error: 'Forbidden', + message: `Unable to get visualization, missing action:saved_objects/visualization/get`, + statusCode: 403, + }); + }; + + const createExpectNotSpaceAwareRbacForbidden = () => (resp: any) => { + expect(resp.body).to.eql({ + error: 'Forbidden', + message: `Unable to get globaltype, missing action:saved_objects/globaltype/get`, + statusCode: 403, + }); + }; + + const createExpectSpaceAwareResults = (spaceId = DEFAULT_SPACE_ID) => (resp: any) => { + expect(resp.body).to.eql({ + id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, + type: 'visualization', + updated_at: '2017-09-21T18:51:23.794Z', + version: resp.body.version, + attributes: { + title: 'Count of requests', + description: '', + version: 1, + // cheat for some of the more complex attributes + visState: resp.body.attributes.visState, + uiStateJSON: resp.body.attributes.uiStateJSON, + kibanaSavedObjectMeta: resp.body.attributes.kibanaSavedObjectMeta, + }, + }); + }; + + const createExpectNotSpaceAwareResults = (spaceId = DEFAULT_SPACE_ID) => (resp: any) => { + expect(resp.body).to.eql({ + id: `${notSpaceAwareId}`, + type: 'globaltype', + updated_at: '2017-09-21T18:59:16.270Z', + version: resp.body.version, + attributes: { + name: 'My favorite global object', + }, + }); + }; + + return { + createExpectNotSpaceAwareNotFound, + createExpectNotSpaceAwareRbacForbidden, + createExpectNotSpaceAwareResults, + createExpectSpaceAwareResults, + createExpectDoesntExistNotFound, + createExpectSpaceAwareNotFound, + createExpectLegacyForbidden, + createExpectSpaceAwareRbacForbidden, + getTest, + }; +} diff --git a/x-pack/test/saved_object_api_integration/common/suites/update.ts b/x-pack/test/saved_object_api_integration/common/suites/update.ts new file mode 100644 index 0000000000000..5f2c7bf82a6e5 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/suites/update.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from 'expect.js'; +import { SuperTest } from 'supertest'; +import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; +import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; +import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; + +interface UpdateTest { + statusCode: number; + response: (resp: any) => void; +} + +interface UpdateTests { + spaceAware: UpdateTest; + notSpaceAware: UpdateTest; + doesntExist: UpdateTest; +} + +interface UpdateTestDefinition { + auth?: TestDefinitionAuthentication; + spaceId?: string; + tests: UpdateTests; +} + +export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const makeUpdateTest = (describeFn: DescribeFn) => ( + description: string, + definition: UpdateTestDefinition + ) => { + const { auth = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition; + + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + it(`should return ${tests.spaceAware.statusCode} for a space-aware doc`, async () => { + await supertest + .put( + `${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${getIdPrefix( + spaceId + )}dd7caf20-9efd-11e7-acb3-3dab96693fab` + ) + .auth(auth.username, auth.password) + .send({ + attributes: { + title: 'My second favorite vis', + }, + }) + .expect(tests.spaceAware.statusCode) + .then(tests.spaceAware.response); + }); + + it(`should return ${tests.notSpaceAware.statusCode} for a non space-aware doc`, async () => { + await supertest + .put( + `${getUrlPrefix( + spaceId + )}/api/saved_objects/globaltype/8121a00-8efd-21e7-1cb3-34ab966434445` + ) + .auth(auth.username, auth.password) + .send({ + attributes: { + name: 'My second favorite', + }, + }) + .expect(tests.notSpaceAware.statusCode) + .then(tests.notSpaceAware.response); + }); + + describe('unknown id', () => { + it(`should return ${tests.doesntExist.statusCode}`, async () => { + await supertest + .put(`${getUrlPrefix(spaceId)}/api/saved_objects/visualization/not an id`) + .auth(auth.username, auth.password) + .send({ + attributes: { + title: 'My second favorite vis', + }, + }) + .expect(tests.doesntExist.statusCode) + .then(tests.doesntExist.response); + }); + }); + }); + }; + + const updateTest = makeUpdateTest(describe); + // @ts-ignore + updateTest.only = makeUpdateTest(describe.only); + + const createExpectLegacyForbidden = (username: string) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + // eslint-disable-next-line max-len + message: `action [indices:data/write/update] is unauthorized for user [${username}]: [security_exception] action [indices:data/write/update] is unauthorized for user [${username}]`, + }); + }; + + const expectSpaceAwareResults = (resp: any) => { + // loose uuid validation ignoring prefix + expect(resp.body) + .to.have.property('id') + .match(/[0-9a-f-]{36}$/); + + // loose ISO8601 UTC time with milliseconds validation + expect(resp.body) + .to.have.property('updated_at') + .match(/^[\d-]{10}T[\d:\.]{12}Z$/); + + expect(resp.body).to.eql({ + id: resp.body.id, + type: 'visualization', + updated_at: resp.body.updated_at, + version: 2, + attributes: { + title: 'My second favorite vis', + }, + }); + }; + + const expectNotSpaceAwareResults = (resp: any) => { + // loose uuid validation + expect(resp.body) + .to.have.property('id') + .match(/^[0-9a-f-]{36}$/); + + // loose ISO8601 UTC time with milliseconds validation + expect(resp.body) + .to.have.property('updated_at') + .match(/^[\d-]{10}T[\d:\.]{12}Z$/); + + expect(resp.body).to.eql({ + id: resp.body.id, + type: 'globaltype', + updated_at: resp.body.updated_at, + version: 2, + attributes: { + name: 'My second favorite', + }, + }); + }; + + const expectNotFound = (resp: any) => { + expect(resp.body).eql({ + statusCode: 404, + error: 'Not Found', + message: 'Saved object [visualization/not an id] not found', + }); + }; + + const createExpectRbacForbidden = (type: string) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `Unable to update ${type}, missing action:saved_objects/${type}/update`, + }); + }; + + return { + createExpectLegacyForbidden, + expectDoesntExistRbacForbidden: createExpectRbacForbidden('visualization'), + expectNotSpaceAwareResults, + expectNotSpaceAwareRbacForbidden: createExpectRbacForbidden('globaltype'), + expectNotFound, + expectSpaceAwareResults, + expectSpaceAwareRbacForbidden: createExpectRbacForbidden('visualization'), + updateTest, + }; +} diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts new file mode 100644 index 0000000000000..6ce55e3470d00 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts @@ -0,0 +1,233 @@ +/* + * 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 { bulkCreateTestSuiteFactory } from '../../common/suites/bulk_create'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + const { + bulkCreateTest, + createExpectLegacyForbidden, + createExpectResults, + expectRbacForbidden, + } = bulkCreateTestSuiteFactory(es, esArchiver, supertest); + + describe('_bulk_create', () => { + [ + { + spaceId: SPACES.DEFAULT.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, + userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithNoKibanaAccess: AUTHENTICATION.NOT_A_KIBANA_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, + userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + }, + ].forEach(scenario => { + bulkCreateTest(`${scenario.notAKibanaUser.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.notAKibanaUser.USERNAME, + password: scenario.notAKibanaUser.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME), + }, + }, + }); + + bulkCreateTest(`${scenario.superuser.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.superuser.USERNAME, + password: scenario.superuser.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + bulkCreateTest( + `${scenario.userWithLegacyAll.USERNAME} within the ${scenario.spaceId} space`, + { + auth: { + username: scenario.userWithLegacyAll.USERNAME, + password: scenario.userWithLegacyAll.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + } + ); + + bulkCreateTest( + `${scenario.userWithLegacyRead.USERNAME} within the ${scenario.spaceId} space`, + { + auth: { + username: scenario.userWithLegacyRead.USERNAME, + password: scenario.userWithLegacyRead.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.userWithLegacyRead.USERNAME), + }, + }, + } + ); + + bulkCreateTest(`${scenario.userWithDualAll.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithDualAll.USERNAME, + password: scenario.userWithDualAll.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + bulkCreateTest(`${scenario.userWithDualRead.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithDualRead.USERNAME, + password: scenario.userWithDualRead.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); + + bulkCreateTest( + `${scenario.userWithAllGlobally.USERNAME} within the ${scenario.spaceId} space`, + { + auth: { + username: scenario.userWithAllGlobally.USERNAME, + password: scenario.userWithAllGlobally.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + } + ); + + bulkCreateTest( + `${scenario.userWithReadGlobally.USERNAME} within the ${scenario.spaceId} space`, + { + auth: { + username: scenario.userWithReadGlobally.USERNAME, + password: scenario.userWithReadGlobally.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + } + ); + + bulkCreateTest( + `${scenario.userWithAllAtSpace.USERNAME} within the ${scenario.spaceId} space`, + { + auth: { + username: scenario.userWithAllAtSpace.USERNAME, + password: scenario.userWithAllAtSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + } + ); + + bulkCreateTest( + `${scenario.userWithReadAtSpace.USERNAME} within the ${scenario.spaceId} space`, + { + auth: { + username: scenario.userWithReadAtSpace.USERNAME, + password: scenario.userWithReadAtSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + } + ); + + bulkCreateTest( + `${scenario.userWithAllAtOtherSpace.USERNAME} within the ${scenario.spaceId} space`, + { + auth: { + username: scenario.userWithAllAtOtherSpace.USERNAME, + password: scenario.userWithAllAtOtherSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + } + ); + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts new file mode 100644 index 0000000000000..8579aa5a986c4 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts @@ -0,0 +1,217 @@ +/* + * 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 { bulkGetTestSuiteFactory } from '../../common/suites/bulk_get'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { + bulkGetTest, + createExpectLegacyForbidden, + createExpectResults, + expectRbacForbidden, + } = bulkGetTestSuiteFactory(esArchiver, supertest); + + describe('_bulk_get', () => { + [ + { + spaceId: SPACES.DEFAULT.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, + userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithNoKibanaAccess: AUTHENTICATION.NOT_A_KIBANA_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, + userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + }, + ].forEach(scenario => { + bulkGetTest(`${scenario.notAKibanaUser.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.notAKibanaUser.USERNAME, + password: scenario.notAKibanaUser.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME), + }, + }, + }); + + bulkGetTest(`${scenario.superuser.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.superuser.USERNAME, + password: scenario.superuser.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + bulkGetTest(`${scenario.userWithLegacyAll.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithLegacyAll.USERNAME, + password: scenario.userWithLegacyAll.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + bulkGetTest(`${scenario.userWithLegacyRead.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithLegacyRead.USERNAME, + password: scenario.userWithLegacyRead.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + bulkGetTest(`${scenario.userWithDualAll.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithDualAll.USERNAME, + password: scenario.userWithDualAll.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + bulkGetTest(`${scenario.userWithDualRead.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithDualRead.USERNAME, + password: scenario.userWithDualRead.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + bulkGetTest(`${scenario.userWithAllGlobally.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithAllGlobally.USERNAME, + password: scenario.userWithAllGlobally.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + bulkGetTest( + `${scenario.userWithReadGlobally.USERNAME} within the ${scenario.spaceId} space`, + { + auth: { + username: scenario.userWithReadGlobally.USERNAME, + password: scenario.userWithReadGlobally.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + } + ); + + bulkGetTest(`${scenario.userWithAllAtSpace.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithAllAtSpace.USERNAME, + password: scenario.userWithAllAtSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + bulkGetTest(`${scenario.userWithReadAtSpace.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithReadAtSpace.USERNAME, + password: scenario.userWithReadAtSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + bulkGetTest( + `${scenario.userWithAllAtOtherSpace.USERNAME} within the ${scenario.spaceId} space`, + { + auth: { + username: scenario.userWithAllAtOtherSpace.USERNAME, + password: scenario.userWithAllAtOtherSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + default: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + } + ); + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts new file mode 100644 index 0000000000000..f147e28c01e98 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts @@ -0,0 +1,261 @@ +/* + * 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 { createTestSuiteFactory } from '../../common/suites/create'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + const { + createTest, + createExpectLegacyForbidden, + createExpectSpaceAwareResults, + expectNotSpaceAwareResults, + expectNotSpaceAwareRbacForbidden, + expectSpaceAwareRbacForbidden, + } = createTestSuiteFactory(es, esArchiver, supertestWithoutAuth); + + describe('create', () => { + [ + { + spaceId: SPACES.DEFAULT.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, + userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithNoKibanaAccess: AUTHENTICATION.NOT_A_KIBANA_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, + userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + }, + ].forEach(scenario => { + createTest(`${scenario.notAKibanaUser.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.notAKibanaUser.USERNAME, + password: scenario.notAKibanaUser.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME), + }, + }, + }); + + createTest(`${scenario.superuser.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.superuser.USERNAME, + password: scenario.superuser.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(scenario.spaceId), + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + }, + }); + + createTest(`${scenario.userWithLegacyAll.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithLegacyAll.USERNAME, + password: scenario.userWithLegacyAll.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(scenario.spaceId), + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + }, + }); + + createTest(`${scenario.userWithLegacyRead.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithLegacyRead.USERNAME, + password: scenario.userWithLegacyRead.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.userWithLegacyRead.USERNAME), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.userWithLegacyRead.USERNAME), + }, + }, + }); + + createTest(`${scenario.userWithDualAll.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithDualAll.USERNAME, + password: scenario.userWithDualAll.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(scenario.spaceId), + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + }, + }); + + createTest(`${scenario.userWithDualRead.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithDualRead.USERNAME, + password: scenario.userWithDualRead.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: expectSpaceAwareRbacForbidden, + }, + notSpaceAware: { + statusCode: 403, + response: expectNotSpaceAwareRbacForbidden, + }, + }, + }); + + createTest(`${scenario.userWithAllGlobally.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithAllGlobally.USERNAME, + password: scenario.userWithAllGlobally.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(scenario.spaceId), + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + }, + }); + + createTest(`${scenario.userWithReadGlobally.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithReadGlobally.USERNAME, + password: scenario.userWithReadGlobally.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: expectSpaceAwareRbacForbidden, + }, + notSpaceAware: { + statusCode: 403, + response: expectNotSpaceAwareRbacForbidden, + }, + }, + }); + + createTest(`${scenario.userWithAllAtSpace.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithAllAtSpace.USERNAME, + password: scenario.userWithAllAtSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(scenario.spaceId), + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + }, + }); + + createTest(`${scenario.userWithReadAtSpace.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithReadAtSpace.USERNAME, + password: scenario.userWithReadAtSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: expectSpaceAwareRbacForbidden, + }, + notSpaceAware: { + statusCode: 403, + response: expectNotSpaceAwareRbacForbidden, + }, + }, + }); + + createTest( + `${scenario.userWithAllAtOtherSpace.USERNAME} within the ${scenario.spaceId} space`, + { + auth: { + username: scenario.userWithAllAtOtherSpace.USERNAME, + password: scenario.userWithAllAtOtherSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: expectSpaceAwareRbacForbidden, + }, + notSpaceAware: { + statusCode: 403, + response: expectNotSpaceAwareRbacForbidden, + }, + }, + } + ); + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/delete.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/delete.ts new file mode 100644 index 0000000000000..39f5aff6ffe1f --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/delete.ts @@ -0,0 +1,305 @@ +/* + * 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 { deleteTestSuiteFactory } from '../../common/suites/delete'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + describe('delete', () => { + const { + createExpectLegacyForbidden, + createExpectUnknownDocNotFound, + deleteTest, + expectEmpty, + expectRbacSpaceAwareForbidden, + expectRbacNotSpaceAwareForbidden, + expectRbacInvalidIdForbidden, + } = deleteTestSuiteFactory(esArchiver, supertest); + + [ + { + spaceId: SPACES.DEFAULT.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, + userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithNoKibanaAccess: AUTHENTICATION.NOT_A_KIBANA_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, + userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + }, + ].forEach(scenario => { + deleteTest(`${scenario.notAKibanaUser.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.notAKibanaUser.USERNAME, + password: scenario.notAKibanaUser.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME), + }, + invalidId: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME), + }, + }, + }); + + deleteTest(`${scenario.superuser.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.superuser.USERNAME, + password: scenario.superuser.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: expectEmpty, + }, + notSpaceAware: { + statusCode: 200, + response: expectEmpty, + }, + invalidId: { + statusCode: 404, + response: createExpectUnknownDocNotFound(scenario.spaceId), + }, + }, + }); + + deleteTest(`${scenario.userWithLegacyAll.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithLegacyAll.USERNAME, + password: scenario.userWithLegacyAll.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: expectEmpty, + }, + notSpaceAware: { + statusCode: 200, + response: expectEmpty, + }, + invalidId: { + statusCode: 404, + response: createExpectUnknownDocNotFound(scenario.spaceId), + }, + }, + }); + + deleteTest(`${scenario.userWithLegacyRead.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithLegacyRead.USERNAME, + password: scenario.userWithLegacyRead.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.userWithLegacyRead.USERNAME), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.userWithLegacyRead.USERNAME), + }, + invalidId: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.userWithLegacyRead.USERNAME), + }, + }, + }); + + deleteTest(`${scenario.userWithDualAll.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithDualAll.USERNAME, + password: scenario.userWithDualAll.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: expectEmpty, + }, + notSpaceAware: { + statusCode: 200, + response: expectEmpty, + }, + invalidId: { + statusCode: 404, + response: createExpectUnknownDocNotFound(scenario.spaceId), + }, + }, + }); + + deleteTest(`${scenario.userWithDualRead.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithDualRead.USERNAME, + password: scenario.userWithDualRead.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: expectRbacSpaceAwareForbidden, + }, + notSpaceAware: { + statusCode: 403, + response: expectRbacNotSpaceAwareForbidden, + }, + invalidId: { + statusCode: 403, + response: expectRbacInvalidIdForbidden, + }, + }, + }); + + deleteTest(`${scenario.userWithAllGlobally.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithAllGlobally.USERNAME, + password: scenario.userWithAllGlobally.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: expectEmpty, + }, + notSpaceAware: { + statusCode: 200, + response: expectEmpty, + }, + invalidId: { + statusCode: 404, + response: createExpectUnknownDocNotFound(scenario.spaceId), + }, + }, + }); + + deleteTest(`${scenario.userWithReadGlobally.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithReadGlobally.USERNAME, + password: scenario.userWithReadGlobally.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: expectRbacSpaceAwareForbidden, + }, + notSpaceAware: { + statusCode: 403, + response: expectRbacNotSpaceAwareForbidden, + }, + invalidId: { + statusCode: 403, + response: expectRbacInvalidIdForbidden, + }, + }, + }); + + deleteTest(`${scenario.userWithAllAtSpace.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithAllAtSpace.USERNAME, + password: scenario.userWithAllAtSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: expectEmpty, + }, + notSpaceAware: { + statusCode: 200, + response: expectEmpty, + }, + invalidId: { + statusCode: 404, + response: createExpectUnknownDocNotFound(scenario.spaceId), + }, + }, + }); + + deleteTest(`${scenario.userWithReadAtSpace.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithReadAtSpace.USERNAME, + password: scenario.userWithReadAtSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: expectRbacSpaceAwareForbidden, + }, + notSpaceAware: { + statusCode: 403, + response: expectRbacNotSpaceAwareForbidden, + }, + invalidId: { + statusCode: 403, + response: expectRbacInvalidIdForbidden, + }, + }, + }); + + deleteTest( + `${scenario.userWithAllAtOtherSpace.USERNAME} within the ${scenario.spaceId} space`, + { + auth: { + username: scenario.userWithAllAtOtherSpace.USERNAME, + password: scenario.userWithAllAtOtherSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: expectRbacSpaceAwareForbidden, + }, + notSpaceAware: { + statusCode: 403, + response: expectRbacNotSpaceAwareForbidden, + }, + invalidId: { + statusCode: 403, + response: expectRbacInvalidIdForbidden, + }, + }, + } + ); + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts new file mode 100644 index 0000000000000..f24d0f7868b97 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts @@ -0,0 +1,503 @@ +/* + * 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 { findTestSuiteFactory } from '../../common/suites/find'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + describe('find', () => { + const { + createExpectEmpty, + createExpectRbacForbidden, + createExpectResults, + createExpectLegacyForbidden, + createExpectVisualizationResults, + expectNotSpaceAwareResults, + findTest, + } = findTestSuiteFactory(esArchiver, supertest); + + [ + { + spaceId: SPACES.DEFAULT.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, + userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithNoKibanaAccess: AUTHENTICATION.NOT_A_KIBANA_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, + userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + }, + ].forEach(scenario => { + findTest(`${scenario.notAKibanaUser.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAwareType: { + description: 'forbidden login and find visualization message', + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + notSpaceAwareType: { + description: 'forbidden login and find globaltype message', + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + unknownType: { + description: 'forbidden login and find wigwags message', + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + pageBeyondTotal: { + description: 'forbidden login and find visualization message', + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + unknownSearchField: { + description: 'forbidden login and find wigwags message', + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + noType: { + description: `forbidded can't find any types`, + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + }, + }); + + findTest(`${scenario.superuser.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: AUTHENTICATION.SUPERUSER.USERNAME, + password: AUTHENTICATION.SUPERUSER.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: createExpectVisualizationResults(scenario.spaceId), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + unknownType: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + findTest(`${scenario.userWithLegacyAll.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: createExpectVisualizationResults(scenario.spaceId), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + unknownType: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + findTest(`${scenario.userWithLegacyRead.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: createExpectVisualizationResults(scenario.spaceId), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + unknownType: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + findTest(`${scenario.userWithDualAll.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME, + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: createExpectVisualizationResults(scenario.spaceId), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + unknownType: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + findTest(`${scenario.userWithDualRead.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: createExpectVisualizationResults(scenario.spaceId), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + unknownType: { + description: 'forbidden find wigwags message', + statusCode: 403, + response: createExpectRbacForbidden('wigwags'), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'forbidden find wigwags message', + statusCode: 403, + response: createExpectRbacForbidden('wigwags'), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + findTest(`${scenario.userWithAllGlobally.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: createExpectVisualizationResults(scenario.spaceId), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + unknownType: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + findTest(`${scenario.userWithReadGlobally.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: createExpectVisualizationResults(scenario.spaceId), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + unknownType: { + description: 'forbidden find wigwags message', + statusCode: 403, + response: createExpectRbacForbidden('wigwags'), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'forbidden find wigwags message', + statusCode: 403, + response: createExpectRbacForbidden('wigwags'), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + findTest(`${scenario.userWithAllAtSpace.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithAllAtSpace.USERNAME, + password: scenario.userWithAllAtSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: createExpectVisualizationResults(scenario.spaceId), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + unknownType: { + description: 'forbidden and find wigwags message', + statusCode: 403, + response: createExpectRbacForbidden('wigwags'), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'forbidden and find wigwags message', + statusCode: 403, + response: createExpectRbacForbidden('wigwags'), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + findTest(`${scenario.userWithReadAtSpace.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithReadAtSpace.USERNAME, + password: scenario.userWithReadAtSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: createExpectVisualizationResults(scenario.spaceId), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + unknownType: { + description: 'forbidden and find wigwags message', + statusCode: 403, + response: createExpectRbacForbidden('wigwags'), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'forbidden and find wigwags message', + statusCode: 403, + response: createExpectRbacForbidden('wigwags'), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + findTest( + `${scenario.userWithAllAtOtherSpace.USERNAME} within the ${scenario.spaceId} space`, + { + auth: { + username: scenario.userWithAllAtOtherSpace.USERNAME, + password: scenario.userWithAllAtOtherSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAwareType: { + description: 'forbidden login and find visualization message', + statusCode: 403, + response: createExpectRbacForbidden('visualization'), + }, + notSpaceAwareType: { + description: 'forbidden login and find globaltype message', + statusCode: 403, + response: createExpectRbacForbidden('globaltype'), + }, + unknownType: { + description: 'forbidden login and find wigwags message', + statusCode: 403, + response: createExpectRbacForbidden('wigwags'), + }, + pageBeyondTotal: { + description: 'forbidden login and find visualization message', + statusCode: 403, + response: createExpectRbacForbidden('visualization'), + }, + unknownSearchField: { + description: 'forbidden login and find wigwags message', + statusCode: 403, + response: createExpectRbacForbidden('wigwags'), + }, + noType: { + description: `forbidded can't find any types`, + statusCode: 403, + response: createExpectRbacForbidden(), + }, + }, + } + ); + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/get.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/get.ts new file mode 100644 index 0000000000000..b718a7a07d9bc --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/get.ts @@ -0,0 +1,302 @@ +/* + * 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 { getTestSuiteFactory } from '../../common/suites/get'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { + createExpectDoesntExistNotFound, + createExpectLegacyForbidden, + createExpectSpaceAwareRbacForbidden, + createExpectSpaceAwareResults, + createExpectNotSpaceAwareResults, + createExpectNotSpaceAwareRbacForbidden, + getTest, + } = getTestSuiteFactory(esArchiver, supertest); + + describe('get', () => { + [ + { + spaceId: SPACES.DEFAULT.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, + userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithNoKibanaAccess: AUTHENTICATION.NOT_A_KIBANA_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, + userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + }, + ].forEach(scenario => { + getTest(`${scenario.notAKibanaUser.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.notAKibanaUser.USERNAME, + password: scenario.notAKibanaUser.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME), + }, + doesntExist: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME), + }, + }, + }); + + getTest(`${scenario.superuser.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.superuser.USERNAME, + password: scenario.superuser.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(scenario.spaceId), + }, + notSpaceAware: { + statusCode: 200, + response: createExpectNotSpaceAwareResults(scenario.spaceId), + }, + doesntExist: { + statusCode: 404, + response: createExpectDoesntExistNotFound(scenario.spaceId), + }, + }, + }); + + getTest(`${scenario.userWithLegacyAll.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithLegacyAll.USERNAME, + password: scenario.userWithLegacyAll.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(scenario.spaceId), + }, + notSpaceAware: { + statusCode: 200, + response: createExpectNotSpaceAwareResults(scenario.spaceId), + }, + doesntExist: { + statusCode: 404, + response: createExpectDoesntExistNotFound(scenario.spaceId), + }, + }, + }); + + getTest(`${scenario.userWithLegacyRead.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithLegacyRead.USERNAME, + password: scenario.userWithLegacyRead.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(scenario.spaceId), + }, + notSpaceAware: { + statusCode: 200, + response: createExpectNotSpaceAwareResults(scenario.spaceId), + }, + doesntExist: { + statusCode: 404, + response: createExpectDoesntExistNotFound(scenario.spaceId), + }, + }, + }); + + getTest(`${scenario.userWithDualAll.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithDualAll.USERNAME, + password: scenario.userWithDualAll.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(scenario.spaceId), + }, + notSpaceAware: { + statusCode: 200, + response: createExpectNotSpaceAwareResults(scenario.spaceId), + }, + doesntExist: { + statusCode: 404, + response: createExpectDoesntExistNotFound(scenario.spaceId), + }, + }, + }); + + getTest(`${scenario.userWithDualRead.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithDualRead.USERNAME, + password: scenario.userWithDualRead.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(scenario.spaceId), + }, + notSpaceAware: { + statusCode: 200, + response: createExpectNotSpaceAwareResults(scenario.spaceId), + }, + doesntExist: { + statusCode: 404, + response: createExpectDoesntExistNotFound(scenario.spaceId), + }, + }, + }); + + getTest(`${scenario.userWithAllGlobally.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithAllGlobally.USERNAME, + password: scenario.userWithAllGlobally.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(scenario.spaceId), + }, + notSpaceAware: { + statusCode: 200, + response: createExpectNotSpaceAwareResults(scenario.spaceId), + }, + doesntExist: { + statusCode: 404, + response: createExpectDoesntExistNotFound(scenario.spaceId), + }, + }, + }); + + getTest(`${scenario.userWithReadGlobally.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithReadGlobally.USERNAME, + password: scenario.userWithReadGlobally.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(scenario.spaceId), + }, + notSpaceAware: { + statusCode: 200, + response: createExpectNotSpaceAwareResults(scenario.spaceId), + }, + doesntExist: { + statusCode: 404, + response: createExpectDoesntExistNotFound(scenario.spaceId), + }, + }, + }); + + getTest(`${scenario.userWithAllAtSpace.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithAllAtSpace.USERNAME, + password: scenario.userWithAllAtSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(scenario.spaceId), + }, + notSpaceAware: { + statusCode: 200, + response: createExpectNotSpaceAwareResults(scenario.spaceId), + }, + doesntExist: { + statusCode: 404, + response: createExpectDoesntExistNotFound(scenario.spaceId), + }, + }, + }); + + getTest(`${scenario.userWithReadAtSpace.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithReadAtSpace.USERNAME, + password: scenario.userWithReadAtSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(scenario.spaceId), + }, + notSpaceAware: { + statusCode: 200, + response: createExpectNotSpaceAwareResults(scenario.spaceId), + }, + doesntExist: { + statusCode: 404, + response: createExpectDoesntExistNotFound(scenario.spaceId), + }, + }, + }); + + getTest(`${scenario.userWithAllAtOtherSpace.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithAllAtOtherSpace.USERNAME, + password: scenario.userWithAllAtOtherSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectSpaceAwareRbacForbidden(), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectNotSpaceAwareRbacForbidden(), + }, + doesntExist: { + statusCode: 403, + response: createExpectSpaceAwareRbacForbidden(), + }, + }, + }); + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts new file mode 100644 index 0000000000000..d25a9b852b789 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createUsersAndRoles } from '../../common/lib/create_users_and_roles'; +import { TestInvoker } from '../../common/lib/types'; + +// tslint:disable:no-default-export +export default function({ getService, loadTestFile }: TestInvoker) { + const es = getService('es'); + const supertest = getService('supertest'); + + describe('saved objects security and spaces enabled', () => { + before(async () => { + await createUsersAndRoles(es, supertest); + }); + + loadTestFile(require.resolve('./bulk_create')); + loadTestFile(require.resolve('./bulk_get')); + loadTestFile(require.resolve('./create')); + loadTestFile(require.resolve('./delete')); + loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./update')); + }); +} diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/update.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/update.ts new file mode 100644 index 0000000000000..ae6465fa91e28 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/update.ts @@ -0,0 +1,306 @@ +/* + * 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 { updateTestSuiteFactory } from '../../common/suites/update'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + describe('update', () => { + const { + createExpectLegacyForbidden, + expectDoesntExistRbacForbidden, + expectNotSpaceAwareResults, + expectNotSpaceAwareRbacForbidden, + expectNotFound, + expectSpaceAwareRbacForbidden, + expectSpaceAwareResults, + updateTest, + } = updateTestSuiteFactory(esArchiver, supertest); + + [ + { + spaceId: SPACES.DEFAULT.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, + userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithNoKibanaAccess: AUTHENTICATION.NOT_A_KIBANA_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, + userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + }, + ].forEach(scenario => { + updateTest(`${scenario.notAKibanaUser.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + doesntExist: { + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + }, + }); + + updateTest(`${scenario.superuser.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.superuser.USERNAME, + password: scenario.superuser.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: expectSpaceAwareResults, + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + }, + }); + + updateTest(`${scenario.userWithLegacyAll.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithLegacyAll.USERNAME, + password: scenario.userWithLegacyAll.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: expectSpaceAwareResults, + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + }, + }); + + updateTest(`${scenario.userWithLegacyRead.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithLegacyRead.USERNAME, + password: scenario.userWithLegacyRead.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.userWithLegacyRead.USERNAME), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.userWithLegacyRead.USERNAME), + }, + doesntExist: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.userWithLegacyRead.USERNAME), + }, + }, + }); + + updateTest(`${scenario.userWithDualAll.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithDualAll.USERNAME, + password: scenario.userWithDualAll.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: expectSpaceAwareResults, + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + }, + }); + + updateTest(`${scenario.userWithDualRead.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithDualRead.USERNAME, + password: scenario.userWithDualRead.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: expectSpaceAwareRbacForbidden, + }, + notSpaceAware: { + statusCode: 403, + response: expectNotSpaceAwareRbacForbidden, + }, + doesntExist: { + statusCode: 403, + response: expectDoesntExistRbacForbidden, + }, + }, + }); + + updateTest(`${scenario.userWithAllGlobally.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithAllGlobally.USERNAME, + password: scenario.userWithAllGlobally.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: expectSpaceAwareResults, + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + }, + }); + + updateTest(`${scenario.userWithReadGlobally.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithReadGlobally.USERNAME, + password: scenario.userWithReadGlobally.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: expectSpaceAwareRbacForbidden, + }, + notSpaceAware: { + statusCode: 403, + response: expectNotSpaceAwareRbacForbidden, + }, + doesntExist: { + statusCode: 403, + response: expectDoesntExistRbacForbidden, + }, + }, + }); + + updateTest(`${scenario.userWithAllAtSpace.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithAllAtSpace.USERNAME, + password: scenario.userWithAllAtSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 200, + response: expectSpaceAwareResults, + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + }, + }); + + updateTest(`${scenario.userWithReadAtSpace.USERNAME} within the ${scenario.spaceId} space`, { + auth: { + username: scenario.userWithReadAtSpace.USERNAME, + password: scenario.userWithReadAtSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: expectSpaceAwareRbacForbidden, + }, + notSpaceAware: { + statusCode: 403, + response: expectNotSpaceAwareRbacForbidden, + }, + doesntExist: { + statusCode: 403, + response: expectDoesntExistRbacForbidden, + }, + }, + }); + + updateTest( + `${scenario.userWithAllAtOtherSpace.USERNAME} within the ${scenario.spaceId} space`, + { + auth: { + username: scenario.userWithAllAtOtherSpace.USERNAME, + password: scenario.userWithAllAtOtherSpace.PASSWORD, + }, + spaceId: scenario.spaceId, + tests: { + spaceAware: { + statusCode: 403, + response: expectSpaceAwareRbacForbidden, + }, + notSpaceAware: { + statusCode: 403, + response: expectNotSpaceAwareRbacForbidden, + }, + doesntExist: { + statusCode: 403, + response: expectDoesntExistRbacForbidden, + }, + }, + } + ); + }); + }); +} diff --git a/x-pack/test/rbac_api_integration/apis/index.js b/x-pack/test/saved_object_api_integration/security_and_spaces/config.ts similarity index 51% rename from x-pack/test/rbac_api_integration/apis/index.js rename to x-pack/test/saved_object_api_integration/security_and_spaces/config.ts index cf26e2e7cf4d8..81cf9d85671d1 100644 --- a/x-pack/test/rbac_api_integration/apis/index.js +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/config.ts @@ -4,10 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export default function ({ loadTestFile }) { - describe('apis RBAC', () => { - loadTestFile(require.resolve('./es')); - loadTestFile(require.resolve('./privileges')); - loadTestFile(require.resolve('./saved_objects')); - }); -} +import { createTestConfig } from '../common/config'; + +// tslint:disable:no-default-export +export default createTestConfig('security_and_spaces', { license: 'trial' }); diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts new file mode 100644 index 0000000000000..791425ddb84fe --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts @@ -0,0 +1,191 @@ +/* + * 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 { TestInvoker } from '../../common/lib/types'; +import { bulkCreateTestSuiteFactory } from '../../common/suites/bulk_create'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + const { + bulkCreateTest, + createExpectLegacyForbidden, + createExpectResults, + expectRbacForbidden, + } = bulkCreateTestSuiteFactory(es, esArchiver, supertest); + + describe('_bulk_create', () => { + bulkCreateTest(`not a kibana user`, { + auth: { + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + }, + }); + + bulkCreateTest(`superuser`, { + auth: { + username: AUTHENTICATION.SUPERUSER.USERNAME, + password: AUTHENTICATION.SUPERUSER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(), + }, + }, + }); + + bulkCreateTest(`kibana legacy user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(), + }, + }, + }); + + bulkCreateTest(`kibana legacy dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME + ), + }, + }, + }); + + bulkCreateTest(`kibana dual-privileges user`, { + auth: { + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME, + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(), + }, + }, + }); + + bulkCreateTest(`kibana dual-privileges dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); + + bulkCreateTest(`kibana rbac user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(), + }, + }, + }); + + bulkCreateTest(`kibana rbac dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); + + bulkCreateTest(`kibana rbac default space all user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + }, + }); + + bulkCreateTest(`kibana rbac default space read user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + }, + }); + + bulkCreateTest(`kibana rbac space 1 all user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + }, + }); + + bulkCreateTest(`kibana rbac space 1 readonly user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + }, + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_get.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_get.ts new file mode 100644 index 0000000000000..60aaf8b64d86d --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_get.ts @@ -0,0 +1,186 @@ +/* + * 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 { TestInvoker } from '../../common/lib/types'; +import { bulkGetTestSuiteFactory } from '../../common/suites/bulk_get'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { bulkGetTest, createExpectLegacyForbidden, createExpectResults } = bulkGetTestSuiteFactory( + esArchiver, + supertest + ); + + describe('_bulk_get', () => { + bulkGetTest(`not a kibana user`, { + auth: { + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + }, + }); + + bulkGetTest(`superuser`, { + auth: { + username: AUTHENTICATION.SUPERUSER.USERNAME, + password: AUTHENTICATION.SUPERUSER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(), + }, + }, + }); + + bulkGetTest(`kibana legacy user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(), + }, + }, + }); + + bulkGetTest(`kibana legacy dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(), + }, + }, + }); + + bulkGetTest(`kibana dual-privileges user`, { + auth: { + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME, + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(), + }, + }, + }); + + bulkGetTest(`kibana dual-privileges dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(), + }, + }, + }); + + bulkGetTest(`kibana rbac user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(), + }, + }, + }); + + bulkGetTest(`kibana rbac dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(), + }, + }, + }); + + bulkGetTest(`kibana rbac default space all user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + }, + }); + + bulkGetTest(`kibana rbac default space read user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + }, + }); + + bulkGetTest(`kibana rbac space 1 all user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + }, + }); + + bulkGetTest(`kibana rbac space 1 readonly user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + }, + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts new file mode 100644 index 0000000000000..21050effbdd99 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts @@ -0,0 +1,251 @@ +/* + * 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 { TestInvoker } from '../../common/lib/types'; +import { createTestSuiteFactory } from '../../common/suites/create'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + const { + createTest, + createExpectLegacyForbidden, + createExpectSpaceAwareResults, + expectNotSpaceAwareResults, + expectNotSpaceAwareRbacForbidden, + expectSpaceAwareRbacForbidden, + } = createTestSuiteFactory(es, esArchiver, supertestWithoutAuth); + + describe('create', () => { + createTest(`not a kibana user`, { + auth: { + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + }, + }); + + createTest(`superuser`, { + auth: { + username: AUTHENTICATION.SUPERUSER.USERNAME, + password: AUTHENTICATION.SUPERUSER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(), + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + }, + }); + + createTest(`kibana legacy user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(), + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + }, + }); + + createTest(`kibana legacy dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME + ), + }, + }, + }); + + createTest(`kibana dual-privileges user`, { + auth: { + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME, + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(), + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + }, + }); + + createTest(`kibana dual-privileges dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: expectSpaceAwareRbacForbidden, + }, + notSpaceAware: { + statusCode: 403, + response: expectNotSpaceAwareRbacForbidden, + }, + }, + }); + + createTest(`kibana rbac user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(), + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + }, + }); + + createTest(`kibana rbac dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: expectSpaceAwareRbacForbidden, + }, + notSpaceAware: { + statusCode: 403, + response: expectNotSpaceAwareRbacForbidden, + }, + }, + }); + + createTest(`kibana rbac default space all user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + }, + }); + + createTest(`kibana rbac default space read user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + }, + }); + + createTest(`kibana rbac space 1 all user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + }, + }); + + createTest(`kibana rbac space 1 readonly user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + }, + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/delete.ts b/x-pack/test/saved_object_api_integration/security_only/apis/delete.ts new file mode 100644 index 0000000000000..634a3d60f1322 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_only/apis/delete.ts @@ -0,0 +1,309 @@ +/* + * 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 { TestInvoker } from '../../common/lib/types'; +import { deleteTestSuiteFactory } from '../../common/suites/delete'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + describe('delete', () => { + const { + createExpectLegacyForbidden, + createExpectUnknownDocNotFound, + deleteTest, + expectEmpty, + expectRbacSpaceAwareForbidden, + expectRbacNotSpaceAwareForbidden, + expectRbacInvalidIdForbidden, + } = deleteTestSuiteFactory(esArchiver, supertest); + + deleteTest(`not a kibana user`, { + auth: { + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + invalidId: { + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + }, + }); + + deleteTest(`superuser`, { + auth: { + username: AUTHENTICATION.SUPERUSER.USERNAME, + password: AUTHENTICATION.SUPERUSER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: expectEmpty, + }, + notSpaceAware: { + statusCode: 200, + response: expectEmpty, + }, + invalidId: { + statusCode: 404, + response: createExpectUnknownDocNotFound(), + }, + }, + }); + + deleteTest(`kibana legacy user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: expectEmpty, + }, + notSpaceAware: { + statusCode: 200, + response: expectEmpty, + }, + invalidId: { + statusCode: 404, + response: createExpectUnknownDocNotFound(), + }, + }, + }); + + deleteTest(`kibana legacy dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME + ), + }, + invalidId: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME + ), + }, + }, + }); + + deleteTest(`kibana dual-privileges user`, { + auth: { + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME, + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: expectEmpty, + }, + notSpaceAware: { + statusCode: 200, + response: expectEmpty, + }, + invalidId: { + statusCode: 404, + response: createExpectUnknownDocNotFound(), + }, + }, + }); + + deleteTest(`kibana dual-privileges dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: expectRbacSpaceAwareForbidden, + }, + notSpaceAware: { + statusCode: 403, + response: expectRbacNotSpaceAwareForbidden, + }, + invalidId: { + statusCode: 403, + response: expectRbacInvalidIdForbidden, + }, + }, + }); + + deleteTest(`kibana rbac user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: expectEmpty, + }, + notSpaceAware: { + statusCode: 200, + response: expectEmpty, + }, + invalidId: { + statusCode: 404, + response: createExpectUnknownDocNotFound(), + }, + }, + }); + + deleteTest(`kibana rbac dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: expectRbacSpaceAwareForbidden, + }, + notSpaceAware: { + statusCode: 403, + response: expectRbacNotSpaceAwareForbidden, + }, + invalidId: { + statusCode: 403, + response: expectRbacInvalidIdForbidden, + }, + }, + }); + + deleteTest(`kibana rbac default space all user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + invalidId: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + }, + }); + + deleteTest(`kibana rbac default space read user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + invalidId: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + }, + }); + + deleteTest(`kibana rbac space 1 all user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + invalidId: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + }, + }); + + deleteTest(`kibana rbac space 1 readonly user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + invalidId: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + }, + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/find.ts b/x-pack/test/saved_object_api_integration/security_only/apis/find.ts new file mode 100644 index 0000000000000..04ba16e075ba6 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_only/apis/find.ts @@ -0,0 +1,543 @@ +/* + * 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 { TestInvoker } from '../../common/lib/types'; +import { findTestSuiteFactory } from '../../common/suites/find'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + describe('find', () => { + const { + createExpectEmpty, + createExpectRbacForbidden, + createExpectResults, + createExpectLegacyForbidden, + createExpectVisualizationResults, + expectNotSpaceAwareResults, + findTest, + } = findTestSuiteFactory(esArchiver, supertest); + + findTest(`not a kibana user`, { + auth: { + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + }, + tests: { + spaceAwareType: { + description: 'forbidden login and find visualization message', + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + notSpaceAwareType: { + description: 'forbidden legacy message', + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + unknownType: { + description: 'forbidden login and find wigwags message', + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + pageBeyondTotal: { + description: 'forbidden login and find visualization message', + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + unknownSearchField: { + description: 'forbidden login and find wigwags message', + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + noType: { + description: `forbidded can't find any types`, + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + }, + }); + + findTest(`superuser`, { + auth: { + username: AUTHENTICATION.SUPERUSER.USERNAME, + password: AUTHENTICATION.SUPERUSER.PASSWORD, + }, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: createExpectVisualizationResults(), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + unknownType: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: createExpectResults(), + }, + }, + }); + + findTest(`kibana legacy user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, + }, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: createExpectVisualizationResults(), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + unknownType: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: createExpectResults(), + }, + }, + }); + + findTest(`kibana legacy dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: createExpectVisualizationResults(), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + unknownType: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: createExpectResults(), + }, + }, + }); + + findTest(`kibana dual-privileges user`, { + auth: { + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME, + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD, + }, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: createExpectVisualizationResults(), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + unknownType: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: createExpectResults(), + }, + }, + }); + + findTest(`kibana dual-privileges dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: createExpectVisualizationResults(), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + unknownType: { + description: 'forbidden find wigwags message', + statusCode: 403, + response: createExpectRbacForbidden('wigwags'), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'forbidden find wigwags message', + statusCode: 403, + response: createExpectRbacForbidden('wigwags'), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: createExpectResults(), + }, + }, + }); + + findTest(`kibana rbac user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + }, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: createExpectVisualizationResults(), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + unknownType: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: createExpectResults(), + }, + }, + }); + + findTest(`kibana rbac dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: createExpectVisualizationResults(), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + unknownType: { + description: 'forbidden find wigwags message', + statusCode: 403, + response: createExpectRbacForbidden('wigwags'), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'forbidden find wigwags message', + statusCode: 403, + response: createExpectRbacForbidden('wigwags'), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: createExpectResults(), + }, + }, + }); + + findTest(`kibana rbac default space all user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.PASSWORD, + }, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + unknownType: { + description: 'empty result', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + unknownSearchField: { + description: 'empty result', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + noType: { + description: 'all objects', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + }, + }); + + findTest(`kibana rbac default space read user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.PASSWORD, + }, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + unknownType: { + description: 'empty result', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + unknownSearchField: { + description: 'empty result', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + noType: { + description: 'all objects', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + }, + }); + + findTest(`kibana rbac space 1 all user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.PASSWORD, + }, + tests: { + spaceAwareType: { + description: 'forbidden login and find visualization message', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + unknownType: { + description: 'forbidden login and find wigwags message', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + pageBeyondTotal: { + description: 'forbidden login and find visualization message', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + unknownSearchField: { + description: 'forbidden login and find wigwags message', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + noType: { + description: `forbidded can't find any types`, + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + }, + }); + + findTest(`kibana rbac space 1 readonly user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.PASSWORD, + }, + tests: { + spaceAwareType: { + description: 'forbidden login and find visualization message', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + notSpaceAwareType: { + description: 'only the globaltype', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + unknownType: { + description: 'forbidden login and find wigwags message', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + pageBeyondTotal: { + description: 'forbidden login and find visualization message', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + unknownSearchField: { + description: 'forbidden login and find wigwags message', + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + noType: { + description: `forbidded can't find any types`, + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + }, + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/get.ts b/x-pack/test/saved_object_api_integration/security_only/apis/get.ts new file mode 100644 index 0000000000000..db3399f8c6d47 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_only/apis/get.ts @@ -0,0 +1,301 @@ +/* + * 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 { TestInvoker } from '../../common/lib/types'; +import { getTestSuiteFactory } from '../../common/suites/get'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { + createExpectDoesntExistNotFound, + createExpectLegacyForbidden, + createExpectSpaceAwareResults, + createExpectNotSpaceAwareResults, + getTest, + } = getTestSuiteFactory(esArchiver, supertest); + + describe('get', () => { + getTest(`not a kibana user`, { + auth: { + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + doesntExist: { + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + }, + }); + + getTest(`superuser`, { + auth: { + username: AUTHENTICATION.SUPERUSER.USERNAME, + password: AUTHENTICATION.SUPERUSER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(), + }, + notSpaceAware: { + statusCode: 200, + response: createExpectNotSpaceAwareResults(), + }, + doesntExist: { + statusCode: 404, + response: createExpectDoesntExistNotFound(), + }, + }, + }); + + getTest(`kibana legacy user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(), + }, + notSpaceAware: { + statusCode: 200, + response: createExpectNotSpaceAwareResults(), + }, + doesntExist: { + statusCode: 404, + response: createExpectDoesntExistNotFound(), + }, + }, + }); + + getTest(`kibana legacy dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(), + }, + notSpaceAware: { + statusCode: 200, + response: createExpectNotSpaceAwareResults(), + }, + doesntExist: { + statusCode: 404, + response: createExpectDoesntExistNotFound(), + }, + }, + }); + + getTest(`kibana dual-privileges user`, { + auth: { + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME, + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(), + }, + notSpaceAware: { + statusCode: 200, + response: createExpectNotSpaceAwareResults(), + }, + doesntExist: { + statusCode: 404, + response: createExpectDoesntExistNotFound(), + }, + }, + }); + + getTest(`kibana dual-privileges dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(), + }, + notSpaceAware: { + statusCode: 200, + response: createExpectNotSpaceAwareResults(), + }, + doesntExist: { + statusCode: 404, + response: createExpectDoesntExistNotFound(), + }, + }, + }); + + getTest(`kibana rbac user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(), + }, + notSpaceAware: { + statusCode: 200, + response: createExpectNotSpaceAwareResults(), + }, + doesntExist: { + statusCode: 404, + response: createExpectDoesntExistNotFound(), + }, + }, + }); + + getTest(`kibana rbac dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(), + }, + notSpaceAware: { + statusCode: 200, + response: createExpectNotSpaceAwareResults(), + }, + doesntExist: { + statusCode: 404, + response: createExpectDoesntExistNotFound(), + }, + }, + }); + + getTest(`kibana rbac default space all user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + doesntExist: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + }, + }); + + getTest(`kibana rbac default space read user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + doesntExist: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + }, + }); + + getTest(`kibana rbac space 1 all user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + doesntExist: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + }, + }); + + getTest(`kibana rbac space 1 readonly user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + doesntExist: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + }, + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/index.ts b/x-pack/test/saved_object_api_integration/security_only/apis/index.ts new file mode 100644 index 0000000000000..c9be7152f96ea --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_only/apis/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createUsersAndRoles } from '../../common/lib/create_users_and_roles'; +import { TestInvoker } from '../../common/lib/types'; + +// tslint:disable:no-default-export +export default function({ getService, loadTestFile }: TestInvoker) { + const es = getService('es'); + const supertest = getService('supertest'); + + describe('saved objects security only enabled', () => { + before(async () => { + await createUsersAndRoles(es, supertest); + }); + + loadTestFile(require.resolve('./bulk_create')); + loadTestFile(require.resolve('./bulk_get')); + loadTestFile(require.resolve('./create')); + loadTestFile(require.resolve('./delete')); + loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./update')); + }); +} diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/update.ts b/x-pack/test/saved_object_api_integration/security_only/apis/update.ts new file mode 100644 index 0000000000000..ab967d6611a00 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_only/apis/update.ts @@ -0,0 +1,310 @@ +/* + * 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 { TestInvoker } from '../../common/lib/types'; +import { updateTestSuiteFactory } from '../../common/suites/update'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + describe('update', () => { + const { + createExpectLegacyForbidden, + expectDoesntExistRbacForbidden, + expectNotSpaceAwareResults, + expectNotSpaceAwareRbacForbidden, + expectNotFound, + expectSpaceAwareRbacForbidden, + expectSpaceAwareResults, + updateTest, + } = updateTestSuiteFactory(esArchiver, supertest); + + updateTest(`not a kibana user`, { + auth: { + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + doesntExist: { + statusCode: 403, + response: createExpectLegacyForbidden(AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME), + }, + }, + }); + + updateTest(`superuser`, { + auth: { + username: AUTHENTICATION.SUPERUSER.USERNAME, + password: AUTHENTICATION.SUPERUSER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: expectSpaceAwareResults, + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + }, + }); + + updateTest(`kibana legacy user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: expectSpaceAwareResults, + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + }, + }); + + updateTest(`kibana legacy dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME + ), + }, + doesntExist: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME + ), + }, + }, + }); + + updateTest(`kibana dual-privileges user`, { + auth: { + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME, + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: expectSpaceAwareResults, + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + }, + }); + + updateTest(`kibana dual-privileges dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: expectSpaceAwareRbacForbidden, + }, + notSpaceAware: { + statusCode: 403, + response: expectNotSpaceAwareRbacForbidden, + }, + doesntExist: { + statusCode: 403, + response: expectDoesntExistRbacForbidden, + }, + }, + }); + + updateTest(`kibana rbac user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 200, + response: expectSpaceAwareResults, + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + }, + }); + + updateTest(`kibana rbac dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: expectSpaceAwareRbacForbidden, + }, + notSpaceAware: { + statusCode: 403, + response: expectNotSpaceAwareRbacForbidden, + }, + doesntExist: { + statusCode: 403, + response: expectDoesntExistRbacForbidden, + }, + }, + }); + + updateTest(AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + doesntExist: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME + ), + }, + }, + }); + + updateTest(AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + doesntExist: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME + ), + }, + }, + }); + + updateTest(AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + doesntExist: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME + ), + }, + }, + }); + + updateTest(AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.PASSWORD, + }, + tests: { + spaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + notSpaceAware: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + doesntExist: { + statusCode: 403, + response: createExpectLegacyForbidden( + AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME + ), + }, + }, + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/security_only/config.ts b/x-pack/test/saved_object_api_integration/security_only/config.ts new file mode 100644 index 0000000000000..f71cc9207b3cc --- /dev/null +++ b/x-pack/test/saved_object_api_integration/security_only/config.ts @@ -0,0 +1,10 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// tslint:disable:no-default-export +export default createTestConfig('security_only', { disabledPlugins: ['spaces'], license: 'trial' }); diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts new file mode 100644 index 0000000000000..6f48c53a3f897 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts @@ -0,0 +1,44 @@ +/* + * 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 { bulkCreateTestSuiteFactory } from '../../common/suites/bulk_create'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + const { bulkCreateTest, createExpectResults } = bulkCreateTestSuiteFactory( + es, + esArchiver, + supertest + ); + + describe('_bulk_create', () => { + bulkCreateTest('in the current space (space_1)', { + ...SPACES.SPACE_1, + tests: { + default: { + statusCode: 200, + response: createExpectResults(SPACES.SPACE_1.spaceId), + }, + }, + }); + + bulkCreateTest('in the default space', { + ...SPACES.DEFAULT, + tests: { + default: { + statusCode: 200, + response: createExpectResults(SPACES.DEFAULT.spaceId), + }, + }, + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts new file mode 100644 index 0000000000000..cfb4b301bccfb --- /dev/null +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts @@ -0,0 +1,43 @@ +/* + * 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 { bulkGetTestSuiteFactory } from '../../common/suites/bulk_get'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const { bulkGetTest, createExpectResults, createExpectNotFoundResults } = bulkGetTestSuiteFactory( + esArchiver, + supertest + ); + + describe('_bulk_get', () => { + bulkGetTest(`objects within the current space (space_1)`, { + ...SPACES.SPACE_1, + tests: { + default: { + statusCode: 200, + response: createExpectResults(SPACES.SPACE_1.spaceId), + }, + }, + }); + + bulkGetTest(`objects within another space`, { + ...SPACES.SPACE_1, + otherSpaceId: SPACES.SPACE_2.spaceId, + tests: { + default: { + statusCode: 200, + response: createExpectNotFoundResults(SPACES.SPACE_2.spaceId), + }, + }, + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts new file mode 100644 index 0000000000000..4da115377f23f --- /dev/null +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SPACES } from '../../common/lib/spaces'; +import { TestInvoker } from '../../common/lib/types'; +import { createTestSuiteFactory } from '../../common/suites/create'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + const { + createTest, + createExpectSpaceAwareResults, + expectNotSpaceAwareResults, + } = createTestSuiteFactory(es, esArchiver, supertestWithoutAuth); + + describe('create', () => { + createTest('in the current space (space_1)', { + ...SPACES.SPACE_1, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(SPACES.SPACE_1.spaceId), + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + }, + }); + + createTest('in the default space', { + ...SPACES.DEFAULT, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(SPACES.DEFAULT.spaceId), + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + }, + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/delete.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/delete.ts new file mode 100644 index 0000000000000..d70f930d63677 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/delete.ts @@ -0,0 +1,58 @@ +/* + * 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 { deleteTestSuiteFactory } from '../../common/suites/delete'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('delete', () => { + const { createExpectUnknownDocNotFound, deleteTest, expectEmpty } = deleteTestSuiteFactory( + esArchiver, + supertest + ); + + deleteTest(`in the default space`, { + ...SPACES.DEFAULT, + tests: { + spaceAware: { + statusCode: 200, + response: expectEmpty, + }, + notSpaceAware: { + statusCode: 200, + response: expectEmpty, + }, + invalidId: { + statusCode: 404, + response: createExpectUnknownDocNotFound(SPACES.DEFAULT.spaceId), + }, + }, + }); + + deleteTest(`in the current space (space_1)`, { + ...SPACES.SPACE_1, + tests: { + spaceAware: { + statusCode: 200, + response: expectEmpty, + }, + notSpaceAware: { + statusCode: 200, + response: expectEmpty, + }, + invalidId: { + statusCode: 404, + response: createExpectUnknownDocNotFound(SPACES.SPACE_1.spaceId), + }, + }, + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts new file mode 100644 index 0000000000000..421b752801bfd --- /dev/null +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts @@ -0,0 +1,97 @@ +/* + * 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 { findTestSuiteFactory } from '../../common/suites/find'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const { + createExpectEmpty, + createExpectResults, + createExpectVisualizationResults, + expectNotSpaceAwareResults, + findTest, + } = findTestSuiteFactory(esArchiver, supertest); + + describe('find', () => { + findTest(`objects only within the current space (space_1)`, { + ...SPACES.SPACE_1, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: createExpectVisualizationResults(SPACES.SPACE_1.spaceId), + }, + notSpaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + unknownType: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: createExpectResults(SPACES.SPACE_1.spaceId), + }, + }, + }); + + findTest(`objects only within the current space (default)`, { + ...SPACES.DEFAULT, + tests: { + spaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: createExpectVisualizationResults(SPACES.DEFAULT.spaceId), + }, + notSpaceAwareType: { + description: 'only the visualization', + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + unknownType: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: createExpectResults(SPACES.DEFAULT.spaceId), + }, + }, + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/get.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/get.ts new file mode 100644 index 0000000000000..c2055bd41dd61 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/get.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 { SPACES } from '../../common/lib/spaces'; +import { TestInvoker } from '../../common/lib/types'; +import { getTestSuiteFactory } from '../../common/suites/get'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const { + createExpectDoesntExistNotFound, + createExpectSpaceAwareResults, + createExpectNotSpaceAwareResults, + getTest, + } = getTestSuiteFactory(esArchiver, supertest); + + describe('get', () => { + getTest(`can access objects belonging to the current space (default)`, { + ...SPACES.DEFAULT, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(SPACES.DEFAULT.spaceId), + }, + notSpaceAware: { + statusCode: 200, + response: createExpectNotSpaceAwareResults(SPACES.DEFAULT.spaceId), + }, + doesntExist: { + statusCode: 404, + response: createExpectDoesntExistNotFound(SPACES.DEFAULT.spaceId), + }, + }, + }); + + getTest(`can access objects belonging to the current space (space_1)`, { + ...SPACES.SPACE_1, + tests: { + spaceAware: { + statusCode: 200, + response: createExpectSpaceAwareResults(SPACES.SPACE_1.spaceId), + }, + notSpaceAware: { + statusCode: 200, + response: createExpectNotSpaceAwareResults(SPACES.SPACE_1.spaceId), + }, + doesntExist: { + statusCode: 404, + response: createExpectDoesntExistNotFound(SPACES.SPACE_1.spaceId), + }, + }, + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/index.js b/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts similarity index 67% rename from x-pack/test/spaces_api_integration/apis/saved_objects/index.js rename to x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts index c74b03792ba03..113cf86454d5f 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/index.js +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { TestInvoker } from '../../common/lib/types'; -export default function ({ loadTestFile }) { - - describe('saved_objects', () => { +// tslint:disable:no-default-export +export default function({ loadTestFile }: TestInvoker) { + describe('saved objects spaces only enabled', () => { + loadTestFile(require.resolve('./bulk_create')); loadTestFile(require.resolve('./bulk_get')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/update.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/update.ts new file mode 100644 index 0000000000000..c22ef7fe17e13 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/update.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 { SPACES } from '../../common/lib/spaces'; +import { TestInvoker } from '../../common/lib/types'; +import { updateTestSuiteFactory } from '../../common/suites/update'; + +// tslint:disable:no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('update', () => { + const { + expectSpaceAwareResults, + expectNotFound, + expectNotSpaceAwareResults, + updateTest, + } = updateTestSuiteFactory(esArchiver, supertest); + + updateTest(`in the default space`, { + ...SPACES.DEFAULT, + tests: { + spaceAware: { + statusCode: 200, + response: expectSpaceAwareResults, + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + }, + }); + + updateTest('in the current space (space_1)', { + ...SPACES.SPACE_1, + tests: { + spaceAware: { + statusCode: 200, + response: expectSpaceAwareResults, + }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + }, + }); + }); +} diff --git a/x-pack/test/saved_object_api_integration/spaces_only/config.ts b/x-pack/test/saved_object_api_integration/spaces_only/config.ts new file mode 100644 index 0000000000000..38d65bab1b107 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/spaces_only/config.ts @@ -0,0 +1,10 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// tslint:disable:no-default-export +export default createTestConfig('spaces_only', { license: 'basic' }); diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js b/x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js deleted file mode 100644 index 517aab85312fa..0000000000000 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from 'expect.js'; -import { SPACES } from './lib/spaces'; -import { getIdPrefix, getUrlPrefix } from './lib/space_test_utils'; - -export default function ({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - const BULK_REQUESTS = [ - { - type: 'visualization', - id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - }, - { - type: 'dashboard', - id: 'does not exist', - }, - { - type: 'config', - id: '7.0.0-alpha1', - }, - ]; - - const createBulkRequests = (spaceId) => BULK_REQUESTS.map(r => ({ - ...r, - id: `${getIdPrefix(spaceId)}${r.id}` - })); - - describe('_bulk_get', () => { - const expectNotFoundResults = (spaceId) => resp => { - expect(resp.body).to.eql({ - saved_objects: [ - { - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - type: 'visualization', - error: { - statusCode: 404, - message: 'Not found', - }, - }, - { - id: `${getIdPrefix(spaceId)}does not exist`, - type: 'dashboard', - error: { - statusCode: 404, - message: 'Not found', - }, - }, - //todo(legrego) fix when config is space aware - { - id: `${getIdPrefix(spaceId)}7.0.0-alpha1`, - type: 'config', - error: { - statusCode: 404, - message: 'Not found', - }, - }, - ], - }); - }; - - const expectResults = (spaceId) => resp => { - expect(resp.body).to.eql({ - saved_objects: [ - { - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - type: 'visualization', - updated_at: '2017-09-21T18:51:23.794Z', - version: resp.body.saved_objects[0].version, - attributes: { - title: 'Count of requests', - description: '', - version: 1, - // cheat for some of the more complex attributes - visState: resp.body.saved_objects[0].attributes.visState, - uiStateJSON: resp.body.saved_objects[0].attributes.uiStateJSON, - kibanaSavedObjectMeta: - resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta, - }, - }, - { - id: `${getIdPrefix(spaceId)}does not exist`, - type: 'dashboard', - error: { - statusCode: 404, - message: 'Not found', - }, - }, - //todo(legrego) fix when config is space aware - { - id: `${getIdPrefix(spaceId)}7.0.0-alpha1`, - type: 'config', - error: { - statusCode: 404, - message: 'Not found', - }, - }, - ], - }); - }; - - const bulkGetTest = (description, { spaceId, tests, otherSpaceId = spaceId }) => { - describe(description, () => { - before(() => esArchiver.load('saved_objects/spaces')); - after(() => esArchiver.unload('saved_objects/spaces')); - - it(`should return ${tests.default.statusCode}`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_get`) - .send(createBulkRequests(otherSpaceId)) - .expect(tests.default.statusCode) - .then(tests.default.response); - }); - }); - }; - - bulkGetTest(`objects within the current space (space_1)`, { - ...SPACES.SPACE_1, - tests: { - default: { - statusCode: 200, - response: expectResults(SPACES.SPACE_1.spaceId), - }, - } - }); - - bulkGetTest(`objects within another space`, { - ...SPACES.SPACE_1, - otherSpaceId: SPACES.SPACE_2.spaceId, - tests: { - default: { - statusCode: 200, - response: expectNotFoundResults(SPACES.SPACE_2.spaceId) - }, - } - }); - - }); -} diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/create.js b/x-pack/test/spaces_api_integration/apis/saved_objects/create.js deleted file mode 100644 index 4a7bfa30d40dc..0000000000000 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/create.js +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from 'expect.js'; -import { getUrlPrefix } from './lib/space_test_utils'; -import { SPACES } from './lib/spaces'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; - -export default function ({ getService }) { - const supertest = getService('supertest'); - const es = getService('es'); - const esArchiver = getService('esArchiver'); - - describe('create', () => { - const expectSpaceAwareResults = (spaceId) => async (resp) => { - expect(resp.body).to.have.property('id').match(/^[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/); - - expect(resp.body).to.eql({ - id: resp.body.id, - type: 'visualization', - updated_at: resp.body.updated_at, - version: 1, - attributes: { - title: 'My favorite vis' - } - }); - - const expectedSpacePrefix = spaceId === DEFAULT_SPACE_ID ? '' : `${spaceId}:`; - - // query ES directory to assert on space id - const { _source } = await es.get({ - id: `${expectedSpacePrefix}visualization:${resp.body.id}`, - type: 'doc', - index: '.kibana' - }); - - const { - namespace: actualSpaceId = '**not defined**' - } = _source; - - if (spaceId === DEFAULT_SPACE_ID) { - expect(actualSpaceId).to.eql('**not defined**'); - } else { - expect(actualSpaceId).to.eql(spaceId); - } - }; - - const expectNotSpaceAwareResults = () => async (resp) => { - expect(resp.body).to.have.property('id').match(/^[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/); - - expect(resp.body).to.eql({ - id: resp.body.id, - type: 'space', - updated_at: resp.body.updated_at, - version: 1, - attributes: { - name: 'My favorite space', - } - }); - - // query ES directory to assert on space id - const { _source } = await es.get({ - id: `space:${resp.body.id}`, - type: 'doc', - index: '.kibana' - }); - - const { - namespace: actualSpaceId = '**not defined**' - } = _source; - - expect(actualSpaceId).to.eql('**not defined**'); - }; - - const createTest = (description, { spaceId, tests }) => { - describe(description, () => { - before(() => esArchiver.load('saved_objects/spaces')); - after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.spaceAware.statusCode} for a space-aware type`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/visualization`) - .send({ - attributes: { - title: 'My favorite vis' - } - }) - .expect(tests.spaceAware.statusCode) - .then(tests.spaceAware.response); - }); - - it(`should return ${tests.notSpaceAware.statusCode} for a non space-aware type`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/space`) - .send({ - attributes: { - name: 'My favorite space', - } - }) - .expect(tests.notSpaceAware.statusCode) - .then(tests.notSpaceAware.response); - }); - - }); - }; - - createTest('in the current space (space_1)', { - ...SPACES.SPACE_1, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults(SPACES.SPACE_1.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults(SPACES.SPACE_1.spaceId), - } - } - }); - - createTest('in the default space', { - ...SPACES.DEFAULT, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults(SPACES.DEFAULT.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults(SPACES.SPACE_1.spaceId), - } - } - }); - }); -} diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js b/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js deleted file mode 100644 index 07bb72a30e982..0000000000000 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from 'expect.js'; -import { SPACES } from './lib/spaces'; -import { getUrlPrefix, getIdPrefix } from './lib/space_test_utils'; - -export default function ({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('delete', () => { - - const expectEmpty = () => (resp) => { - expect(resp.body).to.eql({}); - }; - - const expectNotFound = (type, id) => (resp) => { - expect(resp.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: `Saved object [${type}/${id}] not found` - }); - }; - - const deleteTest = (description, { spaceId, tests }) => { - describe(description, () => { - before(() => esArchiver.load('saved_objects/spaces')); - after(() => esArchiver.unload('saved_objects/spaces')); - - it(`should return ${tests.spaceAware.statusCode} when deleting a space-aware doc`, async () => ( - await supertest - .delete(`${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/${getIdPrefix(spaceId)}be3733a0-9efe-11e7-acb3-3dab96693fab`) - .expect(tests.spaceAware.statusCode) - .then(tests.spaceAware.response()) - )); - - it(`should return ${tests.notSpaceAware.statusCode} when deleting a non-space-aware doc`, async () => ( - await supertest - .delete(`${getUrlPrefix(spaceId)}/api/saved_objects/space/space_2`) - .expect(tests.notSpaceAware.statusCode) - .then(tests.notSpaceAware.response()) - )); - - it(`should return ${tests.inOtherSpace.statusCode} when deleting a doc belonging to another space`, async () => { - const expectedObjectId = `${getIdPrefix('space_2')}be3733a0-9efe-11e7-acb3-3dab96693fab`; - - await supertest - .delete(`${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/${expectedObjectId}`) - .expect(tests.inOtherSpace.statusCode) - .then(tests.inOtherSpace.response('dashboard', expectedObjectId)); - }); - }); - }; - - deleteTest(`in the default space`, { - ...SPACES.DEFAULT, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty - }, - inOtherSpace: { - statusCode: 404, - response: expectNotFound - } - } - }); - - deleteTest(`in the current space (space_1)`, { - ...SPACES.SPACE_1, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty - }, - inOtherSpace: { - statusCode: 404, - response: expectNotFound - } - } - }); - }); -} diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/find.js b/x-pack/test/spaces_api_integration/apis/saved_objects/find.js deleted file mode 100644 index ea59e01eff96d..0000000000000 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/find.js +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from 'expect.js'; -import { SPACES } from './lib/spaces'; -import { getIdPrefix, getUrlPrefix } from './lib/space_test_utils'; - -export default function ({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('find', () => { - - const expectVisualizationResults = (spaceId) => (resp) => { - expect(resp.body).to.eql({ - page: 1, - per_page: 20, - total: 1, - saved_objects: [ - { - type: 'visualization', - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - // no space id on the saved object because the field is not requested as part of a find operation - version: 1, - attributes: { - 'title': 'Count of requests' - } - } - ] - }); - }; - - const expectAllResults = (spaceId) => (resp) => { - // TODO(legrego): update once config is space-aware - - const sortById = ({ id: id1 }, { id: id2 }) => id1 < id2 ? -1 : 1; - - resp.body.saved_objects.sort(sortById); - - const expectedSavedObjects = [{ - id: '7.0.0-alpha1', - type: 'config', - updated_at: '2017-09-21T18:49:16.302Z', - version: 1, - }, { - id: `default`, - type: 'space', - updated_at: '2017-09-21T18:49:16.270Z', - version: 1, - }, - { - id: `space_1`, - type: 'space', - updated_at: '2017-09-21T18:49:16.270Z', - version: 1, - }, - { - id: `space_2`, - type: 'space', - updated_at: '2017-09-21T18:49:16.270Z', - version: 1, - }, - { - id: `${getIdPrefix(spaceId)}91200a00-9efd-11e7-acb3-3dab96693fab`, - type: 'index-pattern', - updated_at: '2017-09-21T18:49:16.270Z', - version: 1, - }, - - { - id: `${getIdPrefix(spaceId)}be3733a0-9efe-11e7-acb3-3dab96693fab`, - type: 'dashboard', - updated_at: '2017-09-21T18:57:40.826Z', - version: 1, - }, - { - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - type: 'visualization', - updated_at: '2017-09-21T18:51:23.794Z', - version: 1, - }] - .sort(sortById); - - expectedSavedObjects.forEach((object, index) => { - if (resp.body.saved_objects[index]) { - object.attributes = resp.body.saved_objects[index].attributes; - } - }); - - - expect(resp.body).to.eql({ - page: 1, - per_page: 20, - total: expectedSavedObjects.length, - saved_objects: expectedSavedObjects, - }); - }; - - const createExpectEmpty = (page, perPage, total) => (resp) => { - expect(resp.body).to.eql({ - page: page, - per_page: perPage, - total: total, - saved_objects: [] - }); - }; - - const findTest = (description, { spaceId, tests }) => { - describe(description, () => { - before(() => esArchiver.load('saved_objects/spaces')); - after(() => esArchiver.unload('saved_objects/spaces')); - - it(`should return ${tests.normal.statusCode} with ${tests.normal.description}`, async () => ( - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=visualization&fields=title`) - .expect(tests.normal.statusCode) - .then(tests.normal.response) - )); - - describe('page beyond total', () => { - it(`should return ${tests.pageBeyondTotal.statusCode} with ${tests.pageBeyondTotal.description}`, async () => ( - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=visualization&page=100&per_page=100`) - .expect(tests.pageBeyondTotal.statusCode) - .then(tests.pageBeyondTotal.response) - )); - }); - - describe('unknown search field', () => { - it(`should return ${tests.unknownSearchField.statusCode} with ${tests.unknownSearchField.description}`, async () => ( - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=wigwags&search_fields=a`) - .expect(tests.unknownSearchField.statusCode) - .then(tests.unknownSearchField.response) - )); - }); - - describe('no type', () => { - it(`should return ${tests.noType.statusCode} with ${tests.noType.description}`, async () => ( - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find`) - .expect(tests.noType.statusCode) - .then(tests.noType.response) - )); - }); - }); - }; - - findTest(`objects only within the current space (space_1)`, { - ...SPACES.SPACE_1, - tests: { - normal: { - description: 'only the visualization', - statusCode: 200, - response: expectVisualizationResults(SPACES.SPACE_1.spaceId), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'all objects', - statusCode: 200, - response: expectAllResults(SPACES.SPACE_1.spaceId), - }, - } - }); - - findTest(`objects only within the current space (default)`, { - ...SPACES.DEFAULT, - tests: { - normal: { - description: 'only the visualization', - statusCode: 200, - response: expectVisualizationResults(SPACES.DEFAULT.spaceId), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'all objects', - statusCode: 200, - response: expectAllResults(SPACES.DEFAULT.spaceId), - }, - } - }); - }); -} diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/get.js b/x-pack/test/spaces_api_integration/apis/saved_objects/get.js deleted file mode 100644 index 0369bffba26e5..0000000000000 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/get.js +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from 'expect.js'; -import { getIdPrefix, getUrlPrefix } from './lib/space_test_utils'; -import { SPACES } from './lib/spaces'; - -export default function ({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('get', () => { - - const expectResults = (spaceId) => () => (resp) => { - const expectedBody = { - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - type: 'visualization', - updated_at: '2017-09-21T18:51:23.794Z', - version: resp.body.version, - - attributes: { - title: 'Count of requests', - description: '', - version: 1, - // cheat for some of the more complex attributes - visState: resp.body.attributes.visState, - uiStateJSON: resp.body.attributes.uiStateJSON, - kibanaSavedObjectMeta: resp.body.attributes.kibanaSavedObjectMeta - } - }; - - expect(resp.body).to.eql(expectedBody); - }; - - const expectNotFound = (type, id) => (resp) => { - expect(resp.body).to.eql({ - error: 'Not Found', - message: `Saved object [${type}/${id}] not found`, - statusCode: 404, - }); - }; - - const getTest = (description, { spaceId, tests, otherSpaceId = spaceId }) => { - describe(description, () => { - before(async () => esArchiver.load(`saved_objects/spaces`)); - after(async () => esArchiver.unload(`saved_objects/spaces`)); - - it(`should return ${tests.exists.statusCode}`, async () => { - const objectId = `${getIdPrefix(otherSpaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`; - - return supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${objectId}`) - .expect(tests.exists.statusCode) - .then(tests.exists.response('visualization', objectId)); - }); - }); - }; - - getTest(`can access objects belonging to the current space (space_1)`, { - ...SPACES.SPACE_1, - tests: { - exists: { - statusCode: 200, - response: expectResults(SPACES.SPACE_1.spaceId), - }, - } - }); - - getTest(`cannot access objects belonging to a different space (space_1)`, { - ...SPACES.SPACE_1, - otherSpaceId: SPACES.SPACE_2.spaceId, - tests: { - exists: { - statusCode: 404, - response: expectNotFound - }, - } - }); - - getTest(`can access objects belonging to the current space (default)`, { - ...SPACES.DEFAULT, - tests: { - exists: { - statusCode: 200, - response: expectResults(SPACES.DEFAULT.spaceId), - }, - } - }); - - getTest(`cannot access objects belonging to a different space (default)`, { - ...SPACES.DEFAULT, - otherSpaceId: SPACES.SPACE_1.spaceId, - tests: { - exists: { - statusCode: 404, - response: expectNotFound - }, - } - }); - }); -} diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/authentication.js b/x-pack/test/spaces_api_integration/apis/saved_objects/lib/authentication.js deleted file mode 100644 index 8b140fd3b2a30..0000000000000 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/authentication.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const AUTHENTICATION = { - NOT_A_KIBANA_USER: { - USERNAME: 'not_a_kibana_user', - PASSWORD: 'password' - }, - SUPERUSER: { - USERNAME: 'elastic', - PASSWORD: 'changeme' - }, - KIBANA_LEGACY_USER: { - USERNAME: 'a_kibana_legacy_user', - PASSWORD: 'password' - }, - KIBANA_LEGACY_DASHBOARD_ONLY_USER: { - USERNAME: 'a_kibana_legacy_dashboard_only_user', - PASSWORD: 'password' - }, - KIBANA_RBAC_USER: { - USERNAME: 'a_kibana_rbac_user', - PASSWORD: 'password' - }, - KIBANA_RBAC_DASHBOARD_ONLY_USER: { - USERNAME: 'a_kibana_rbac_dashboard_only_user', - PASSWORD: 'password' - } -}; diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/update.js b/x-pack/test/spaces_api_integration/apis/saved_objects/update.js deleted file mode 100644 index 4bc938ba7a499..0000000000000 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/update.js +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from 'expect.js'; -import { SPACES } from './lib/spaces'; -import { getUrlPrefix, getIdPrefix } from './lib/space_test_utils'; - -export default function ({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('update', () => { - const expectSpaceAwareResults = () => resp => { - - // loose ISO8601 UTC time with milliseconds validation - expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/); - - expect(resp.body).to.eql({ - id: resp.body.id, - type: 'visualization', - updated_at: resp.body.updated_at, - version: 2, - attributes: { - title: 'My second favorite vis' - } - }); - }; - - const expectNonSpaceAwareResults = () => resp => { - - // loose ISO8601 UTC time with milliseconds validation - expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/); - - expect(resp.body).to.eql({ - id: resp.body.id, - type: 'space', - updated_at: resp.body.updated_at, - version: 2, - attributes: { - name: 'My second favorite space' - } - }); - }; - - const expectNotFound = (type, id) => resp => { - expect(resp.body).eql({ - statusCode: 404, - error: 'Not Found', - message: `Saved object [${type}/${id}] not found` - }); - }; - - const updateTest = (description, { spaceId, tests }) => { - describe(description, () => { - before(() => esArchiver.load('saved_objects/spaces')); - after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.spaceAware.statusCode} for a space-aware doc`, async () => { - await supertest - .put(`${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`) - .send({ - attributes: { - title: 'My second favorite vis' - } - }) - .expect(tests.spaceAware.statusCode) - .then(tests.spaceAware.response()); - }); - - it(`should return ${tests.notSpaceAware.statusCode} for a non space-aware doc`, async () => { - await supertest - .put(`${getUrlPrefix(spaceId)}/api/saved_objects/space/space_1`) - .send({ - attributes: { - name: 'My second favorite space' - } - }) - .expect(tests.notSpaceAware.statusCode) - .then(tests.notSpaceAware.response()); - }); - - it(`should return ${tests.inOtherSpace.statusCode} for a doc in another space`, async () => { - const id = `${getIdPrefix('space_2')}dd7caf20-9efd-11e7-acb3-3dab96693fab`; - await supertest - .put(`${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${id}`) - .send({ - attributes: { - title: 'My second favorite vis' - } - }) - .expect(tests.inOtherSpace.statusCode) - .then(tests.inOtherSpace.response(`visualization`, `${id}`)); - }); - - describe('unknown id', () => { - it(`should return ${tests.doesntExist.statusCode}`, async () => { - await supertest - .put(`${getUrlPrefix(spaceId)}/api/saved_objects/visualization/not an id`) - .send({ - attributes: { - title: 'My second favorite vis' - } - }) - .expect(tests.doesntExist.statusCode) - .then(tests.doesntExist.response(`visualization`, `not an id`)); - }); - }); - }); - }; - - updateTest(`in the default space`, { - ...SPACES.DEFAULT, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNonSpaceAwareResults, - }, - inOtherSpace: { - statusCode: 404, - response: expectNotFound, - }, - doesntExist: { - statusCode: 404, - response: expectNotFound, - }, - } - }); - - updateTest('in the current space (space_1)', { - ...SPACES.SPACE_1, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNonSpaceAwareResults, - }, - inOtherSpace: { - statusCode: 404, - response: expectNotFound, - }, - doesntExist: { - statusCode: 404, - response: expectNotFound, - }, - } - }); - - }); -} diff --git a/x-pack/test/spaces_api_integration/common/config.ts b/x-pack/test/spaces_api_integration/common/config.ts new file mode 100644 index 0000000000000..ed0e45809d1b1 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/config.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-ignore +import { resolveKibanaPath } from '@kbn/plugin-helpers'; +import path from 'path'; +import { TestInvoker } from './lib/types'; +// @ts-ignore +import { EsProvider } from './services/es'; + +interface CreateTestConfigOptions { + license: string; + disabledPlugins?: string[]; +} + +export function createTestConfig(name: string, options: CreateTestConfigOptions) { + const { license, disabledPlugins = [] } = options; + + return async ({ readConfigFile }: TestInvoker) => { + const config = { + kibana: { + api: await readConfigFile(resolveKibanaPath('test/api_integration/config.js')), + functional: await readConfigFile(require.resolve('../../../../test/functional/config.js')), + }, + xpack: { + api: await readConfigFile(require.resolve('../../api_integration/config.js')), + }, + }; + + return { + testFiles: [require.resolve(`../${name}/apis/`)], + servers: config.xpack.api.get('servers'), + services: { + es: EsProvider, + esSupertestWithoutAuth: config.xpack.api.get('services.esSupertestWithoutAuth'), + supertest: config.kibana.api.get('services.supertest'), + supertestWithoutAuth: config.xpack.api.get('services.supertestWithoutAuth'), + esArchiver: config.kibana.functional.get('services.esArchiver'), + kibanaServer: config.kibana.functional.get('services.kibanaServer'), + }, + junit: { + reportName: 'X-Pack Spaces API Integration Tests -- ' + name, + }, + + esArchiver: { + directory: path.join(__dirname, 'fixtures', 'es_archiver'), + }, + + esTestCluster: { + ...config.xpack.api.get('esTestCluster'), + license, + serverArgs: [ + `xpack.license.self_generated.type=${license}`, + `xpack.security.enabled=${!disabledPlugins.includes('security') && license === 'trial'}`, + ], + }, + + kbnTestServer: { + ...config.xpack.api.get('kbnTestServer'), + serverArgs: [ + ...config.xpack.api.get('kbnTestServer.serverArgs'), + '--optimize.enabled=false', + '--server.xsrf.disableProtection=true', + `--plugin-path=${path.join(__dirname, 'fixtures', 'namespace_agnostic_type_plugin')}`, + ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), + ], + }, + }; + }; +} diff --git a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json new file mode 100644 index 0000000000000..383f7083ff070 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space:default", + "source": { + "type": "space", + "updated_at": "2017-09-21T18:49:16.270Z", + "space": { + "name": "Default Space", + "description": "This is the default space", + "_reserved": true + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space:space_1", + "source": { + "type": "space", + "updated_at": "2017-09-21T18:49:16.270Z", + "space": { + "name": "Space 1", + "description": "This is the first test space" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space:space_2", + "source": { + "type": "space", + "updated_at": "2017-09-21T18:49:16.270Z", + "space": { + "name": "Space 2", + "description": "This is the second test space" + } + } + } +} diff --git a/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json similarity index 100% rename from x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/mappings.json rename to x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json diff --git a/x-pack/test/spaces_api_integration/common/lib/authentication.ts b/x-pack/test/spaces_api_integration/common/lib/authentication.ts new file mode 100644 index 0000000000000..20aa0b5e955e5 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/lib/authentication.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const AUTHENTICATION = { + NOT_A_KIBANA_USER: { + USERNAME: 'not_a_kibana_user', + PASSWORD: 'password', + }, + SUPERUSER: { + USERNAME: 'elastic', + PASSWORD: 'changeme', + }, + KIBANA_LEGACY_USER: { + USERNAME: 'a_kibana_legacy_user', + PASSWORD: 'password', + }, + KIBANA_LEGACY_DASHBOARD_ONLY_USER: { + USERNAME: 'a_kibana_legacy_dashboard_only_user', + PASSWORD: 'password', + }, + KIBANA_DUAL_PRIVILEGES_USER: { + USERNAME: 'a_kibana_dual_privileges_user', + PASSWORD: 'password', + }, + KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER: { + USERNAME: 'a_kibana_dual_privileges_dashboard_only_user', + PASSWORD: 'password', + }, + KIBANA_RBAC_USER: { + USERNAME: 'a_kibana_rbac_user', + PASSWORD: 'password', + }, + KIBANA_RBAC_DASHBOARD_ONLY_USER: { + USERNAME: 'a_kibana_rbac_dashboard_only_user', + PASSWORD: 'password', + }, + KIBANA_RBAC_DEFAULT_SPACE_ALL_USER: { + USERNAME: 'a_kibana_rbac_default_space_all_user', + PASSWORD: 'password', + }, + KIBANA_RBAC_DEFAULT_SPACE_READ_USER: { + USERNAME: 'a_kibana_rbac_default_space_read_user', + PASSWORD: 'password', + }, + KIBANA_RBAC_SPACE_1_ALL_USER: { + USERNAME: 'a_kibana_rbac_space_1_all_user', + PASSWORD: 'password', + }, + KIBANA_RBAC_SPACE_1_READ_USER: { + USERNAME: 'a_kibana_rbac_space_1_read_user', + PASSWORD: 'password', + }, + KIBANA_RBAC_SPACE_2_ALL_USER: { + USERNAME: 'a_kibana_rbac_space_2_all_user', + PASSWORD: 'password', + }, + KIBANA_RBAC_SPACE_2_READ_USER: { + USERNAME: 'a_kibana_rbac_space_2_read_user', + PASSWORD: 'password', + }, + KIBANA_RBAC_SPACE_1_2_ALL_USER: { + USERNAME: 'a_kibana_rbac_space_1_2_all_user', + PASSWORD: 'password', + }, + KIBANA_RBAC_SPACE_1_2_READ_USER: { + USERNAME: 'a_kibana_rbac_space_1_2_read_user', + PASSWORD: 'password', + }, +}; diff --git a/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts b/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts new file mode 100644 index 0000000000000..d7e0f55e27619 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts @@ -0,0 +1,287 @@ +/* + * 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 { SuperTest } from 'supertest'; +import { AUTHENTICATION } from './authentication'; + +export const createUsersAndRoles = async (es: any, supertest: SuperTest) => { + await supertest.put('/api/security/role/kibana_legacy_user').send({ + elasticsearch: { + indices: [ + { + names: ['.kibana'], + privileges: ['manage', 'read', 'index', 'delete'], + }, + ], + }, + }); + + await supertest.put('/api/security/role/kibana_legacy_dashboard_only_user').send({ + elasticsearch: { + indices: [ + { + names: ['.kibana'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + }); + + await supertest.put('/api/security/role/kibana_dual_privileges_user').send({ + elasticsearch: { + indices: [ + { + names: ['.kibana'], + privileges: ['manage', 'read', 'index', 'delete'], + }, + ], + }, + kibana: { + global: ['all'], + }, + }); + + await supertest.put('/api/security/role/kibana_dual_privileges_dashboard_only_user').send({ + elasticsearch: { + indices: [ + { + names: ['.kibana'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: { + global: ['read'], + }, + }); + + await supertest.put('/api/security/role/kibana_rbac_user').send({ + kibana: { + global: ['all'], + }, + }); + + await supertest.put('/api/security/role/kibana_rbac_dashboard_only_user').send({ + kibana: { + global: ['read'], + }, + }); + + await supertest.put('/api/security/role/kibana_rbac_default_space_all_user').send({ + kibana: { + space: { + default: ['all'], + }, + }, + }); + + await supertest.put('/api/security/role/kibana_rbac_default_space_read_user').send({ + kibana: { + space: { + default: ['read'], + }, + }, + }); + + await supertest.put('/api/security/role/kibana_rbac_space_1_all_user').send({ + kibana: { + space: { + space_1: ['all'], + }, + }, + }); + + await supertest.put('/api/security/role/kibana_rbac_space_1_read_user').send({ + kibana: { + space: { + space_1: ['read'], + }, + }, + }); + + await supertest.put('/api/security/role/kibana_rbac_space_2_all_user').send({ + kibana: { + space: { + space_2: ['all'], + }, + }, + }); + + await supertest.put('/api/security/role/kibana_rbac_space_2_read_user').send({ + kibana: { + space: { + space_2: ['read'], + }, + }, + }); + + await supertest.put('/api/security/role/kibana_rbac_space_1_2_all_user').send({ + kibana: { + space: { + space_1: ['all'], + space_2: ['all'], + }, + }, + }); + + await supertest.put('/api/security/role/kibana_rbac_space_1_2_read_user').send({ + kibana: { + space: { + space_1: ['read'], + space_2: ['read'], + }, + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + body: { + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + roles: [], + full_name: 'not a kibana user', + email: 'not_a_kibana_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, + roles: ['kibana_legacy_user'], + full_name: 'a kibana legacy user', + email: 'a_kibana_legacy_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, + roles: ['kibana_legacy_dashboard_only_user'], + full_name: 'a kibana legacy dashboard only user', + email: 'a_kibana_legacy_dashboard_only_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD, + roles: ['kibana_dual_privileges_user'], + full_name: 'a kibana dual_privileges user', + email: 'a_kibana_dual_privileges_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD, + roles: ['kibana_dual_privileges_dashboard_only_user'], + full_name: 'a kibana dual_privileges dashboard only user', + email: 'a_kibana_dual_privileges_dashboard_only_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + roles: ['kibana_rbac_user'], + full_name: 'a kibana user', + email: 'a_kibana_rbac_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + roles: ['kibana_rbac_dashboard_only_user'], + full_name: 'a kibana dashboard only user', + email: 'a_kibana_rbac_dashboard_only_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER.PASSWORD, + roles: ['kibana_rbac_default_space_all_user'], + full_name: 'a kibana default space all user', + email: 'a_kibana_rbac_default_space_all_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER.PASSWORD, + roles: ['kibana_rbac_default_space_read_user'], + full_name: 'a kibana default space read-only user', + email: 'a_kibana_rbac_default_space_read_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER.PASSWORD, + roles: ['kibana_rbac_space_1_all_user'], + full_name: 'a kibana rbac space 1 all user', + email: 'a_kibana_rbac_space_1_all_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER.PASSWORD, + roles: ['kibana_rbac_space_1_read_user'], + full_name: 'a kibana rbac space 1 read-only user', + email: 'a_kibana_rbac_space_1_readonly_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_SPACE_2_ALL_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_RBAC_SPACE_2_ALL_USER.PASSWORD, + roles: ['kibana_rbac_space_2_all_user'], + full_name: 'a kibana rbac space 2 all user', + email: 'a_kibana_rbac_space_2_all_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_SPACE_2_READ_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_RBAC_SPACE_2_READ_USER.PASSWORD, + roles: ['kibana_rbac_space_2_read_user'], + full_name: 'a kibana rbac space 2 read-only user', + email: 'a_kibana_rbac_space_2_readonly_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_2_ALL_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_2_ALL_USER.PASSWORD, + roles: ['kibana_rbac_space_1_2_all_user'], + full_name: 'a kibana rbac space 1 and 2 all user', + email: 'a_kibana_rbac_space_1_2_all_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_2_READ_USER.USERNAME, + body: { + password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_2_READ_USER.PASSWORD, + roles: ['kibana_rbac_space_1_2_read_user'], + full_name: 'a kibana rbac space 1 and 2 read-only user', + email: 'a_kibana_rbac_space_1_2_readonly_user@elastic.co', + }, + }); +}; diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js b/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts similarity index 68% rename from x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js rename to x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts index 5d7f40f9c54b1..f233bc1d11d7c 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js +++ b/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_SPACE_ID } from '../../../../../plugins/spaces/common/constants'; +import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -export function getUrlPrefix(spaceId) { +export function getUrlPrefix(spaceId?: string) { return spaceId && spaceId !== DEFAULT_SPACE_ID ? `/s/${spaceId}` : ``; } -export function getIdPrefix(spaceId) { +export function getIdPrefix(spaceId?: string) { return spaceId === DEFAULT_SPACE_ID ? '' : `${spaceId}-`; } diff --git a/x-pack/plugins/security/common/model/kibana_application_privilege.ts b/x-pack/test/spaces_api_integration/common/lib/spaces.ts similarity index 61% rename from x-pack/plugins/security/common/model/kibana_application_privilege.ts rename to x-pack/test/spaces_api_integration/common/lib/spaces.ts index 54350ec2abcef..a9c552d4ccd78 100644 --- a/x-pack/plugins/security/common/model/kibana_application_privilege.ts +++ b/x-pack/test/spaces_api_integration/common/lib/spaces.ts @@ -4,8 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaPrivilege } from './kibana_privilege'; - -export interface KibanaApplicationPrivilege { - name: KibanaPrivilege; -} +export const SPACES = { + SPACE_1: { + spaceId: 'space_1', + }, + SPACE_2: { + spaceId: 'space_2', + }, + DEFAULT: { + spaceId: 'default', + }, +}; diff --git a/x-pack/test/spaces_api_integration/common/lib/types.ts b/x-pack/test/spaces_api_integration/common/lib/types.ts new file mode 100644 index 0000000000000..f149ad02cc1f7 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/lib/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type DescribeFn = (text: string, fn: () => void) => void; + +export interface TestDefinitionAuthentication { + username?: string; + password?: string; +} +export type LoadTestFileFn = (path: string) => string; + +export type GetServiceFn = (service: string) => any; + +export type ReadConfigFileFn = (path: string) => any; + +export interface TestInvoker { + getService: GetServiceFn; + loadTestFile: LoadTestFileFn; + readConfigFile: ReadConfigFileFn; +} diff --git a/x-pack/test/rbac_api_integration/services/es.js b/x-pack/test/spaces_api_integration/common/services/es.js similarity index 89% rename from x-pack/test/rbac_api_integration/services/es.js rename to x-pack/test/spaces_api_integration/common/services/es.js index 420541fa7ec5f..c4fa7c504e12c 100644 --- a/x-pack/test/rbac_api_integration/services/es.js +++ b/x-pack/test/spaces_api_integration/common/services/es.js @@ -7,7 +7,7 @@ import { format as formatUrl } from 'url'; import elasticsearch from 'elasticsearch'; -import shieldPlugin from '../../../server/lib/esjs_shield_plugin'; +import shieldPlugin from '../../../../server/lib/esjs_shield_plugin'; export function EsProvider({ getService }) { const config = getService('config'); diff --git a/x-pack/test/spaces_api_integration/common/suites/create.ts b/x-pack/test/spaces_api_integration/common/suites/create.ts new file mode 100644 index 0000000000000..54f20c93d0352 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/suites/create.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from 'expect.js'; +import { SuperTest } from 'supertest'; +import { getUrlPrefix } from '../lib/space_test_utils'; +import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; + +interface CreateTest { + statusCode: number; + response: (resp: any) => void; +} + +interface CreateTests { + newSpace: CreateTest; + alreadyExists: CreateTest; + reservedSpecified: CreateTest; +} + +interface CreateTestDefinition { + auth?: TestDefinitionAuthentication; + spaceId: string; + tests: CreateTests; +} + +export function createTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const makeCreateTest = (describeFn: DescribeFn) => ( + description: string, + { auth = {}, spaceId, tests }: CreateTestDefinition + ) => { + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + it(`should return ${tests.newSpace.statusCode}`, async () => { + return supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/space`) + .auth(auth.username, auth.password) + .send({ + name: 'marketing', + id: 'marketing', + description: 'a description', + color: '#5c5959', + }) + .expect(tests.newSpace.statusCode) + .then(tests.newSpace.response); + }); + + describe('when it already exists', () => { + it(`should return ${tests.alreadyExists.statusCode}`, async () => { + return supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/space`) + .auth(auth.username, auth.password) + .send({ + name: 'space_1', + id: 'space_1', + color: '#ffffff', + description: 'a description', + }) + .expect(tests.alreadyExists.statusCode) + .then(tests.alreadyExists.response); + }); + }); + + describe('when _reserved is specified', () => { + it(`should return ${tests.reservedSpecified.statusCode} and ignore _reserved`, async () => { + return supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/space`) + .auth(auth.username, auth.password) + .send({ + name: 'reserved space', + id: 'reserved', + description: 'a description', + color: '#5c5959', + _reserved: true, + }) + .expect(tests.reservedSpecified.statusCode) + .then(tests.reservedSpecified.response); + }); + }); + }); + }; + + const createTest = makeCreateTest(describe); + // @ts-ignore + createTest.only = makeCreateTest(describe.only); + + const expectConflictResponse = (resp: any) => { + expect(resp.body).to.only.have.keys(['error', 'message', 'statusCode']); + expect(resp.body.error).to.equal('Conflict'); + expect(resp.body.statusCode).to.equal(409); + expect(resp.body.message).to.match(new RegExp(`A space with the identifier .*`)); + }; + + const expectRbacForbiddenResponse = (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: 'Unauthorized to create spaces', + }); + }; + + const createExpectLegacyForbiddenResponse = (username: string) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `action [indices:data/write/index] is unauthorized for user [${username}]: [security_exception] action [indices:data/write/index] is unauthorized for user [${username}]`, + }); + }; + + const expectNewSpaceResult = (resp: any) => { + expect(resp.body).to.eql({ + name: 'marketing', + id: 'marketing', + description: 'a description', + color: '#5c5959', + }); + }; + + const expectReservedSpecifiedResult = (resp: any) => { + expect(resp.body).to.eql({ + name: 'reserved space', + id: 'reserved', + description: 'a description', + color: '#5c5959', + }); + }; + + return { + createTest, + expectNewSpaceResult, + expectReservedSpecifiedResult, + expectConflictResponse, + expectRbacForbiddenResponse, + createExpectLegacyForbiddenResponse, + }; +} diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.ts b/x-pack/test/spaces_api_integration/common/suites/delete.ts new file mode 100644 index 0000000000000..d35fe5aa8470a --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from 'expect.js'; +import { SuperTest } from 'supertest'; +import { getUrlPrefix } from '../lib/space_test_utils'; +import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; + +interface DeleteTest { + statusCode: number; + response: (resp: any) => void; +} + +interface DeleteTests { + exists: DeleteTest; + reservedSpace: DeleteTest; + doesntExist: DeleteTest; +} + +interface DeleteTestDefinition { + auth?: TestDefinitionAuthentication; + spaceId: string; + tests: DeleteTests; +} + +export function deleteTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const makeDeleteTest = (describeFn: DescribeFn) => ( + description: string, + { auth = {}, spaceId, tests }: DeleteTestDefinition + ) => { + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + it(`should return ${tests.exists.statusCode}`, async () => { + return supertest + .delete(`${getUrlPrefix(spaceId)}/api/spaces/space/space_2`) + .auth(auth.username, auth.password) + .expect(tests.exists.statusCode) + .then(tests.exists.response); + }); + + describe(`when the space is reserved`, async () => { + it(`should return ${tests.reservedSpace.statusCode}`, async () => { + return supertest + .delete(`${getUrlPrefix(spaceId)}/api/spaces/space/default`) + .auth(auth.username, auth.password) + .expect(tests.reservedSpace.statusCode) + .then(tests.reservedSpace.response); + }); + }); + + describe(`when the space doesn't exist`, () => { + it(`should return ${tests.doesntExist.statusCode}`, async () => { + return supertest + .delete(`${getUrlPrefix(spaceId)}/api/spaces/space/space_3`) + .auth(auth.username, auth.password) + .expect(tests.doesntExist.statusCode) + .then(tests.doesntExist.response); + }); + }); + }); + }; + + const deleteTest = makeDeleteTest(describe); + // @ts-ignore + deleteTest.only = makeDeleteTest(describe.only); + + const createExpectResult = (expectedResult: any) => (resp: any) => { + expect(resp.body).to.eql(expectedResult); + }; + + const expectEmptyResult = (resp: any) => { + expect(resp.body).to.eql(''); + }; + + const expectNotFoundResult = (resp: any) => { + expect(resp.body).to.eql({ + error: 'Not Found', + statusCode: 404, + }); + }; + + const expectReservedSpaceResult = (resp: any) => { + expect(resp.body).to.eql({ + error: 'Bad Request', + statusCode: 400, + message: `This Space cannot be deleted because it is reserved.`, + }); + }; + + const expectRbacForbidden = (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: 'Unauthorized to delete spaces', + }); + }; + + const createExpectLegacyForbidden = (username: string, action: string) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `action [indices:data/${action}] is unauthorized for user [${username}]: [security_exception] action [indices:data/${action}] is unauthorized for user [${username}]`, + }); + }; + + return { + deleteTest, + createExpectLegacyForbidden, + createExpectResult, + expectRbacForbidden, + expectEmptyResult, + expectNotFoundResult, + expectReservedSpaceResult, + }; +} diff --git a/x-pack/test/spaces_api_integration/common/suites/get.ts b/x-pack/test/spaces_api_integration/common/suites/get.ts new file mode 100644 index 0000000000000..fcb0d90d1de20 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/suites/get.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from 'expect.js'; +import { SuperAgent } from 'superagent'; +import { getUrlPrefix } from '../lib/space_test_utils'; +import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; + +interface GetTest { + statusCode: number; + response: (resp: any) => void; +} + +interface GetTests { + default: GetTest; +} + +interface GetTestDefinition { + auth?: TestDefinitionAuthentication; + currentSpaceId: string; + spaceId: string; + tests: GetTests; +} + +export function getTestSuiteFactory(esArchiver: any, supertest: SuperAgent) { + const nonExistantSpaceId = 'not-a-space'; + + const makeGetTest = (describeFn: DescribeFn) => ( + description: string, + { auth = {}, currentSpaceId, spaceId, tests }: GetTestDefinition + ) => { + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + it(`should return ${tests.default.statusCode}`, async () => { + return supertest + .get(`${getUrlPrefix(currentSpaceId)}/api/spaces/space/${spaceId}`) + .auth(auth.username, auth.password) + .expect(tests.default.statusCode) + .then(tests.default.response); + }); + }); + }; + + const getTest = makeGetTest(describe); + // @ts-ignore + getTest.only = makeGetTest(describe); + + const createExpectResults = (spaceId: string) => (resp: any) => { + const allSpaces = [ + { + id: 'default', + name: 'Default Space', + description: 'This is the default space', + _reserved: true, + }, + { + id: 'space_1', + name: 'Space 1', + description: 'This is the first test space', + }, + { + id: 'space_2', + name: 'Space 2', + description: 'This is the second test space', + }, + ]; + expect(resp.body).to.eql(allSpaces.find(space => space.id === spaceId)); + }; + + const createExpectEmptyResult = () => (resp: any) => { + expect(resp.body).to.eql(''); + }; + + const createExpectNotFoundResult = () => (resp: any) => { + expect(resp.body).to.eql({ + error: 'Not Found', + statusCode: 404, + }); + }; + + const createExpectRbacForbidden = (spaceId: string) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `Unauthorized to get ${spaceId} space`, + }); + }; + + const createExpectLegacyForbidden = (username: string) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `action [indices:data/read/get] is unauthorized for user [${username}]: [security_exception] action [indices:data/read/get] is unauthorized for user [${username}]`, + }); + }; + + return { + getTest, + nonExistantSpaceId, + createExpectResults, + createExpectRbacForbidden, + createExpectEmptyResult, + createExpectNotFoundResult, + createExpectLegacyForbidden, + }; +} diff --git a/x-pack/test/spaces_api_integration/common/suites/get_all.ts b/x-pack/test/spaces_api_integration/common/suites/get_all.ts new file mode 100644 index 0000000000000..9437414cc8b5c --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/suites/get_all.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from 'expect.js'; +import { SuperTest } from 'supertest'; +import { getUrlPrefix } from '../lib/space_test_utils'; +import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; + +interface GetAllTest { + statusCode: number; + response: (resp: any) => void; +} + +interface GetAllTests { + exists: GetAllTest; +} + +interface GetAllTestDefinition { + auth?: TestDefinitionAuthentication; + spaceId: string; + tests: GetAllTests; +} + +export function getAllTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const makeGetAllTest = (describeFn: DescribeFn) => ( + description: string, + { auth = {}, spaceId, tests }: GetAllTestDefinition + ) => { + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + it(`should return ${tests.exists.statusCode}`, async () => { + return supertest + .get(`${getUrlPrefix(spaceId)}/api/spaces/space`) + .auth(auth.username, auth.password) + .expect(tests.exists.statusCode) + .then(tests.exists.response); + }); + }); + }; + + const getAllTest = makeGetAllTest(describe); + // @ts-ignore + getAllTest.only = makeGetAllTest(describe.only); + + const createExpectResults = (...spaceIds: string[]) => (resp: any) => { + const expectedBody = [ + { + id: 'default', + name: 'Default Space', + description: 'This is the default space', + _reserved: true, + }, + { + id: 'space_1', + name: 'Space 1', + description: 'This is the first test space', + }, + { + id: 'space_2', + name: 'Space 2', + description: 'This is the second test space', + }, + ].filter(entry => spaceIds.includes(entry.id)); + expect(resp.body).to.eql(expectedBody); + }; + + const expectEmptyResult = (resp: any) => { + expect(resp.body).to.eql(''); + }; + + const createExpectLegacyForbidden = (username: string) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `action [indices:data/read/search] is unauthorized for user [${username}]: [security_exception] action [indices:data/read/search] is unauthorized for user [${username}]`, + }); + }; + + return { + getAllTest, + createExpectResults, + createExpectLegacyForbidden, + expectEmptyResult, + }; +} diff --git a/x-pack/test/spaces_api_integration/common/suites/select.ts b/x-pack/test/spaces_api_integration/common/suites/select.ts new file mode 100644 index 0000000000000..b2f86a77f7865 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/suites/select.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from 'expect.js'; +import { SuperTest } from 'supertest'; +import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; +import { getUrlPrefix } from '../lib/space_test_utils'; +import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; + +interface SelectTest { + statusCode: number; + response: (resp: any) => void; +} + +interface SelectTests { + default: SelectTest; +} + +interface SelectTestDefinition { + auth?: TestDefinitionAuthentication; + currentSpaceId: string; + spaceId: string; + tests: SelectTests; +} + +export function selectTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const nonExistantSpaceId = 'not-a-space'; + + const makeSelectTest = (describeFn: DescribeFn) => ( + description: string, + { auth = {}, currentSpaceId, spaceId, 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/${spaceId}/select`) + .auth(auth.username, auth.password) + .expect(tests.default.statusCode) + .then(tests.default.response); + }); + }); + }; + + const selectTest = makeSelectTest(describe); + // @ts-ignore + selectTest.only = makeSelectTest(describe.only); + + const createExpectResults = (spaceId: string) => (resp: any) => { + const allSpaces = [ + { + id: 'default', + name: 'Default Space', + description: 'This is the default space', + _reserved: true, + }, + { + id: 'space_1', + name: 'Space 1', + description: 'This is the first test space', + }, + { + id: 'space_2', + name: 'Space 2', + description: 'This is the second test space', + }, + ]; + expect(resp.body).to.eql(allSpaces.find(space => space.id === spaceId)); + }; + + const createExpectEmptyResult = () => (resp: any) => { + expect(resp.body).to.eql(''); + }; + + const createExpectNotFoundResult = () => (resp: any) => { + expect(resp.body).to.eql({ + error: 'Not Found', + statusCode: 404, + }); + }; + const createExpectRbacForbidden = (spaceId: any) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `Unauthorized to get ${spaceId} space`, + }); + }; + + const expectDefaultSpaceResponse = (resp: any) => { + expect(resp.body).to.eql({ + location: `/app/kibana`, + }); + }; + + const createExpectSpaceResponse = (spaceId: string) => (resp: any) => { + if (spaceId === DEFAULT_SPACE_ID) { + expectDefaultSpaceResponse(resp); + } else { + expect(resp.body).to.eql({ + location: `/s/${spaceId}/app/kibana`, + }); + } + }; + + const createExpectLegacyForbidden = (username: string) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `action [indices:data/read/get] is unauthorized for user [${username}]: [security_exception] action [indices:data/read/get] is unauthorized for user [${username}]`, + }); + }; + + return { + selectTest, + nonExistantSpaceId, + expectDefaultSpaceResponse, + createExpectSpaceResponse, + createExpectResults, + createExpectRbacForbidden, + createExpectEmptyResult, + createExpectNotFoundResult, + createExpectLegacyForbidden, + }; +} diff --git a/x-pack/test/spaces_api_integration/common/suites/update.ts b/x-pack/test/spaces_api_integration/common/suites/update.ts new file mode 100644 index 0000000000000..d18f5b1da9412 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/suites/update.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from 'expect.js'; +import { SuperTest } from 'supertest'; +import { getUrlPrefix } from '../lib/space_test_utils'; +import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; + +interface UpdateTest { + statusCode: number; + response: (resp: any) => void; +} + +interface UpdateTests { + alreadyExists: UpdateTest; + defaultSpace: UpdateTest; + newSpace: UpdateTest; +} + +interface UpdateTestDefinition { + auth?: TestDefinitionAuthentication; + spaceId: string; + tests: UpdateTests; +} + +export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const makeUpdateTest = (describeFn: DescribeFn) => ( + description: string, + { auth = {}, spaceId, tests }: UpdateTestDefinition + ) => { + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + it(`should return ${tests.alreadyExists.statusCode}`, async () => { + return supertest + .put(`${getUrlPrefix(spaceId)}/api/spaces/space/space_1`) + .auth(auth.username, auth.password) + .send({ + name: 'space 1', + id: 'space_1', + description: 'a description', + color: '#5c5959', + _reserved: true, + }) + .expect(tests.alreadyExists.statusCode) + .then(tests.alreadyExists.response); + }); + + describe(`default space`, () => { + it(`should return ${tests.defaultSpace.statusCode}`, async () => { + return supertest + .put(`${getUrlPrefix(spaceId)}/api/spaces/space/default`) + .auth(auth.username, auth.password) + .send({ + name: 'the new default', + id: 'default', + description: 'a description', + color: '#ffffff', + _reserved: false, + }) + .expect(tests.defaultSpace.statusCode) + .then(tests.defaultSpace.response); + }); + }); + + describe(`when space doesn't exist`, () => { + it(`should return ${tests.newSpace.statusCode}`, async () => { + return supertest + .put(`${getUrlPrefix(spaceId)}/api/spaces/space/marketing`) + .auth(auth.username, auth.password) + .send({ + name: 'marketing', + id: 'marketing', + description: 'a description', + color: '#5c5959', + }) + .expect(tests.newSpace.statusCode) + .then(tests.newSpace.response); + }); + }); + }); + }; + + const updateTest = makeUpdateTest(describe); + // @ts-ignore + updateTest.only = makeUpdateTest(describe.only); + + const createExpectNotFoundResult = (spaceId: string) => (resp: any) => { + expect(resp.body).to.eql({ + error: 'Not Found', + statusCode: 404, + }); + }; + + const expectRbacForbidden = (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: 'Unauthorized to update spaces', + }); + }; + + const createExpectLegacyForbidden = (username: string) => (resp: any) => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `action [indices:data/write/update] is unauthorized for user [${username}]: [security_exception] action [indices:data/write/update] is unauthorized for user [${username}]`, + }); + }; + + const expectNewSpaceNotFound = createExpectNotFoundResult('marketing'); + + const expectDefaultSpaceResult = (resp: any) => { + expect(resp.body).to.eql({ + name: 'the new default', + id: 'default', + description: 'a description', + color: '#ffffff', + _reserved: true, + }); + }; + + const expectAlreadyExistsResult = (resp: any) => { + expect(resp.body).to.eql({ + name: 'space 1', + id: 'space_1', + description: 'a description', + color: '#5c5959', + }); + }; + + return { + updateTest, + expectNewSpaceNotFound, + expectRbacForbidden, + createExpectLegacyForbidden, + expectAlreadyExistsResult, + expectDefaultSpaceResult, + }; +} diff --git a/x-pack/test/spaces_api_integration/config.js b/x-pack/test/spaces_api_integration/config.js deleted file mode 100644 index d93381cbd76db..0000000000000 --- a/x-pack/test/spaces_api_integration/config.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import path from 'path'; -import { resolveKibanaPath } from '@kbn/plugin-helpers'; - -export default async function ({ readConfigFile }) { - - const config = { - kibana: { - api: await readConfigFile(resolveKibanaPath('test/api_integration/config.js')), - common: await readConfigFile(require.resolve('../../../test/common/config.js')), - functional: await readConfigFile(require.resolve('../../../test/functional/config.js')) - }, - xpack: { - api: await readConfigFile(require.resolve('../api_integration/config.js')), - functional: await readConfigFile(require.resolve('../functional/config.js')) - } - }; - - return { - testFiles: [require.resolve('./apis')], - servers: config.xpack.api.get('servers'), - services: { - es: config.kibana.common.get('services.es'), - supertest: config.kibana.api.get('services.supertest'), - supertestWithoutAuth: config.xpack.api.get('services.supertestWithoutAuth'), - esArchiver: config.kibana.functional.get('services.esArchiver'), - }, - junit: { - reportName: 'X-Pack Spaces API Integration Tests', - }, - - esArchiver: { - directory: path.join(__dirname, 'fixtures', 'es_archiver') - }, - - esTestCluster: { - ...config.xpack.api.get('esTestCluster'), - serverArgs: [ - ...config.xpack.api.get('esTestCluster.serverArgs'), - ], - }, - - kbnTestServer: { - ...config.xpack.api.get('kbnTestServer'), - serverArgs: [ - ...config.xpack.api.get('kbnTestServer.serverArgs'), - '--optimize.enabled=false', - '--server.xsrf.disableProtection=true', - ], - }, - }; -} diff --git a/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz b/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz deleted file mode 100644 index a795be4f88ea3..0000000000000 Binary files a/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz and /dev/null differ diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/create.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/create.ts new file mode 100644 index 0000000000000..760fd1d6cd9b4 --- /dev/null +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/create.ts @@ -0,0 +1,252 @@ +/* + * 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 { createTestSuiteFactory } from '../../common/suites/create'; + +// tslint:disable:no-default-export +export default function createSpacesOnlySuite({ getService }: TestInvoker) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { + createTest, + expectNewSpaceResult, + expectReservedSpecifiedResult, + expectConflictResponse, + expectRbacForbiddenResponse, + createExpectLegacyForbiddenResponse, + } = createTestSuiteFactory(esArchiver, supertestWithoutAuth); + + describe('create', () => { + [ + { + spaceId: SPACES.DEFAULT.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + }, + ].forEach(scenario => { + createTest(`${scenario.notAKibanaUser.USERNAME} within the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.notAKibanaUser.USERNAME, + password: scenario.notAKibanaUser.PASSWORD, + }, + tests: { + newSpace: { + statusCode: 403, + response: createExpectLegacyForbiddenResponse(scenario.notAKibanaUser.USERNAME), + }, + alreadyExists: { + statusCode: 403, + response: createExpectLegacyForbiddenResponse(scenario.notAKibanaUser.USERNAME), + }, + reservedSpecified: { + statusCode: 403, + response: createExpectLegacyForbiddenResponse(scenario.notAKibanaUser.USERNAME), + }, + }, + }); + + createTest(`${scenario.superuser.USERNAME} within the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.superuser.USERNAME, + password: scenario.superuser.PASSWORD, + }, + tests: { + newSpace: { + statusCode: 200, + response: expectNewSpaceResult, + }, + alreadyExists: { + statusCode: 409, + response: expectConflictResponse, + }, + reservedSpecified: { + statusCode: 200, + response: expectReservedSpecifiedResult, + }, + }, + }); + + createTest(`${scenario.userWithAllGlobally.USERNAME} within the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithAllGlobally.USERNAME, + password: scenario.userWithAllGlobally.PASSWORD, + }, + tests: { + newSpace: { + statusCode: 200, + response: expectNewSpaceResult, + }, + alreadyExists: { + statusCode: 409, + response: expectConflictResponse, + }, + reservedSpecified: { + statusCode: 200, + response: expectReservedSpecifiedResult, + }, + }, + }); + + createTest(`${scenario.userWithDualAll.USERNAME} within the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithDualAll.USERNAME, + password: scenario.userWithDualAll.PASSWORD, + }, + tests: { + newSpace: { + statusCode: 200, + response: expectNewSpaceResult, + }, + alreadyExists: { + statusCode: 409, + response: expectConflictResponse, + }, + reservedSpecified: { + statusCode: 200, + response: expectReservedSpecifiedResult, + }, + }, + }); + + createTest(`${scenario.userWithLegacyAll.USERNAME} within the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithLegacyAll.USERNAME, + password: scenario.userWithLegacyAll.PASSWORD, + }, + tests: { + newSpace: { + statusCode: 200, + response: expectNewSpaceResult, + }, + alreadyExists: { + statusCode: 409, + response: expectConflictResponse, + }, + reservedSpecified: { + statusCode: 200, + response: expectReservedSpecifiedResult, + }, + }, + }); + + createTest(`${scenario.userWithReadGlobally.USERNAME} within the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithReadGlobally.USERNAME, + password: scenario.userWithReadGlobally.PASSWORD, + }, + tests: { + newSpace: { + statusCode: 403, + response: expectRbacForbiddenResponse, + }, + alreadyExists: { + statusCode: 403, + response: expectRbacForbiddenResponse, + }, + reservedSpecified: { + statusCode: 403, + response: expectRbacForbiddenResponse, + }, + }, + }); + + createTest(`${scenario.userWithDualRead.USERNAME} within the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithDualRead.USERNAME, + password: scenario.userWithDualRead.PASSWORD, + }, + tests: { + newSpace: { + statusCode: 403, + response: expectRbacForbiddenResponse, + }, + alreadyExists: { + statusCode: 403, + response: expectRbacForbiddenResponse, + }, + reservedSpecified: { + statusCode: 403, + response: expectRbacForbiddenResponse, + }, + }, + }); + + createTest(`${scenario.userWithLegacyRead.USERNAME} within the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithLegacyRead.USERNAME, + password: scenario.userWithLegacyRead.PASSWORD, + }, + tests: { + newSpace: { + statusCode: 403, + response: createExpectLegacyForbiddenResponse(scenario.userWithLegacyRead.USERNAME), + }, + alreadyExists: { + statusCode: 403, + response: createExpectLegacyForbiddenResponse(scenario.userWithLegacyRead.USERNAME), + }, + reservedSpecified: { + statusCode: 403, + response: createExpectLegacyForbiddenResponse(scenario.userWithLegacyRead.USERNAME), + }, + }, + }); + + createTest(`${scenario.userWithAllAtSpace.USERNAME} within the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithAllAtSpace.USERNAME, + password: scenario.userWithAllAtSpace.PASSWORD, + }, + tests: { + newSpace: { + statusCode: 403, + response: expectRbacForbiddenResponse, + }, + alreadyExists: { + statusCode: 403, + response: expectRbacForbiddenResponse, + }, + reservedSpecified: { + statusCode: 403, + response: expectRbacForbiddenResponse, + }, + }, + }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts new file mode 100644 index 0000000000000..3e43defaa910c --- /dev/null +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts @@ -0,0 +1,255 @@ +/* + * 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 { deleteTestSuiteFactory } from '../../common/suites/delete'; + +// tslint:disable:no-default-export +export default function deleteSpaceTestSuite({ getService }: TestInvoker) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { + deleteTest, + createExpectLegacyForbidden, + expectRbacForbidden, + expectEmptyResult, + expectNotFoundResult, + expectReservedSpaceResult, + } = deleteTestSuiteFactory(esArchiver, supertestWithoutAuth); + + describe('delete', () => { + [ + { + spaceId: SPACES.DEFAULT.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userwithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userwithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + }, + ].forEach(scenario => { + deleteTest(`${scenario.notAKibanaUser.USERNAME} from the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.notAKibanaUser.USERNAME, + password: scenario.notAKibanaUser.PASSWORD, + }, + tests: { + exists: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME, 'read/get'), + }, + reservedSpace: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME, 'read/get'), + }, + doesntExist: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME, 'read/get'), + }, + }, + }); + + deleteTest(`${scenario.superuser.USERNAME} from the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.superuser.USERNAME, + password: scenario.superuser.PASSWORD, + }, + tests: { + exists: { + statusCode: 204, + response: expectEmptyResult, + }, + reservedSpace: { + statusCode: 400, + response: expectReservedSpaceResult, + }, + doesntExist: { + statusCode: 404, + response: expectNotFoundResult, + }, + }, + }); + + deleteTest(`${scenario.userWithAllGlobally.USERNAME} from the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithAllGlobally.USERNAME, + password: scenario.userWithAllGlobally.PASSWORD, + }, + tests: { + exists: { + statusCode: 204, + response: expectEmptyResult, + }, + reservedSpace: { + statusCode: 400, + response: expectReservedSpaceResult, + }, + doesntExist: { + statusCode: 404, + response: expectNotFoundResult, + }, + }, + }); + + deleteTest(`${scenario.userWithDualAll.USERNAME} from the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithDualAll.USERNAME, + password: scenario.userWithDualAll.PASSWORD, + }, + tests: { + exists: { + statusCode: 204, + response: expectEmptyResult, + }, + reservedSpace: { + statusCode: 400, + response: expectReservedSpaceResult, + }, + doesntExist: { + statusCode: 404, + response: expectNotFoundResult, + }, + }, + }); + + deleteTest(`${scenario.userWithLegacyAll.USERNAME} from the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithLegacyAll.USERNAME, + password: scenario.userWithLegacyAll.PASSWORD, + }, + tests: { + exists: { + statusCode: 204, + response: expectEmptyResult, + }, + reservedSpace: { + statusCode: 400, + response: expectReservedSpaceResult, + }, + doesntExist: { + statusCode: 404, + response: expectNotFoundResult, + }, + }, + }); + + deleteTest(`${scenario.userWithReadGlobally.USERNAME} from the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithReadGlobally.USERNAME, + password: scenario.userWithReadGlobally.PASSWORD, + }, + tests: { + exists: { + statusCode: 403, + response: expectRbacForbidden, + }, + reservedSpace: { + statusCode: 403, + response: expectRbacForbidden, + }, + doesntExist: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); + + deleteTest(`${scenario.userwithDualRead.USERNAME} from the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.userwithDualRead.USERNAME, + password: scenario.userwithDualRead.PASSWORD, + }, + tests: { + exists: { + statusCode: 403, + response: expectRbacForbidden, + }, + reservedSpace: { + statusCode: 403, + response: expectRbacForbidden, + }, + doesntExist: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); + + deleteTest(`${scenario.userWithLegacyRead.USERNAME} from the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithLegacyRead.USERNAME, + password: scenario.userWithLegacyRead.PASSWORD, + }, + tests: { + exists: { + statusCode: 403, + response: createExpectLegacyForbidden( + scenario.userWithLegacyRead.USERNAME, + 'write/delete' + ), + }, + reservedSpace: { + statusCode: 400, + response: expectReservedSpaceResult, + }, + doesntExist: { + statusCode: 404, + response: expectNotFoundResult, + }, + }, + }); + + deleteTest(`${scenario.userWithAllAtSpace} from the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithAllAtSpace.USERNAME, + password: scenario.userWithAllAtSpace.PASSWORD, + }, + tests: { + exists: { + statusCode: 403, + response: expectRbacForbidden, + }, + reservedSpace: { + statusCode: 403, + response: expectRbacForbidden, + }, + doesntExist: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get.ts new file mode 100644 index 0000000000000..94aaaaff6d9c9 --- /dev/null +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get.ts @@ -0,0 +1,332 @@ +/* + * 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 { getTestSuiteFactory } from '../../common/suites/get'; + +// tslint:disable:no-default-export +export default function getSpaceTestSuite({ getService }: TestInvoker) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { + getTest, + createExpectResults, + createExpectNotFoundResult, + createExpectRbacForbidden, + createExpectLegacyForbidden, + nonExistantSpaceId, + } = getTestSuiteFactory(esArchiver, supertestWithoutAuth); + + describe('get', () => { + // valid spaces + [ + { + spaceId: SPACES.DEFAULT.spaceId, + otherSpaceId: SPACES.SPACE_1.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, + userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userwithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + otherSpaceId: SPACES.DEFAULT.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, + userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userwithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + }, + ].forEach(scenario => { + getTest(`${scenario.notAKibanaUser.USERNAME}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.spaceId, + auth: { + username: scenario.notAKibanaUser.USERNAME, + password: scenario.notAKibanaUser.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME), + }, + }, + }); + + getTest(`${scenario.superuser.USERNAME}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.spaceId, + auth: { + username: scenario.superuser.USERNAME, + password: scenario.superuser.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + getTest(`${scenario.userWithAllGlobally.USERNAME}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithAllGlobally.USERNAME, + password: scenario.userWithAllGlobally.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + getTest(`${scenario.userWithDualAll.USERNAME}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithDualAll.USERNAME, + password: scenario.userWithDualAll.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + getTest(`${scenario.userWithLegacyAll.USERNAME}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithLegacyAll.USERNAME, + password: scenario.userWithLegacyAll.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + getTest(`${scenario.userWithReadGlobally.USERNAME}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithReadGlobally.USERNAME, + password: scenario.userWithReadGlobally.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + getTest(`${scenario.userwithDualRead.USERNAME}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.spaceId, + auth: { + username: scenario.userwithDualRead.USERNAME, + password: scenario.userwithDualRead.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + getTest(`${scenario.userWithLegacyRead.USERNAME}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithLegacyRead.USERNAME, + password: scenario.userWithLegacyRead.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + getTest(`${scenario.userWithReadAtSpace.USERNAME} at ${scenario.spaceId}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithReadAtSpace.USERNAME, + password: scenario.userWithReadAtSpace.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + + getTest(`${scenario.userWithAllAtOtherSpace.USERNAME} at a different space`, { + currentSpaceId: scenario.otherSpaceId, + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithAllAtOtherSpace.USERNAME, + password: scenario.userWithAllAtOtherSpace.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectRbacForbidden(scenario.spaceId), + }, + }, + }); + }); + + describe('non-existant space', () => { + [ + { + spaceId: SPACES.DEFAULT.spaceId, + otherSpaceId: nonExistantSpaceId, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userwithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + }, + ].forEach(scenario => { + getTest(`${scenario.userWithAllGlobally.USERNAME}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.otherSpaceId, + auth: { + username: scenario.userWithAllGlobally.USERNAME, + password: scenario.userWithAllGlobally.PASSWORD, + }, + tests: { + default: { + statusCode: 404, + response: createExpectNotFoundResult(), + }, + }, + }); + + getTest(`${scenario.userWithDualAll.USERNAME}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.otherSpaceId, + auth: { + username: scenario.userWithDualAll.USERNAME, + password: scenario.userWithDualAll.PASSWORD, + }, + tests: { + default: { + statusCode: 404, + response: createExpectNotFoundResult(), + }, + }, + }); + + getTest(`${scenario.userWithLegacyAll.USERNAME}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.otherSpaceId, + auth: { + username: scenario.userWithLegacyAll.USERNAME, + password: scenario.userWithLegacyAll.PASSWORD, + }, + tests: { + default: { + statusCode: 404, + response: createExpectNotFoundResult(), + }, + }, + }); + + getTest(`${scenario.userWithReadGlobally.USERNAME}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.otherSpaceId, + auth: { + username: scenario.userWithReadGlobally.USERNAME, + password: scenario.userWithReadGlobally.PASSWORD, + }, + tests: { + default: { + statusCode: 404, + response: createExpectNotFoundResult(), + }, + }, + }); + + getTest(`${scenario.userwithDualRead.USERNAME}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.otherSpaceId, + auth: { + username: scenario.userwithDualRead.USERNAME, + password: scenario.userwithDualRead.PASSWORD, + }, + tests: { + default: { + statusCode: 404, + response: createExpectNotFoundResult(), + }, + }, + }); + + getTest(`${scenario.userWithLegacyRead.USERNAME}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.otherSpaceId, + auth: { + username: scenario.userWithLegacyRead.USERNAME, + password: scenario.userWithLegacyRead.PASSWORD, + }, + tests: { + default: { + statusCode: 404, + response: createExpectNotFoundResult(), + }, + }, + }); + + getTest(`${scenario.userWithAllAtSpace.USERNAME}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.otherSpaceId, + auth: { + username: scenario.userWithAllAtSpace.USERNAME, + password: scenario.userWithAllAtSpace.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectRbacForbidden(scenario.otherSpaceId), + }, + }, + }); + }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts new file mode 100644 index 0000000000000..9410103cb25d7 --- /dev/null +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts @@ -0,0 +1,257 @@ +/* + * 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 { getAllTestSuiteFactory } from '../../common/suites/get_all'; + +// tslint:disable:no-default-export +export default function getAllSpacesTestSuite({ getService }: TestInvoker) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { getAllTest, createExpectResults, createExpectLegacyForbidden } = getAllTestSuiteFactory( + esArchiver, + supertestWithoutAuth + ); + + describe('get all', () => { + [ + { + spaceId: SPACES.DEFAULT.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithAllAtSpace_1: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + userWithReadAtSpace_1: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, + userWithAllAtDefault: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + userWithReadAtDefault: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userwithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithAllAtSpace_1: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + userWithReadAtSpace_1: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, + userWithAllAtDefault: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + userWithReadAtDefault: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userwithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + }, + ].forEach(scenario => { + getAllTest( + `${scenario.notAKibanaUser.USERNAME} can't access any spaces from ${scenario.spaceId}`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.notAKibanaUser.USERNAME, + password: scenario.notAKibanaUser.PASSWORD, + }, + tests: { + exists: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME), + }, + }, + } + ); + + getAllTest(`${scenario.superuser.USERNAME} can access all spaces from ${scenario.spaceId}`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.superuser.USERNAME, + password: scenario.superuser.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: createExpectResults('default', 'space_1', 'space_2'), + }, + }, + }); + + getAllTest( + `${scenario.userWithAllGlobally.USERNAME} can access all spaces from ${scenario.spaceId}`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithAllGlobally.USERNAME, + password: scenario.userWithAllGlobally.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: createExpectResults('default', 'space_1', 'space_2'), + }, + }, + } + ); + + getAllTest( + `${scenario.userWithDualAll.USERNAME} can access all spaces from ${scenario.spaceId}`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithDualAll.USERNAME, + password: scenario.userWithDualAll.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: createExpectResults('default', 'space_1', 'space_2'), + }, + }, + } + ); + + getAllTest( + `${scenario.userWithLegacyAll.USERNAME} can access all spaces from ${scenario.spaceId}`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithLegacyAll.USERNAME, + password: scenario.userWithLegacyAll.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: createExpectResults('default', 'space_1', 'space_2'), + }, + }, + } + ); + + getAllTest( + `${scenario.userWithReadGlobally.USERNAME} can access all spaces from ${scenario.spaceId}`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithReadGlobally.USERNAME, + password: scenario.userWithReadGlobally.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: createExpectResults('default', 'space_1', 'space_2'), + }, + }, + } + ); + + getAllTest( + `${scenario.userwithDualRead.USERNAME} can access all spaces from ${scenario.spaceId}`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.userwithDualRead.USERNAME, + password: scenario.userwithDualRead.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: createExpectResults('default', 'space_1', 'space_2'), + }, + }, + } + ); + + getAllTest( + `${scenario.userWithLegacyRead.USERNAME} can access all spaces from ${scenario.spaceId}`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithLegacyRead.USERNAME, + password: scenario.userWithLegacyRead.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: createExpectResults('default', 'space_1', 'space_2'), + }, + }, + } + ); + + getAllTest( + `${scenario.userWithAllAtSpace_1.USERNAME} can access space_1 from ${scenario.spaceId}`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithAllAtSpace_1.USERNAME, + password: scenario.userWithAllAtSpace_1.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: createExpectResults('space_1'), + }, + }, + } + ); + + getAllTest( + `${scenario.userWithReadAtSpace_1.USERNAME} can access space_1 from ${scenario.spaceId}`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithReadAtSpace_1.USERNAME, + password: scenario.userWithReadAtSpace_1.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: createExpectResults('space_1'), + }, + }, + } + ); + + getAllTest( + `${scenario.userWithAllAtDefault.USERNAME} can access default from ${scenario.spaceId}`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithAllAtDefault.USERNAME, + password: scenario.userWithAllAtDefault.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: createExpectResults('default'), + }, + }, + } + ); + + getAllTest( + `${scenario.userWithReadAtDefault.USERNAME} can access default from ${scenario.spaceId}`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithReadAtDefault.USERNAME, + password: scenario.userWithReadAtDefault.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: createExpectResults('default'), + }, + }, + } + ); + }); + }); +} 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 new file mode 100644 index 0000000000000..044670a822ac2 --- /dev/null +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts @@ -0,0 +1,31 @@ +/* + * 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. + */ +/* + * 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 { createUsersAndRoles } from '../../common/lib/create_users_and_roles'; +import { TestInvoker } from '../../common/lib/types'; + +// tslint:disable:no-default-export +export default function({ loadTestFile, getService }: TestInvoker) { + const es = getService('es'); + const supertest = getService('supertest'); + + describe('spaces api with security', () => { + before(async () => { + await createUsersAndRoles(es, supertest); + }); + + loadTestFile(require.resolve('./create')); + 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 new file mode 100644 index 0000000000000..9f323001e5506 --- /dev/null +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/select.ts @@ -0,0 +1,356 @@ +/* + * 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'; + +// tslint:disable:no-default-export +export default function selectSpaceTestSuite({ getService }: TestInvoker) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { + selectTest, + nonExistantSpaceId, + createExpectSpaceResponse, + createExpectRbacForbidden, + createExpectNotFoundResult, + createExpectLegacyForbidden, + } = selectTestSuiteFactory(esArchiver, supertestWithoutAuth); + + describe('select', () => { + // Global authorization tests + [ + { + spaceId: SPACES.DEFAULT.spaceId, + otherSpaceId: SPACES.SPACE_1.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + otherSpaceId: SPACES.DEFAULT.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + }, + ].forEach(scenario => { + selectTest(`${scenario.notAKibanaUser.USERNAME} selects ${scenario.otherSpaceId}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.otherSpaceId, + auth: { + username: scenario.notAKibanaUser.USERNAME, + password: scenario.notAKibanaUser.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME), + }, + }, + }); + + selectTest(`${scenario.superuser.USERNAME} selects ${scenario.otherSpaceId}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.otherSpaceId, + auth: { + username: scenario.superuser.USERNAME, + password: scenario.superuser.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectSpaceResponse(scenario.otherSpaceId), + }, + }, + }); + + selectTest(`${scenario.userWithAllGlobally.USERNAME} selects ${scenario.otherSpaceId}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.otherSpaceId, + auth: { + username: scenario.userWithAllGlobally.USERNAME, + password: scenario.userWithAllGlobally.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectSpaceResponse(scenario.otherSpaceId), + }, + }, + }); + + selectTest(`${scenario.userWithDualAll.USERNAME} selects ${scenario.otherSpaceId}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.otherSpaceId, + auth: { + username: scenario.userWithDualAll.USERNAME, + password: scenario.userWithDualAll.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectSpaceResponse(scenario.otherSpaceId), + }, + }, + }); + + selectTest(`${scenario.userWithLegacyAll.USERNAME} selects ${scenario.otherSpaceId}`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.otherSpaceId, + auth: { + username: scenario.userWithLegacyAll.USERNAME, + password: scenario.userWithLegacyAll.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectSpaceResponse(scenario.otherSpaceId), + }, + }, + }); + + selectTest( + `${scenario.userWithReadGlobally.USERNAME} selects ${scenario.otherSpaceId} from + ${scenario.spaceId}`, + { + currentSpaceId: scenario.spaceId, + spaceId: scenario.otherSpaceId, + auth: { + username: scenario.userWithReadGlobally.USERNAME, + password: scenario.userWithReadGlobally.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectSpaceResponse(scenario.otherSpaceId), + }, + }, + } + ); + + selectTest( + `${scenario.userWithDualRead.USERNAME} selects ${scenario.otherSpaceId} from + ${scenario.spaceId}`, + { + currentSpaceId: scenario.spaceId, + spaceId: scenario.otherSpaceId, + auth: { + username: scenario.userWithDualRead.USERNAME, + password: scenario.userWithDualRead.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectSpaceResponse(scenario.otherSpaceId), + }, + }, + } + ); + + selectTest( + `${scenario.userWithLegacyRead.USERNAME} can select ${scenario.otherSpaceId} + from ${scenario.spaceId}`, + { + currentSpaceId: scenario.spaceId, + spaceId: scenario.otherSpaceId, + auth: { + username: scenario.userWithLegacyRead.USERNAME, + password: scenario.userWithLegacyRead.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectSpaceResponse(scenario.otherSpaceId), + }, + }, + } + ); + }); + + // Same-Space authorization tests + [ + { + spaceId: SPACES.DEFAULT.spaceId, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, + userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, + userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + }, + ].forEach(scenario => { + selectTest( + `${scenario.userWithAllAtSpace.USERNAME} can select ${scenario.spaceId} + from ${scenario.spaceId}`, + { + currentSpaceId: scenario.spaceId, + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithAllAtSpace.USERNAME, + password: scenario.userWithAllAtSpace.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectSpaceResponse(scenario.spaceId), + }, + }, + } + ); + + selectTest( + `${scenario.userWithReadAtSpace.USERNAME} can select ${scenario.spaceId} + from ${scenario.spaceId}`, + { + currentSpaceId: scenario.spaceId, + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithReadAtSpace.USERNAME, + password: scenario.userWithReadAtSpace.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectSpaceResponse(scenario.spaceId), + }, + }, + } + ); + + selectTest( + `${scenario.userWithAllAtOtherSpace.USERNAME} cannot select ${scenario.spaceId} + from ${scenario.spaceId}`, + { + currentSpaceId: scenario.spaceId, + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithAllAtOtherSpace.USERNAME, + password: scenario.userWithAllAtOtherSpace.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectRbacForbidden(scenario.spaceId), + }, + }, + } + ); + }); + + // Cross-Space authorization tests + [ + { + spaceId: SPACES.SPACE_1.spaceId, + otherSpaceId: SPACES.SPACE_2.spaceId, + 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( + `${scenario.userWithAllAtBothSpaces.USERNAME} can select ${scenario.spaceId} + from ${scenario.otherSpaceId}`, + { + currentSpaceId: scenario.otherSpaceId, + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithAllAtBothSpaces.USERNAME, + password: scenario.userWithAllAtBothSpaces.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: createExpectSpaceResponse(scenario.spaceId), + }, + }, + } + ); + + selectTest( + `${scenario.userWithAllAtOtherSpace.USERNAME} cannot select ${scenario.spaceId} + from ${scenario.otherSpaceId}`, + { + currentSpaceId: scenario.otherSpaceId, + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithAllAtOtherSpace.USERNAME, + password: scenario.userWithAllAtOtherSpace.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectRbacForbidden(scenario.spaceId), + }, + }, + } + ); + }); + + describe('non-existant space', () => { + [ + { + spaceId: SPACES.DEFAULT.spaceId, + otherSpaceId: nonExistantSpaceId, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + otherSpaceId: nonExistantSpaceId, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + }, + ].forEach(scenario => { + selectTest(`${scenario.userWithAllGlobally.USERNAME} cannot access non-existant space`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.otherSpaceId, + auth: { + username: scenario.userWithAllGlobally.USERNAME, + password: scenario.userWithAllGlobally.PASSWORD, + }, + tests: { + default: { + statusCode: 404, + response: createExpectNotFoundResult(), + }, + }, + }); + + selectTest(`${scenario.userWithAllAtSpace.USERNAME} cannot access non-existant space`, { + currentSpaceId: scenario.spaceId, + spaceId: scenario.otherSpaceId, + auth: { + username: scenario.userWithAllAtSpace.USERNAME, + password: scenario.userWithAllAtSpace.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectRbacForbidden(scenario.otherSpaceId), + }, + }, + }); + }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/update.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/update.ts new file mode 100644 index 0000000000000..afe5c3b39505a --- /dev/null +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/update.ts @@ -0,0 +1,308 @@ +/* + * 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 { updateTestSuiteFactory } from '../../common/suites/update'; + +// tslint:disable:no-default-export +export default function updateSpaceTestSuite({ getService }: TestInvoker) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { + updateTest, + expectNewSpaceNotFound, + expectAlreadyExistsResult, + expectDefaultSpaceResult, + expectRbacForbidden, + createExpectLegacyForbidden, + } = updateTestSuiteFactory(esArchiver, supertestWithoutAuth); + + describe.only('update', () => { + [ + { + spaceId: SPACES.DEFAULT.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + notAKibanaUser: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + userWithReadGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, + userWithReadAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, + userWithLegacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + userWithLegacyRead: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER, + userWithDualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + userWithDualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + }, + ].forEach(scenario => { + updateTest( + `${scenario.notAKibanaUser.USERNAME} can't update space_1 from + the ${scenario.spaceId} space`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.notAKibanaUser.USERNAME, + password: scenario.notAKibanaUser.PASSWORD, + }, + tests: { + alreadyExists: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME), + }, + defaultSpace: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME), + }, + newSpace: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.notAKibanaUser.USERNAME), + }, + }, + } + ); + + updateTest( + `${scenario.superuser.USERNAME} can update space_1 from + the ${scenario.spaceId} space`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.superuser.USERNAME, + password: scenario.superuser.PASSWORD, + }, + tests: { + alreadyExists: { + statusCode: 200, + response: expectAlreadyExistsResult, + }, + defaultSpace: { + statusCode: 200, + response: expectDefaultSpaceResult, + }, + newSpace: { + statusCode: 404, + response: expectNewSpaceNotFound, + }, + }, + } + ); + + updateTest( + `${scenario.userWithAllGlobally.USERNAME} can update space_1 from + the ${scenario.spaceId} space`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithAllGlobally.USERNAME, + password: scenario.userWithAllGlobally.PASSWORD, + }, + tests: { + alreadyExists: { + statusCode: 200, + response: expectAlreadyExistsResult, + }, + defaultSpace: { + statusCode: 200, + response: expectDefaultSpaceResult, + }, + newSpace: { + statusCode: 404, + response: expectNewSpaceNotFound, + }, + }, + } + ); + + updateTest( + `${scenario.userWithDualAll.USERNAME} can update space_1 from + the ${scenario.spaceId} space`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithDualAll.USERNAME, + password: scenario.userWithDualAll.PASSWORD, + }, + tests: { + alreadyExists: { + statusCode: 200, + response: expectAlreadyExistsResult, + }, + defaultSpace: { + statusCode: 200, + response: expectDefaultSpaceResult, + }, + newSpace: { + statusCode: 404, + response: expectNewSpaceNotFound, + }, + }, + } + ); + + updateTest( + `${scenario.userWithLegacyAll.USERNAME} can update space_1 from + the ${scenario.spaceId} space`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithLegacyAll.USERNAME, + password: scenario.userWithLegacyAll.PASSWORD, + }, + tests: { + alreadyExists: { + statusCode: 200, + response: expectAlreadyExistsResult, + }, + defaultSpace: { + statusCode: 200, + response: expectDefaultSpaceResult, + }, + newSpace: { + statusCode: 404, + response: expectNewSpaceNotFound, + }, + }, + } + ); + + updateTest( + `${scenario.userWithReadGlobally.USERNAME} cannot update space_1 + from the ${scenario.spaceId} space`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithReadGlobally.USERNAME, + password: scenario.userWithReadGlobally.PASSWORD, + }, + tests: { + alreadyExists: { + statusCode: 403, + response: expectRbacForbidden, + }, + defaultSpace: { + statusCode: 403, + response: expectRbacForbidden, + }, + newSpace: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + } + ); + + updateTest( + `${scenario.userWithDualRead.USERNAME} cannot update space_1 + from the ${scenario.spaceId} space`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithDualRead.USERNAME, + password: scenario.userWithDualRead.PASSWORD, + }, + tests: { + alreadyExists: { + statusCode: 403, + response: expectRbacForbidden, + }, + defaultSpace: { + statusCode: 403, + response: expectRbacForbidden, + }, + newSpace: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + } + ); + + updateTest( + `${scenario.userWithLegacyRead.USERNAME} cannot update space_1 + from the ${scenario.spaceId} space`, + { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithLegacyRead.USERNAME, + password: scenario.userWithLegacyRead.PASSWORD, + }, + tests: { + alreadyExists: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.userWithLegacyRead.USERNAME), + }, + defaultSpace: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.userWithLegacyRead.USERNAME), + }, + newSpace: { + statusCode: 403, + response: createExpectLegacyForbidden(scenario.userWithLegacyRead.USERNAME), + }, + }, + } + ); + + updateTest(`${scenario.userWithAllAtSpace.USERNAME} cannot update space_1`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithAllAtSpace.USERNAME, + password: scenario.userWithAllAtSpace.PASSWORD, + }, + tests: { + alreadyExists: { + statusCode: 403, + response: expectRbacForbidden, + }, + defaultSpace: { + statusCode: 403, + response: expectRbacForbidden, + }, + newSpace: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); + + updateTest(`${scenario.userWithReadAtSpace.USERNAME} cannot update space_1`, { + spaceId: scenario.spaceId, + auth: { + username: scenario.userWithReadAtSpace.USERNAME, + password: scenario.userWithReadAtSpace.PASSWORD, + }, + tests: { + alreadyExists: { + statusCode: 403, + response: expectRbacForbidden, + }, + defaultSpace: { + statusCode: 403, + response: expectRbacForbidden, + }, + newSpace: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/config.ts b/x-pack/test/spaces_api_integration/security_and_spaces/config.ts new file mode 100644 index 0000000000000..81cf9d85671d1 --- /dev/null +++ b/x-pack/test/spaces_api_integration/security_and_spaces/config.ts @@ -0,0 +1,10 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// tslint:disable:no-default-export +export default createTestConfig('security_and_spaces', { license: 'trial' }); diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/create.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/create.ts new file mode 100644 index 0000000000000..fb01fd18527c2 --- /dev/null +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/create.ts @@ -0,0 +1,51 @@ +/* + * 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 { createTestSuiteFactory } from '../../common/suites/create'; + +// tslint:disable:no-default-export +export default function createSpacesOnlySuite({ getService }: TestInvoker) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { + createTest, + expectNewSpaceResult, + expectConflictResponse, + expectReservedSpecifiedResult, + } = createTestSuiteFactory(esArchiver, supertestWithoutAuth); + + describe('create', () => { + [ + { + spaceId: SPACES.DEFAULT.spaceId, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + }, + ].forEach(scenario => { + createTest(`from the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + tests: { + newSpace: { + statusCode: 200, + response: expectNewSpaceResult, + }, + alreadyExists: { + statusCode: 409, + response: expectConflictResponse, + }, + reservedSpecified: { + statusCode: 200, + response: expectReservedSpecifiedResult, + }, + }, + }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/delete.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/delete.ts new file mode 100644 index 0000000000000..3b073e377a4e5 --- /dev/null +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/delete.ts @@ -0,0 +1,51 @@ +/* + * 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 { deleteTestSuiteFactory } from '../../common/suites/delete'; + +// tslint:disable:no-default-export +export default function deleteSpaceTestSuite({ getService }: TestInvoker) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { + deleteTest, + expectEmptyResult, + expectReservedSpaceResult, + expectNotFoundResult, + } = deleteTestSuiteFactory(esArchiver, supertestWithoutAuth); + + describe('delete', () => { + [ + { + spaceId: SPACES.DEFAULT.spaceId, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + }, + ].forEach(scenario => { + deleteTest(`from the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + tests: { + exists: { + statusCode: 204, + response: expectEmptyResult, + }, + reservedSpace: { + statusCode: 400, + response: expectReservedSpaceResult, + }, + doesntExist: { + statusCode: 404, + response: expectNotFoundResult, + }, + }, + }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/get.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/get.ts new file mode 100644 index 0000000000000..8017e3c62eec8 --- /dev/null +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/get.ts @@ -0,0 +1,74 @@ +/* + * 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 { getTestSuiteFactory } from '../../common/suites/get'; + +// tslint:disable:no-default-export +export default function getSpaceTestSuite({ getService }: TestInvoker) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { + getTest, + createExpectResults, + createExpectNotFoundResult, + nonExistantSpaceId, + } = getTestSuiteFactory(esArchiver, supertestWithoutAuth); + + describe('get', () => { + // valid spaces + [ + { + currentSpaceId: SPACES.DEFAULT.spaceId, + spaceId: SPACES.DEFAULT.spaceId, + }, + { + currentSpaceId: SPACES.DEFAULT.spaceId, + spaceId: SPACES.SPACE_1.spaceId, + }, + { + currentSpaceId: SPACES.SPACE_1.spaceId, + spaceId: SPACES.DEFAULT.spaceId, + }, + { + currentSpaceId: SPACES.SPACE_1.spaceId, + spaceId: SPACES.SPACE_1.spaceId, + }, + ].forEach(scenario => { + getTest(`can access ${scenario.spaceId} from within the ${scenario.currentSpaceId} space`, { + spaceId: scenario.spaceId, + currentSpaceId: scenario.currentSpaceId, + tests: { + default: { + statusCode: 200, + response: createExpectResults(scenario.spaceId), + }, + }, + }); + }); + + // invalid spaces + [ + { + currentSpaceId: SPACES.DEFAULT.spaceId, + spaceId: nonExistantSpaceId, + }, + ].forEach(scenario => { + getTest(`can't access ${scenario.spaceId} from within the ${scenario.currentSpaceId} space`, { + spaceId: scenario.spaceId, + currentSpaceId: scenario.currentSpaceId, + tests: { + default: { + statusCode: 404, + response: createExpectNotFoundResult(), + }, + }, + }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts new file mode 100644 index 0000000000000..4380df0d196f9 --- /dev/null +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts @@ -0,0 +1,41 @@ +/* + * 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 { getAllTestSuiteFactory } from '../../common/suites/get_all'; + +// tslint:disable:no-default-export +export default function getAllSpacesTestSuite({ getService }: TestInvoker) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { getAllTest, createExpectResults } = getAllTestSuiteFactory( + esArchiver, + supertestWithoutAuth + ); + + describe('get all', () => { + [ + { + spaceId: SPACES.DEFAULT.spaceId, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + }, + ].forEach(scenario => { + getAllTest(`can access all spaces from ${scenario.spaceId}`, { + spaceId: scenario.spaceId, + tests: { + exists: { + statusCode: 200, + response: createExpectResults('default', 'space_1', 'space_2'), + }, + }, + }); + }); + }); +} 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 new file mode 100644 index 0000000000000..6864ee7fbda94 --- /dev/null +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TestInvoker } from '../../common/lib/types'; + +// tslint:disable:no-default-export +export default function spacesOnlyTestSuite({ loadTestFile }: TestInvoker) { + describe('spaces api without security', () => { + loadTestFile(require.resolve('./create')); + 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 new file mode 100644 index 0000000000000..95e05822393b5 --- /dev/null +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/select.ts @@ -0,0 +1,74 @@ +/* + * 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'; + +// tslint:disable: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, + spaceId: 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, + spaceId: scenario.otherSpaceId, + tests: { + default: { + statusCode: 404, + response: createExpectNotFoundResult(), + }, + }, + }); + }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/update.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/update.ts new file mode 100644 index 0000000000000..aece5a0efc639 --- /dev/null +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/update.ts @@ -0,0 +1,51 @@ +/* + * 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 { updateTestSuiteFactory } from '../../common/suites/update'; + +// tslint:disable:no-default-export +export default function updateSpaceTestSuite({ getService }: TestInvoker) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { + updateTest, + expectAlreadyExistsResult, + expectDefaultSpaceResult, + expectNewSpaceNotFound, + } = updateTestSuiteFactory(esArchiver, supertestWithoutAuth); + + describe.only('update', () => { + [ + { + spaceId: SPACES.DEFAULT.spaceId, + }, + { + spaceId: SPACES.SPACE_1.spaceId, + }, + ].forEach(scenario => { + updateTest(`can update from the ${scenario.spaceId} space`, { + spaceId: scenario.spaceId, + tests: { + alreadyExists: { + statusCode: 200, + response: expectAlreadyExistsResult, + }, + defaultSpace: { + statusCode: 200, + response: expectDefaultSpaceResult, + }, + newSpace: { + statusCode: 404, + response: expectNewSpaceNotFound, + }, + }, + }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/apis/index.js b/x-pack/test/spaces_api_integration/spaces_only/config.ts similarity index 60% rename from x-pack/test/spaces_api_integration/apis/index.js rename to x-pack/test/spaces_api_integration/spaces_only/config.ts index 3c747f6554132..49e31da77dd74 100644 --- a/x-pack/test/spaces_api_integration/apis/index.js +++ b/x-pack/test/spaces_api_integration/spaces_only/config.ts @@ -3,9 +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 { createTestConfig } from '../common/config'; -export default function ({ loadTestFile }) { - describe('apis spaces', () => { - loadTestFile(require.resolve('./saved_objects')); - }); -} +// tslint:disable:no-default-export +export default createTestConfig('spaces_only', { license: 'basic' }); diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json new file mode 100644 index 0000000000000..87e34dc754cdc --- /dev/null +++ b/x-pack/test/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": [ + "expect.js", + "mocha", + "node" + ] + }, + "include": [ + "**/*", + ], + "exclude": [], +} diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index a01f0127c96cb..4a965fa793432 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -1,8 +1,17 @@ { - "extends": "../tsconfig.json", - "include": [ - "common/**/*", - "server/**/*", - "plugins/**/*" - ] + "extends": "../tsconfig.json", + "include": [ + "common/**/*", + "server/**/*", + "plugins/**/*", + ], + "exclude": [ + "test/**/*" + ], + "compilerOptions": { + "types": [ + "node", + "jest" + ] + } } diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index 7b2a8ad84cc67..bf3b9ea293564 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -124,6 +124,10 @@ url-join "^4.0.0" ws "^4.1.0" +"@types/cookiejar@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.0.tgz#4b7daf2c51696cfc70b942c11690528229d1a1ce" + "@types/delay@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/delay/-/delay-2.0.1.tgz#61bcf318a74b61e79d1658fbf054f984c90ef901" @@ -132,6 +136,10 @@ version "1.2.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" +"@types/expect.js@^0.3.29": + version "0.3.29" + resolved "https://registry.yarnpkg.com/@types/expect.js/-/expect.js-0.3.29.tgz#28dd359155b84b8ecb094afc3f4b74c3222dca3b" + "@types/form-data@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-2.2.1.tgz#ee2b3b8eaa11c0938289953606b745b738c54b1e" @@ -158,6 +166,10 @@ version "1.5.3" resolved "https://registry.yarnpkg.com/@types/loglevel/-/loglevel-1.5.3.tgz#adfce55383edc5998a2170ad581b3e23d6adb5b8" +"@types/mocha@^5.2.5": + version "5.2.5" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.5.tgz#8a4accfc403c124a0bafe8a9fc61a05ec1032073" + "@types/node@*": version "9.3.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-9.3.0.tgz#3a129cda7c4e5df2409702626892cb4b96546dd5" @@ -190,6 +202,19 @@ version "0.10.2" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.10.2.tgz#bd1740c4ad51966609b058803ee6874577848b37" +"@types/superagent@*": + version "3.8.4" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-3.8.4.tgz#24a5973c7d1a9c024b4bbda742a79267c33fb86a" + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + +"@types/supertest@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.5.tgz#18d082a667eaed22759be98f4923e0061ae70c62" + dependencies: + "@types/superagent" "*" + "@types/url-join@^0.8.2": version "0.8.2" resolved "https://registry.yarnpkg.com/@types/url-join/-/url-join-0.8.2.tgz#1181ecbe1d97b7034e0ea1e35e62e86cc26b422d" @@ -2357,7 +2382,7 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -expect.js@0.3.1: +expect.js@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/expect.js/-/expect.js-0.3.1.tgz#b0a59a0d2eff5437544ebf0ceaa6015841d09b5b"