class="kuiButton kuiButton--primary"
ng-click="saveRole(role)"
ng-if="!role.metadata._reserved && isRoleEnabled(role)"
- ng-disabled="form.$invalid || !areIndicesValid(role.indices)"
+ ng-disabled="form.$invalid || !areIndicesValid(role.elasticsearch.indices)"
data-test-subj="roleFormSaveButton"
>
Save
diff --git a/x-pack/plugins/security/public/views/management/edit_role.js b/x-pack/plugins/security/public/views/management/edit_role.js
index caa686ffd00f26..6a3f3a79573ffc 100644
--- a/x-pack/plugins/security/public/views/management/edit_role.js
+++ b/x-pack/plugins/security/public/views/management/edit_role.js
@@ -5,7 +5,6 @@
*/
import _ from 'lodash';
-import chrome from 'ui/chrome';
import routes from 'ui/routes';
import { fatalError, toastNotifications } from 'ui/notify';
import { toggle } from 'plugins/security/lib/util';
@@ -22,60 +21,41 @@ import { IndexPatternsProvider } from 'ui/index_patterns/index_patterns';
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
import { checkLicenseError } from 'plugins/security/lib/check_license_error';
import { EDIT_ROLES_PATH, ROLES_PATH } from './management_urls';
-import { ALL_RESOURCE } from '../../../common/constants';
-const getKibanaPrivileges = (applicationPrivileges, roleApplications, application) => {
- const kibanaPrivileges = applicationPrivileges.reduce((acc, p) => {
- acc[p.name] = false;
+const getKibanaPrivilegesViewModel = (applicationPrivileges, roleKibanaPrivileges) => {
+ const viewModel = applicationPrivileges.reduce((acc, applicationPrivilege) => {
+ acc[applicationPrivilege.name] = false;
return acc;
}, {});
- if (!roleApplications || roleApplications.length === 0) {
- return kibanaPrivileges;
+ if (!roleKibanaPrivileges || roleKibanaPrivileges.length === 0) {
+ return viewModel;
}
- // we're filtering out privileges for non-all resources incase the roles were created in a future version
- const applications = roleApplications
- .filter(roleApplication => roleApplication.application === application)
- .filter(roleApplication => !roleApplication.resources.some(resource => resource !== ALL_RESOURCE));
-
- const assigned = _.uniq(_.flatten(_.pluck(applications, 'privileges')));
- assigned.forEach(a => {
+ const assignedPrivileges = _.uniq(_.flatten(_.pluck(roleKibanaPrivileges, 'privileges')));
+ assignedPrivileges.forEach(assignedPrivilege => {
// we don't want to display privileges that aren't in our expected list of privileges
- if (a in kibanaPrivileges) {
- kibanaPrivileges[a] = true;
+ if (assignedPrivilege in viewModel) {
+ viewModel[assignedPrivilege] = true;
}
});
- return kibanaPrivileges;
+ return viewModel;
};
-const getRoleApplications = (kibanaPrivileges, currentRoleApplications = [], application) => {
- // we keep any other applications
- const newRoleApplications = currentRoleApplications.filter(roleApplication => {
- return roleApplication.application !== application;
- });
-
- const selectedPrivileges = Object.keys(kibanaPrivileges).filter(key => kibanaPrivileges[key]);
+const getKibanaPrivileges = (kibanaPrivilegesViewModel) => {
+ const selectedPrivileges = Object.keys(kibanaPrivilegesViewModel).filter(key => kibanaPrivilegesViewModel[key]);
// if we have any selected privileges, add a single application entry
if (selectedPrivileges.length > 0) {
- newRoleApplications.push({
- application,
- privileges: selectedPrivileges,
- resources: [ALL_RESOURCE]
- });
- }
-
- return newRoleApplications;
-};
-
-const getOtherApplications = (roleApplications, application) => {
- if (!roleApplications || roleApplications.length === 0) {
- return [];
+ return [
+ {
+ privileges: selectedPrivileges
+ }
+ ];
}
- return roleApplications.map(roleApplication => roleApplication.application).filter(app =>app !== application);
+ return [];
};
routes.when(`${EDIT_ROLES_PATH}/:name?`, {
@@ -97,10 +77,13 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
});
}
return new ShieldRole({
- cluster: [],
- indices: [],
- run_as: [],
- applications: []
+ elasticsearch: {
+ cluster: [],
+ indices: [],
+ run_as: [],
+ },
+ kibana: [],
+ _unrecognized_applications: []
});
},
applicationPrivileges(ApplicationPrivileges, kbnUrl, Promise, Private) {
@@ -126,7 +109,6 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
const Private = $injector.get('Private');
const confirmModal = $injector.get('confirmModal');
const shieldIndices = $injector.get('shieldIndices');
- const rbacApplication = chrome.getInjected('rbacApplication');
$scope.role = $route.current.locals.role;
$scope.users = $route.current.locals.users;
@@ -135,8 +117,8 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
const applicationPrivileges = $route.current.locals.applicationPrivileges;
const role = $route.current.locals.role;
- $scope.kibanaPrivileges = getKibanaPrivileges(applicationPrivileges, role.applications, rbacApplication);
- $scope.otherApplications = getOtherApplications(role.applications, rbacApplication);
+ $scope.kibanaPrivilegesViewModel = getKibanaPrivilegesViewModel(applicationPrivileges, role.kibana);
+ $scope.otherApplications = role._unrecognized_applications;
$scope.rolesHref = `#${ROLES_PATH}`;
@@ -158,10 +140,10 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
};
$scope.saveRole = (role) => {
- role.indices = role.indices.filter((index) => index.names.length);
- role.indices.forEach((index) => index.query || delete index.query);
+ role.elasticsearch.indices = role.elasticsearch.indices.filter((index) => index.names.length);
+ role.elasticsearch.indices.forEach((index) => index.query || delete index.query);
- role.applications = getRoleApplications($scope.kibanaPrivileges, role.applications, rbacApplication);
+ role.kibana = getKibanaPrivileges($scope.kibanaPrivilegesViewModel);
return role.$save()
.then(() => toastNotifications.addSuccess('Updated role'))
@@ -199,7 +181,7 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
$scope.allowDocumentLevelSecurity = xpackInfo.get('features.security.allowRoleDocumentLevelSecurity');
$scope.allowFieldLevelSecurity = xpackInfo.get('features.security.allowRoleFieldLevelSecurity');
- $scope.$watch('role.indices', (indices) => {
+ $scope.$watch('role.elasticsearch.indices', (indices) => {
if (!indices.length) $scope.addIndex(indices);
else indices.forEach($scope.fetchFieldOptions);
}, true);
diff --git a/x-pack/plugins/security/server/routes/api/public/roles/delete.js b/x-pack/plugins/security/server/routes/api/public/roles/delete.js
new file mode 100644
index 00000000000000..697a9bd32df1b9
--- /dev/null
+++ b/x-pack/plugins/security/server/routes/api/public/roles/delete.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import _ from 'lodash';
+import Joi from 'joi';
+import { wrapError } from '../../../../lib/errors';
+
+export function initDeleteRolesApi(server, callWithRequest, routePreCheckLicenseFn) {
+ server.route({
+ method: 'DELETE',
+ path: '/api/security/role/{name}',
+ handler(request, reply) {
+ const name = request.params.name;
+ return callWithRequest(request, 'shield.deleteRole', { name }).then(
+ () => reply().code(204),
+ _.flow(wrapError, reply));
+ },
+ config: {
+ validate: {
+ params: Joi.object()
+ .keys({
+ name: Joi.string()
+ .required(),
+ })
+ .required(),
+ },
+ pre: [routePreCheckLicenseFn]
+ }
+ });
+}
diff --git a/x-pack/plugins/security/server/routes/api/public/roles/delete.test.js b/x-pack/plugins/security/server/routes/api/public/roles/delete.test.js
new file mode 100644
index 00000000000000..0337b8e8b9f661
--- /dev/null
+++ b/x-pack/plugins/security/server/routes/api/public/roles/delete.test.js
@@ -0,0 +1,125 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import Hapi from 'hapi';
+import Boom from 'boom';
+import { initDeleteRolesApi } from './delete';
+
+const createMockServer = () => {
+ const mockServer = new Hapi.Server({ debug: false });
+ mockServer.connection({ port: 8080 });
+ return mockServer;
+};
+
+const defaultPreCheckLicenseImpl = (request, reply) => reply();
+
+describe('DELETE role', () => {
+ const deleteRoleTest = (
+ description,
+ {
+ name,
+ preCheckLicenseImpl,
+ callWithRequestImpl,
+ asserts,
+ }
+ ) => {
+ test(description, async () => {
+ const mockServer = createMockServer();
+ const pre = jest.fn().mockImplementation(preCheckLicenseImpl);
+ const mockCallWithRequest = jest.fn();
+ if (callWithRequestImpl) {
+ mockCallWithRequest.mockImplementation(callWithRequestImpl);
+ }
+ initDeleteRolesApi(mockServer, mockCallWithRequest, pre);
+ const headers = {
+ authorization: 'foo',
+ };
+
+ const request = {
+ method: 'DELETE',
+ url: `/api/security/role/${name}`,
+ headers,
+ };
+ const { result, statusCode } = await mockServer.inject(request);
+
+ if (preCheckLicenseImpl) {
+ expect(pre).toHaveBeenCalled();
+ } else {
+ expect(pre).not.toHaveBeenCalled();
+ }
+
+ if (callWithRequestImpl) {
+ expect(mockCallWithRequest).toHaveBeenCalledWith(
+ expect.objectContaining({
+ headers: expect.objectContaining({
+ authorization: headers.authorization,
+ }),
+ }),
+ 'shield.deleteRole',
+ { name },
+ );
+ } else {
+ expect(mockCallWithRequest).not.toHaveBeenCalled();
+ }
+ expect(statusCode).toBe(asserts.statusCode);
+ expect(result).toEqual(asserts.result);
+ });
+ };
+
+ describe('failure', () => {
+ deleteRoleTest(`requires name in params`, {
+ name: '',
+ asserts: {
+ statusCode: 404,
+ result: {
+ error: 'Not Found',
+ statusCode: 404,
+ },
+ },
+ });
+
+ deleteRoleTest(`returns result of routePreCheckLicense`, {
+ preCheckLicenseImpl: (request, reply) =>
+ reply(Boom.forbidden('test forbidden message')),
+ asserts: {
+ statusCode: 403,
+ result: {
+ error: 'Forbidden',
+ statusCode: 403,
+ message: 'test forbidden message',
+ },
+ },
+ });
+
+ deleteRoleTest(`returns error from callWithRequest`, {
+ name: 'foo-role',
+ preCheckLicenseImpl: defaultPreCheckLicenseImpl,
+ callWithRequestImpl: async () => {
+ throw Boom.notFound('test not found message');
+ },
+ asserts: {
+ statusCode: 404,
+ result: {
+ error: 'Not Found',
+ statusCode: 404,
+ message: 'test not found message',
+ },
+ },
+ });
+ });
+
+ describe('success', () => {
+ deleteRoleTest(`deletes role`, {
+ name: 'foo-role',
+ preCheckLicenseImpl: defaultPreCheckLicenseImpl,
+ callWithRequestImpl: async () => {},
+ asserts: {
+ statusCode: 204,
+ result: null
+ }
+ });
+ });
+});
diff --git a/x-pack/plugins/security/server/routes/api/public/roles/get.js b/x-pack/plugins/security/server/routes/api/public/roles/get.js
new file mode 100644
index 00000000000000..9ae89a97c36f1a
--- /dev/null
+++ b/x-pack/plugins/security/server/routes/api/public/roles/get.js
@@ -0,0 +1,78 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import _ from 'lodash';
+import Boom from 'boom';
+import { ALL_RESOURCE } from '../../../../../common/constants';
+import { wrapError } from '../../../../lib/errors';
+
+export function initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, application) {
+
+ const transformKibanaApplicationsFromEs = (roleApplications) => {
+ return roleApplications
+ .filter(roleApplication => roleApplication.application === application)
+ .filter(roleApplication => roleApplication.resources.length > 0)
+ .filter(roleApplication => roleApplication.resources.every(resource => resource === ALL_RESOURCE))
+ .map(roleApplication => ({ privileges: roleApplication.privileges }));
+ };
+
+ const transformUnrecognizedApplicationsFromEs = (roleApplications) => {
+ return _.uniq(roleApplications
+ .filter(roleApplication => roleApplication.application !== application)
+ .map(roleApplication => roleApplication.application));
+ };
+
+ const transformRoleFromEs = (role, name) => {
+ return {
+ name,
+ metadata: role.metadata,
+ transient_metadata: role.transient_metadata,
+ elasticsearch: {
+ cluster: role.cluster,
+ indices: role.indices,
+ run_as: role.run_as,
+ },
+ kibana: transformKibanaApplicationsFromEs(role.applications),
+ _unrecognized_applications: transformUnrecognizedApplicationsFromEs(role.applications),
+ };
+ };
+
+ const transformRolesFromEs = (roles) => {
+ return _.map(roles, (role, name) => transformRoleFromEs(role, name));
+ };
+
+ server.route({
+ method: 'GET',
+ path: '/api/security/role',
+ handler(request, reply) {
+ return callWithRequest(request, 'shield.getRole').then(
+ (response) => {
+ return reply(transformRolesFromEs(response));
+ },
+ _.flow(wrapError, reply)
+ );
+ },
+ config: {
+ pre: [routePreCheckLicenseFn]
+ }
+ });
+
+ server.route({
+ method: 'GET',
+ path: '/api/security/role/{name}',
+ handler(request, reply) {
+ const name = request.params.name;
+ return callWithRequest(request, 'shield.getRole', { name }).then(
+ (response) => {
+ if (response[name]) return reply(transformRoleFromEs(response[name], name));
+ return reply(Boom.notFound());
+ },
+ _.flow(wrapError, reply));
+ },
+ config: {
+ pre: [routePreCheckLicenseFn]
+ }
+ });
+}
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
new file mode 100644
index 00000000000000..a8dd3a38bdeb91
--- /dev/null
+++ b/x-pack/plugins/security/server/routes/api/public/roles/get.test.js
@@ -0,0 +1,577 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import Hapi from 'hapi';
+import Boom from 'boom';
+import { initGetRolesApi } from './get';
+
+const application = 'kibana-.kibana';
+
+const createMockServer = () => {
+ const mockServer = new Hapi.Server({ debug: false });
+ mockServer.connection({ port: 8080 });
+ return mockServer;
+};
+
+describe('GET roles', () => {
+ const getRolesTest = (
+ description,
+ {
+ preCheckLicenseImpl = (request, reply) => reply(),
+ callWithRequestImpl,
+ asserts,
+ }
+ ) => {
+ test(description, async () => {
+ const mockServer = createMockServer();
+ const pre = jest.fn().mockImplementation(preCheckLicenseImpl);
+ const mockCallWithRequest = jest.fn();
+ if (callWithRequestImpl) {
+ mockCallWithRequest.mockImplementation(callWithRequestImpl);
+ }
+ initGetRolesApi(mockServer, mockCallWithRequest, pre, application);
+ const headers = {
+ authorization: 'foo',
+ };
+
+ const request = {
+ method: 'GET',
+ url: '/api/security/role',
+ headers,
+ };
+ const { result, statusCode } = await mockServer.inject(request);
+
+ expect(pre).toHaveBeenCalled();
+ if (callWithRequestImpl) {
+ expect(mockCallWithRequest).toHaveBeenCalledWith(
+ expect.objectContaining({
+ headers: expect.objectContaining({
+ authorization: headers.authorization,
+ }),
+ }),
+ 'shield.getRole'
+ );
+ } else {
+ expect(mockCallWithRequest).not.toHaveBeenCalled();
+ }
+ expect(statusCode).toBe(asserts.statusCode);
+ expect(result).toEqual(asserts.result);
+ });
+ };
+
+ describe('failure', () => {
+ getRolesTest(`returns result of routePreCheckLicense`, {
+ preCheckLicenseImpl: (request, reply) =>
+ reply(Boom.forbidden('test forbidden message')),
+ asserts: {
+ statusCode: 403,
+ result: {
+ error: 'Forbidden',
+ statusCode: 403,
+ message: 'test forbidden message',
+ },
+ },
+ });
+
+ getRolesTest(`returns error from callWithRequest`, {
+ callWithRequestImpl: async () => {
+ throw Boom.notAcceptable('test not acceptable message');
+ },
+ asserts: {
+ statusCode: 406,
+ result: {
+ error: 'Not Acceptable',
+ statusCode: 406,
+ message: 'test not acceptable message',
+ },
+ },
+ });
+ });
+
+ describe('success', () => {
+ getRolesTest(`transforms elasticsearch privileges`, {
+ callWithRequestImpl: async () => ({
+ first_role: {
+ cluster: ['manage_watcher'],
+ indices: [
+ {
+ names: ['.kibana*'],
+ privileges: ['read', 'view_index_metadata'],
+ },
+ ],
+ applications: [],
+ run_as: ['other_user'],
+ metadata: {
+ _reserved: true,
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ },
+ }),
+ asserts: {
+ statusCode: 200,
+ result: [
+ {
+ name: 'first_role',
+ metadata: {
+ _reserved: true,
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ elasticsearch: {
+ cluster: ['manage_watcher'],
+ indices: [
+ {
+ names: ['.kibana*'],
+ privileges: ['read', 'view_index_metadata'],
+ },
+ ],
+ run_as: ['other_user'],
+ },
+ kibana: [],
+ _unrecognized_applications: [],
+ },
+ ],
+ },
+ });
+
+ getRolesTest(`transforms matching applications to kibana privileges`, {
+ callWithRequestImpl: async () => ({
+ first_role: {
+ cluster: [],
+ indices: [],
+ applications: [
+ {
+ application,
+ privileges: ['read'],
+ resources: ['*'],
+ },
+ {
+ application,
+ privileges: ['all'],
+ resources: ['*'],
+ },
+ ],
+ run_as: [],
+ metadata: {
+ _reserved: true,
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ },
+ }),
+ asserts: {
+ statusCode: 200,
+ result: [
+ {
+ name: 'first_role',
+ metadata: {
+ _reserved: true,
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ elasticsearch: {
+ cluster: [],
+ indices: [],
+ run_as: [],
+ },
+ kibana: [
+ {
+ privileges: ['read'],
+ },
+ {
+ privileges: ['all'],
+ },
+ ],
+ _unrecognized_applications: [],
+ },
+ ],
+ },
+ });
+
+ getRolesTest(`excludes resources other than * from kibana privileges`, {
+ callWithRequestImpl: async () => ({
+ first_role: {
+ cluster: [],
+ indices: [],
+ applications: [
+ {
+ application,
+ privileges: ['read'],
+ // Elasticsearch should prevent this from happening
+ resources: [],
+ },
+ {
+ application,
+ privileges: ['read'],
+ resources: ['default', '*'],
+ },
+ {
+ application,
+ privileges: ['read'],
+ resources: ['some-other-space'],
+ },
+ ],
+ run_as: [],
+ metadata: {
+ _reserved: true,
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ },
+ }),
+ asserts: {
+ statusCode: 200,
+ result: [
+ {
+ name: 'first_role',
+ metadata: {
+ _reserved: true,
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ elasticsearch: {
+ cluster: [],
+ indices: [],
+ run_as: [],
+ },
+ kibana: [],
+ _unrecognized_applications: [],
+ },
+ ],
+ },
+ });
+
+ getRolesTest(`transforms unrecognized applications`, {
+ callWithRequestImpl: async () => ({
+ first_role: {
+ cluster: [],
+ indices: [],
+ applications: [
+ {
+ application: 'kibana-.another-kibana',
+ privileges: ['read'],
+ resources: ['*'],
+ },
+ ],
+ run_as: [],
+ metadata: {
+ _reserved: true,
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ },
+ }),
+ asserts: {
+ statusCode: 200,
+ result: [
+ {
+ name: 'first_role',
+ metadata: {
+ _reserved: true,
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ elasticsearch: {
+ cluster: [],
+ indices: [],
+ run_as: [],
+ },
+ kibana: [],
+ _unrecognized_applications: ['kibana-.another-kibana']
+ },
+ ],
+ },
+ });
+ });
+});
+
+describe('GET role', () => {
+ const getRoleTest = (
+ description,
+ {
+ name,
+ preCheckLicenseImpl = (request, reply) => reply(),
+ callWithRequestImpl,
+ asserts,
+ }
+ ) => {
+ test(description, async () => {
+ const mockServer = createMockServer();
+ const pre = jest.fn().mockImplementation(preCheckLicenseImpl);
+ const mockCallWithRequest = jest.fn();
+ if (callWithRequestImpl) {
+ mockCallWithRequest.mockImplementation(callWithRequestImpl);
+ }
+ initGetRolesApi(mockServer, mockCallWithRequest, pre, 'kibana-.kibana');
+ const headers = {
+ authorization: 'foo',
+ };
+
+ const request = {
+ method: 'GET',
+ url: `/api/security/role/${name}`,
+ headers,
+ };
+ const { result, statusCode } = await mockServer.inject(request);
+
+ expect(pre).toHaveBeenCalled();
+ if (callWithRequestImpl) {
+ expect(mockCallWithRequest).toHaveBeenCalledWith(
+ expect.objectContaining({
+ headers: expect.objectContaining({
+ authorization: headers.authorization,
+ }),
+ }),
+ 'shield.getRole',
+ { name }
+ );
+ } else {
+ expect(mockCallWithRequest).not.toHaveBeenCalled();
+ }
+ expect(statusCode).toBe(asserts.statusCode);
+ expect(result).toEqual(asserts.result);
+ });
+ };
+
+ describe('failure', () => {
+ getRoleTest(`returns result of routePreCheckLicense`, {
+ preCheckLicenseImpl: (request, reply) =>
+ reply(Boom.forbidden('test forbidden message')),
+ asserts: {
+ statusCode: 403,
+ result: {
+ error: 'Forbidden',
+ statusCode: 403,
+ message: 'test forbidden message',
+ },
+ },
+ });
+
+ getRoleTest(`returns error from callWithRequest`, {
+ name: 'foo-role',
+ callWithRequestImpl: async () => {
+ throw Boom.notAcceptable('test not acceptable message');
+ },
+ asserts: {
+ statusCode: 406,
+ result: {
+ error: 'Not Acceptable',
+ statusCode: 406,
+ message: 'test not acceptable message',
+ },
+ },
+ });
+ });
+
+ describe('success', () => {
+ getRoleTest(`transforms elasticsearch privileges`, {
+ name: 'first_role',
+ callWithRequestImpl: async () => ({
+ first_role: {
+ cluster: ['manage_watcher'],
+ indices: [
+ {
+ names: ['.kibana*'],
+ privileges: ['read', 'view_index_metadata'],
+ },
+ ],
+ applications: [],
+ run_as: ['other_user'],
+ metadata: {
+ _reserved: true,
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ },
+ }),
+ asserts: {
+ statusCode: 200,
+ result: {
+ name: 'first_role',
+ metadata: {
+ _reserved: true,
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ elasticsearch: {
+ cluster: ['manage_watcher'],
+ indices: [
+ {
+ names: ['.kibana*'],
+ privileges: ['read', 'view_index_metadata'],
+ },
+ ],
+ run_as: ['other_user'],
+ },
+ kibana: [],
+ _unrecognized_applications: [],
+ },
+ },
+ });
+
+ getRoleTest(`transforms matching applications to kibana privileges`, {
+ name: 'first_role',
+ callWithRequestImpl: async () => ({
+ first_role: {
+ cluster: [],
+ indices: [],
+ applications: [
+ {
+ application,
+ privileges: ['read'],
+ resources: ['*'],
+ },
+ {
+ application,
+ privileges: ['all'],
+ resources: ['*'],
+ },
+ ],
+ run_as: [],
+ metadata: {
+ _reserved: true,
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ },
+ }),
+ asserts: {
+ statusCode: 200,
+ result: {
+ name: 'first_role',
+ metadata: {
+ _reserved: true,
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ elasticsearch: {
+ cluster: [],
+ indices: [],
+ run_as: [],
+ },
+ kibana: [
+ {
+ privileges: ['read'],
+ },
+ {
+ privileges: ['all'],
+ },
+ ],
+ _unrecognized_applications: [],
+ },
+ },
+ });
+
+ getRoleTest(`excludes resources other than * from kibana privileges`, {
+ name: 'first_role',
+ callWithRequestImpl: async () => ({
+ first_role: {
+ cluster: [],
+ indices: [],
+ applications: [
+ {
+ application,
+ privileges: ['read'],
+ // Elasticsearch should prevent this from happening
+ resources: [],
+ },
+ {
+ application,
+ privileges: ['read'],
+ resources: ['default', '*'],
+ },
+ {
+ application,
+ privileges: ['read'],
+ resources: ['some-other-space'],
+ },
+ ],
+ run_as: [],
+ metadata: {
+ _reserved: true,
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ },
+ }),
+ asserts: {
+ statusCode: 200,
+ result: {
+ name: 'first_role',
+ metadata: {
+ _reserved: true,
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ elasticsearch: {
+ cluster: [],
+ indices: [],
+ run_as: [],
+ },
+ kibana: [],
+ _unrecognized_applications: [],
+ },
+ },
+ });
+
+ getRoleTest(`transforms unrecognized applications`, {
+ name: 'first_role',
+ callWithRequestImpl: async () => ({
+ first_role: {
+ cluster: [],
+ indices: [],
+ applications: [
+ {
+ application: 'kibana-.another-kibana',
+ privileges: ['read'],
+ resources: ['*'],
+ },
+ ],
+ run_as: [],
+ metadata: {
+ _reserved: true,
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ },
+ }),
+ asserts: {
+ statusCode: 200,
+ result: {
+ name: 'first_role',
+ metadata: {
+ _reserved: true,
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ elasticsearch: {
+ cluster: [],
+ indices: [],
+ run_as: [],
+ },
+ kibana: [],
+ _unrecognized_applications: ['kibana-.another-kibana'],
+ },
+ },
+ });
+ });
+});
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
new file mode 100644
index 00000000000000..5425af0a1202d3
--- /dev/null
+++ b/x-pack/plugins/security/server/routes/api/public/roles/index.js
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { buildPrivilegeMap } from '../../../../lib/authorization';
+import { getClient } from '../../../../../../../server/lib/get_client_shield';
+import { routePreCheckLicense } from '../../../../lib/route_pre_check_license';
+import { initGetRolesApi } from './get';
+import { initDeleteRolesApi } from './delete';
+import { initPutRolesApi } from './put';
+
+export function initPublicRolesApi(server) {
+ const callWithRequest = getClient(server).callWithRequest;
+ const routePreCheckLicenseFn = routePreCheckLicense(server);
+
+ const { application, actions } = server.plugins.security.authorization;
+ const savedObjectTypes = server.savedObjects.types;
+ const privilegeMap = buildPrivilegeMap(savedObjectTypes, application, actions);
+
+ initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, application);
+ initPutRolesApi(server, callWithRequest, routePreCheckLicenseFn, privilegeMap, application);
+ initDeleteRolesApi(server, callWithRequest, routePreCheckLicenseFn);
+}
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
new file mode 100644
index 00000000000000..123ce128e15099
--- /dev/null
+++ b/x-pack/plugins/security/server/routes/api/public/roles/put.js
@@ -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 { pick, identity } from 'lodash';
+import Joi from 'joi';
+import { ALL_RESOURCE } from '../../../../../common/constants';
+import { wrapError } from '../../../../lib/errors';
+
+const transformKibanaPrivilegeToEs = (application, kibanaPrivilege) => {
+ return {
+ privileges: kibanaPrivilege.privileges,
+ application,
+ resources: [ALL_RESOURCE],
+ };
+};
+
+const transformRolesToEs = (
+ application,
+ payload,
+ existingApplications = []
+) => {
+ const { elasticsearch = {}, kibana = [] } = payload;
+ const otherApplications = existingApplications.filter(
+ roleApplication => roleApplication.application !== application
+ );
+
+ return pick({
+ metadata: payload.metadata,
+ cluster: elasticsearch.cluster || [],
+ indices: elasticsearch.indices || [],
+ run_as: elasticsearch.run_as || [],
+ applications: [
+ ...kibana.map(kibanaPrivilege =>
+ transformKibanaPrivilegeToEs(application, kibanaPrivilege)
+ ),
+ ...otherApplications,
+ ],
+ }, identity);
+};
+
+export function initPutRolesApi(
+ server,
+ callWithRequest,
+ routePreCheckLicenseFn,
+ privilegeMap,
+ application
+) {
+
+ const schema = Joi.object().keys({
+ metadata: Joi.object().optional(),
+ elasticsearch: Joi.object().keys({
+ cluster: Joi.array().items(Joi.string()),
+ indices: Joi.array().items({
+ names: Joi.array().items(Joi.string()),
+ field_security: Joi.object().keys({
+ grant: Joi.array().items(Joi.string()),
+ except: Joi.array().items(Joi.string()),
+ }),
+ privileges: Joi.array().items(Joi.string()),
+ query: Joi.string().allow(''),
+ }),
+ run_as: Joi.array().items(Joi.string()),
+ }),
+ kibana: Joi.array().items({
+ privileges: Joi.array().items(Joi.string().valid(Object.keys(privilegeMap))),
+ }),
+ });
+
+ server.route({
+ method: 'PUT',
+ path: '/api/security/role/{name}',
+ async handler(request, reply) {
+ const name = request.params.name;
+ try {
+ const existingRoleResponse = await callWithRequest(request, 'shield.getRole', {
+ name,
+ ignore: [404],
+ });
+
+ const body = transformRolesToEs(
+ application,
+ request.payload,
+ existingRoleResponse[name] ? existingRoleResponse[name].applications : []
+ );
+
+ await callWithRequest(request, 'shield.putRole', { name, body });
+ reply().code(204);
+ } catch (err) {
+ reply(wrapError(err));
+ }
+ },
+ config: {
+ validate: {
+ params: Joi.object()
+ .keys({
+ name: Joi.string()
+ .required()
+ .min(1)
+ .max(1024),
+ })
+ .required(),
+ payload: schema,
+ },
+ pre: [routePreCheckLicenseFn],
+ },
+ });
+}
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
new file mode 100644
index 00000000000000..d6c32ce00ac563
--- /dev/null
+++ b/x-pack/plugins/security/server/routes/api/public/roles/put.test.js
@@ -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 Hapi from 'hapi';
+import Boom from 'boom';
+import { initPutRolesApi } from './put';
+import { ALL_RESOURCE } from '../../../../../common/constants';
+
+const application = 'kibana-.kibana';
+
+const createMockServer = () => {
+ const mockServer = new Hapi.Server({ debug: false });
+ mockServer.connection({ port: 8080 });
+ return mockServer;
+};
+
+const defaultPreCheckLicenseImpl = (request, reply) => reply();
+
+const privilegeMap = {
+ 'test-kibana-privilege-1': {},
+ 'test-kibana-privilege-2': {},
+ 'test-kibana-privilege-3': {},
+};
+
+const putRoleTest = (
+ description,
+ { name, payload, preCheckLicenseImpl, callWithRequestImpls = [], asserts }
+) => {
+ test(description, async () => {
+ const mockServer = createMockServer();
+ const mockPreCheckLicense = jest
+ .fn()
+ .mockImplementation(preCheckLicenseImpl);
+ const mockCallWithRequest = jest.fn();
+ for (const impl of callWithRequestImpls) {
+ mockCallWithRequest.mockImplementationOnce(impl);
+ }
+ initPutRolesApi(
+ mockServer,
+ mockCallWithRequest,
+ mockPreCheckLicense,
+ privilegeMap,
+ application,
+ );
+ const headers = {
+ authorization: 'foo',
+ };
+
+ const request = {
+ method: 'PUT',
+ url: `/api/security/role/${name}`,
+ headers,
+ payload,
+ };
+ const { result, statusCode } = await mockServer.inject(request);
+
+ expect(result).toEqual(asserts.result);
+ expect(statusCode).toBe(asserts.statusCode);
+ if (preCheckLicenseImpl) {
+ expect(mockPreCheckLicense).toHaveBeenCalled();
+ } else {
+ expect(mockPreCheckLicense).not.toHaveBeenCalled();
+ }
+ if (asserts.callWithRequests) {
+ for (const args of asserts.callWithRequests) {
+ expect(mockCallWithRequest).toHaveBeenCalledWith(
+ expect.objectContaining({
+ headers: expect.objectContaining({
+ authorization: headers.authorization,
+ }),
+ }),
+ ...args
+ );
+ }
+ } else {
+ expect(mockCallWithRequest).not.toHaveBeenCalled();
+ }
+ });
+};
+
+describe('PUT role', () => {
+ describe('failure', () => {
+ putRoleTest(`requires name in params`, {
+ name: '',
+ payload: {},
+ asserts: {
+ statusCode: 404,
+ result: {
+ error: 'Not Found',
+ statusCode: 404,
+ },
+ },
+ });
+
+ putRoleTest(`requires name in params to not exceed 1024 characters`, {
+ name: 'a'.repeat(1025),
+ payload: {},
+ asserts: {
+ statusCode: 400,
+ result: {
+ error: 'Bad Request',
+ message: `child "name" fails because ["name" length must be less than or equal to 1024 characters long]`,
+ statusCode: 400,
+ validation: {
+ keys: ['name'],
+ source: 'params',
+ },
+ },
+ },
+ });
+
+ putRoleTest(`only allows known Kibana privileges`, {
+ name: 'foo-role',
+ payload: {
+ kibana: [
+ {
+ privileges: ['foo']
+ }
+ ]
+ },
+ asserts: {
+ statusCode: 400,
+ result: {
+ error: 'Bad Request',
+ //eslint-disable-next-line max-len
+ message: `child "kibana" fails because ["kibana" at position 0 fails because [child "privileges" fails because ["privileges" at position 0 fails because ["0" must be one of [test-kibana-privilege-1, test-kibana-privilege-2, test-kibana-privilege-3]]]]]`,
+ statusCode: 400,
+ validation: {
+ keys: ['kibana.0.privileges.0'],
+ source: 'payload',
+ },
+ },
+ },
+ });
+
+ putRoleTest(`returns result of routePreCheckLicense`, {
+ name: 'foo-role',
+ payload: {},
+ preCheckLicenseImpl: (request, reply) =>
+ reply(Boom.forbidden('test forbidden message')),
+ asserts: {
+ statusCode: 403,
+ result: {
+ error: 'Forbidden',
+ statusCode: 403,
+ message: 'test forbidden message',
+ },
+ },
+ });
+ });
+
+ describe('success', () => {
+ putRoleTest(`creates empty role`, {
+ name: 'foo-role',
+ payload: {},
+ preCheckLicenseImpl: defaultPreCheckLicenseImpl,
+ callWithRequestImpls: [async () => ({}), async () => {}],
+ asserts: {
+ callWithRequests: [
+ ['shield.getRole', { name: 'foo-role', ignore: [404] }],
+ [
+ 'shield.putRole',
+ {
+ name: 'foo-role',
+ body: {
+ cluster: [],
+ indices: [],
+ run_as: [],
+ applications: [],
+ },
+ },
+ ],
+ ],
+ statusCode: 204,
+ result: null,
+ },
+ });
+
+ putRoleTest(`creates role with everything`, {
+ name: 'foo-role',
+ payload: {
+ metadata: {
+ foo: 'test-metadata',
+ },
+ elasticsearch: {
+ cluster: ['test-cluster-privilege'],
+ indices: [
+ {
+ field_security: {
+ grant: ['test-field-security-grant-1', 'test-field-security-grant-2'],
+ except: [ 'test-field-security-except-1', 'test-field-security-except-2' ]
+ },
+ names: ['test-index-name-1', 'test-index-name-2'],
+ privileges: ['test-index-privilege-1', 'test-index-privilege-2'],
+ query: `{ "match": { "title": "foo" } }`,
+ },
+ ],
+ run_as: ['test-run-as-1', 'test-run-as-2'],
+ },
+ kibana: [
+ {
+ privileges: ['test-kibana-privilege-1', 'test-kibana-privilege-2'],
+ },
+ {
+ privileges: ['test-kibana-privilege-3'],
+ },
+ ],
+ },
+ preCheckLicenseImpl: defaultPreCheckLicenseImpl,
+ callWithRequestImpls: [async () => ({}), async () => {}],
+ asserts: {
+ callWithRequests: [
+ ['shield.getRole', { name: 'foo-role', ignore: [404] }],
+ [
+ 'shield.putRole',
+ {
+ name: 'foo-role',
+ body: {
+ applications: [
+ {
+ application,
+ privileges: [
+ 'test-kibana-privilege-1',
+ 'test-kibana-privilege-2',
+ ],
+ resources: [ALL_RESOURCE],
+ },
+ {
+ application,
+ privileges: ['test-kibana-privilege-3'],
+ resources: [ALL_RESOURCE],
+ },
+ ],
+ cluster: ['test-cluster-privilege'],
+ indices: [
+ {
+ field_security: {
+ grant: ['test-field-security-grant-1', 'test-field-security-grant-2'],
+ except: [ 'test-field-security-except-1', 'test-field-security-except-2' ]
+ },
+ names: ['test-index-name-1', 'test-index-name-2'],
+ privileges: [
+ 'test-index-privilege-1',
+ 'test-index-privilege-2',
+ ],
+ query: `{ "match": { "title": "foo" } }`,
+ },
+ ],
+ metadata: { foo: 'test-metadata' },
+ run_as: ['test-run-as-1', 'test-run-as-2'],
+ },
+ },
+ ],
+ ],
+ statusCode: 204,
+ result: null,
+ },
+ });
+
+ putRoleTest(`updates role which has existing kibana privileges`, {
+ name: 'foo-role',
+ payload: {
+ metadata: {
+ foo: 'test-metadata',
+ },
+ elasticsearch: {
+ cluster: ['test-cluster-privilege'],
+ indices: [
+ {
+ field_security: {
+ grant: ['test-field-security-grant-1', 'test-field-security-grant-2'],
+ except: [ 'test-field-security-except-1', 'test-field-security-except-2' ]
+ },
+ names: ['test-index-name-1', 'test-index-name-2'],
+ privileges: ['test-index-privilege-1', 'test-index-privilege-2'],
+ query: `{ "match": { "title": "foo" } }`,
+ },
+ ],
+ run_as: ['test-run-as-1', 'test-run-as-2'],
+ },
+ kibana: [
+ {
+ privileges: ['test-kibana-privilege-1', 'test-kibana-privilege-2'],
+ },
+ {
+ privileges: ['test-kibana-privilege-3'],
+ },
+ ],
+ },
+ preCheckLicenseImpl: defaultPreCheckLicenseImpl,
+ callWithRequestImpls: [
+ async () => ({
+ 'foo-role': {
+ metadata: {
+ bar: 'old-metadata',
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ cluster: ['old-cluster-privilege'],
+ indices: [
+ {
+ field_security: {
+ grant: ['old-field-security-grant-1', 'old-field-security-grant-2'],
+ except: [ 'old-field-security-except-1', 'old-field-security-except-2' ]
+ },
+ names: ['old-index-name'],
+ privileges: ['old-privilege'],
+ query: `{ "match": { "old-title": "foo" } }`,
+ },
+ ],
+ run_as: ['old-run-as'],
+ applications: [
+ {
+ application,
+ privileges: ['old-kibana-privilege'],
+ resources: ['old-resource'],
+ },
+ ],
+ },
+ }),
+ async () => {},
+ ],
+ asserts: {
+ callWithRequests: [
+ ['shield.getRole', { name: 'foo-role', ignore: [404] }],
+ [
+ 'shield.putRole',
+ {
+ name: 'foo-role',
+ body: {
+ applications: [
+ {
+ application,
+ privileges: [
+ 'test-kibana-privilege-1',
+ 'test-kibana-privilege-2',
+ ],
+ resources: [ALL_RESOURCE],
+ },
+ {
+ application,
+ privileges: ['test-kibana-privilege-3'],
+ resources: [ALL_RESOURCE],
+ },
+ ],
+ cluster: ['test-cluster-privilege'],
+ indices: [
+ {
+ field_security: {
+ grant: ['test-field-security-grant-1', 'test-field-security-grant-2'],
+ except: [ 'test-field-security-except-1', 'test-field-security-except-2' ]
+ },
+ names: ['test-index-name-1', 'test-index-name-2'],
+ privileges: [
+ 'test-index-privilege-1',
+ 'test-index-privilege-2',
+ ],
+ query: `{ "match": { "title": "foo" } }`,
+ },
+ ],
+ metadata: { foo: 'test-metadata' },
+ run_as: ['test-run-as-1', 'test-run-as-2'],
+ },
+ },
+ ],
+ ],
+ statusCode: 204,
+ result: null,
+ },
+ });
+
+ putRoleTest(
+ `updates role which has existing other application privileges`,
+ {
+ name: 'foo-role',
+ payload: {
+ metadata: {
+ foo: 'test-metadata',
+ },
+ elasticsearch: {
+ cluster: ['test-cluster-privilege'],
+ indices: [
+ {
+ names: ['test-index-name-1', 'test-index-name-2'],
+ privileges: [
+ 'test-index-privilege-1',
+ 'test-index-privilege-2',
+ ],
+ },
+ ],
+ run_as: ['test-run-as-1', 'test-run-as-2'],
+ },
+ kibana: [
+ {
+ privileges: [
+ 'test-kibana-privilege-1',
+ 'test-kibana-privilege-2',
+ ],
+ },
+ {
+ privileges: ['test-kibana-privilege-3'],
+ },
+ ],
+ },
+ preCheckLicenseImpl: defaultPreCheckLicenseImpl,
+ callWithRequestImpls: [
+ async () => ({
+ 'foo-role': {
+ metadata: {
+ bar: 'old-metadata',
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ cluster: ['old-cluster-privilege'],
+ indices: [
+ {
+ names: ['old-index-name'],
+ privileges: ['old-privilege'],
+ },
+ ],
+ run_as: ['old-run-as'],
+ applications: [
+ {
+ application,
+ privileges: ['old-kibana-privilege'],
+ resources: ['old-resource'],
+ },
+ {
+ application: 'logstash-foo',
+ privileges: ['logstash-privilege'],
+ resources: ['logstash-resource'],
+ },
+ {
+ application: 'beats-foo',
+ privileges: ['beats-privilege'],
+ resources: ['beats-resource'],
+ },
+ ],
+ },
+ }),
+ async () => {},
+ ],
+ asserts: {
+ callWithRequests: [
+ ['shield.getRole', { name: 'foo-role', ignore: [404] }],
+ [
+ 'shield.putRole',
+ {
+ name: 'foo-role',
+ body: {
+ applications: [
+ {
+ application,
+ privileges: [
+ 'test-kibana-privilege-1',
+ 'test-kibana-privilege-2',
+ ],
+ resources: [ALL_RESOURCE],
+ },
+ {
+ application,
+ privileges: ['test-kibana-privilege-3'],
+ resources: [ALL_RESOURCE],
+ },
+ {
+ application: 'logstash-foo',
+ privileges: ['logstash-privilege'],
+ resources: ['logstash-resource'],
+ },
+ {
+ application: 'beats-foo',
+ privileges: ['beats-privilege'],
+ resources: ['beats-resource'],
+ },
+ ],
+ cluster: ['test-cluster-privilege'],
+ indices: [
+ {
+ names: ['test-index-name-1', 'test-index-name-2'],
+ privileges: [
+ 'test-index-privilege-1',
+ 'test-index-privilege-2',
+ ],
+ },
+ ],
+ metadata: { foo: 'test-metadata' },
+ run_as: ['test-run-as-1', 'test-run-as-2'],
+ },
+ },
+ ],
+ ],
+ statusCode: 204,
+ result: null,
+ },
+ }
+ );
+ });
+});
diff --git a/x-pack/plugins/security/server/routes/api/v1/roles.js b/x-pack/plugins/security/server/routes/api/v1/roles.js
deleted file mode 100644
index 180160cb85a17e..00000000000000
--- a/x-pack/plugins/security/server/routes/api/v1/roles.js
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import _ from 'lodash';
-import Boom from 'boom';
-import { getClient } from '../../../../../../server/lib/get_client_shield';
-import { roleSchema } from '../../../lib/role_schema';
-import { wrapError } from '../../../lib/errors';
-import { routePreCheckLicense } from '../../../lib/route_pre_check_license';
-
-export function initRolesApi(server) {
- const callWithRequest = getClient(server).callWithRequest;
- const routePreCheckLicenseFn = routePreCheckLicense(server);
-
- server.route({
- method: 'GET',
- path: '/api/security/v1/roles',
- handler(request, reply) {
- return callWithRequest(request, 'shield.getRole').then(
- (response) => {
- const roles = _.map(response, (role, name) => _.assign(role, { name }));
-
- return reply(roles);
- },
- _.flow(wrapError, reply)
- );
- },
- config: {
- pre: [routePreCheckLicenseFn]
- }
- });
-
- server.route({
- method: 'GET',
- path: '/api/security/v1/roles/{name}',
- handler(request, reply) {
- const name = request.params.name;
- return callWithRequest(request, 'shield.getRole', { name }).then(
- (response) => {
- if (response[name]) return reply(_.assign(response[name], { name }));
- return reply(Boom.notFound());
- },
- _.flow(wrapError, reply));
- },
- config: {
- pre: [routePreCheckLicenseFn]
- }
- });
-
- server.route({
- method: 'POST',
- path: '/api/security/v1/roles/{name}',
- handler(request, reply) {
- const name = request.params.name;
- const body = _.omit(request.payload, 'name');
- return callWithRequest(request, 'shield.putRole', { name, body }).then(
- () => reply(request.payload),
- _.flow(wrapError, reply));
- },
- config: {
- validate: {
- payload: roleSchema
- },
- pre: [routePreCheckLicenseFn]
- }
- });
-
- server.route({
- method: 'DELETE',
- path: '/api/security/v1/roles/{name}',
- handler(request, reply) {
- const name = request.params.name;
- return callWithRequest(request, 'shield.deleteRole', { name }).then(
- () => reply().code(204),
- _.flow(wrapError, reply));
- },
- config: {
- pre: [routePreCheckLicenseFn]
- }
- });
-}
diff --git a/x-pack/test/api_integration/apis/security/index.js b/x-pack/test/api_integration/apis/security/index.js
index 08d020da311f48..ff3e5b33e832b7 100644
--- a/x-pack/test/api_integration/apis/security/index.js
+++ b/x-pack/test/api_integration/apis/security/index.js
@@ -7,5 +7,6 @@
export default function ({ loadTestFile }) {
describe('security', () => {
loadTestFile(require.resolve('./basic_login'));
+ loadTestFile(require.resolve('./roles'));
});
}
diff --git a/x-pack/test/api_integration/apis/security/roles.js b/x-pack/test/api_integration/apis/security/roles.js
new file mode 100644
index 00000000000000..f77ea88bde2c8a
--- /dev/null
+++ b/x-pack/test/api_integration/apis/security/roles.js
@@ -0,0 +1,221 @@
+/*
+ * 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 }) {
+ const es = getService('es');
+ const supertest = getService('supertest');
+
+ describe('Roles', () => {
+ describe('Create Role', () => {
+ it('should allow us to create an empty role', async () => {
+ await supertest.put('/api/security/role/empty_role')
+ .set('kbn-xsrf', 'xxx')
+ .send({})
+ .expect(204);
+ });
+
+ it('should create a role with kibana and elasticsearch privileges', async () => {
+ await supertest.put('/api/security/role/role_with_privileges')
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ metadata: {
+ foo: 'test-metadata',
+ },
+ elasticsearch: {
+ cluster: ['manage'],
+ indices: [
+ {
+ field_security: {
+ grant: ['*'],
+ except: [ 'geo.*' ]
+ },
+ names: ['logstash-*'],
+ privileges: ['read', 'view_index_metadata'],
+ query: `{ "match": { "geo.src": "CN" } }`,
+ },
+ ],
+ run_as: ['watcher_user'],
+ },
+ kibana: [
+ {
+ privileges: ['all'],
+ },
+ {
+ privileges: ['read'],
+ },
+ ],
+ })
+ .expect(204);
+
+ const role = await es.shield.getRole({ name: 'role_with_privileges' });
+ expect(role).to.eql({
+ role_with_privileges: {
+ cluster: ['manage'],
+ indices: [
+ {
+ names: ['logstash-*'],
+ privileges: ['read', 'view_index_metadata'],
+ field_security: {
+ grant: ['*'],
+ except: [ 'geo.*' ]
+ },
+ query: `{ "match": { "geo.src": "CN" } }`,
+ },
+ ],
+ applications: [
+ {
+ application: 'kibana-.kibana',
+ privileges: ['all'],
+ resources: ['*'],
+ },
+ {
+ application: 'kibana-.kibana',
+ privileges: ['read'],
+ resources: ['*'],
+ }
+ ],
+ run_as: ['watcher_user'],
+ metadata: {
+ foo: 'test-metadata',
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ }
+ });
+ });
+ });
+
+ describe('Update Role', () => {
+ it('should update a role with elasticsearch, kibana and other applications privileges', async () => {
+ await es.shield.putRole({
+ name: 'role_to_update',
+ body: {
+ cluster: ['monitor'],
+ indices: [
+ {
+ names: ['beats-*'],
+ privileges: ['write'],
+ field_security: {
+ grant: [ 'request.*' ],
+ except: [ 'response.*' ]
+ },
+ query: `{ "match": { "host.name": "localhost" } }`,
+ },
+ ],
+ applications: [
+ {
+ application: 'kibana-.kibana',
+ privileges: ['read'],
+ resources: ['*'],
+ },
+ {
+ application: 'logstash-default',
+ privileges: ['logstash-privilege'],
+ resources: ['*'],
+ },
+ ],
+ run_as: ['reporting_user'],
+ metadata: {
+ bar: 'old-metadata',
+ },
+ }
+ });
+
+ await supertest.put('/api/security/role/role_to_update')
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ metadata: {
+ foo: 'test-metadata',
+ },
+ elasticsearch: {
+ cluster: ['manage'],
+ indices: [
+ {
+ field_security: {
+ grant: ['*'],
+ except: [ 'geo.*' ]
+ },
+ names: ['logstash-*'],
+ privileges: ['read', 'view_index_metadata'],
+ query: `{ "match": { "geo.src": "CN" } }`,
+ },
+ ],
+ run_as: ['watcher_user'],
+ },
+ kibana: [
+ {
+ privileges: ['all'],
+ },
+ {
+ privileges: ['read'],
+ },
+ ],
+ })
+ .expect(204);
+
+ const role = await es.shield.getRole({ name: 'role_to_update' });
+ expect(role).to.eql({
+ role_to_update: {
+ cluster: ['manage'],
+ indices: [
+ {
+ names: ['logstash-*'],
+ privileges: ['read', 'view_index_metadata'],
+ field_security: {
+ grant: ['*'],
+ except: [ 'geo.*' ]
+ },
+ query: `{ "match": { "geo.src": "CN" } }`,
+ },
+ ],
+ applications: [
+ {
+ application: 'kibana-.kibana',
+ privileges: ['all'],
+ resources: ['*'],
+ },
+ {
+ application: 'kibana-.kibana',
+ privileges: ['read'],
+ resources: ['*'],
+ },
+ {
+ application: 'logstash-default',
+ privileges: ['logstash-privilege'],
+ resources: ['*'],
+ },
+ ],
+ run_as: ['watcher_user'],
+ metadata: {
+ foo: 'test-metadata',
+ },
+ transient_metadata: {
+ enabled: true,
+ },
+ }
+ });
+ });
+ });
+
+ describe('Delete Role', () => {
+ it('should delete the three roles we created', async () => {
+ await supertest.delete('/api/security/role/empty_role').set('kbn-xsrf', 'xxx').expect(204);
+ await supertest.delete('/api/security/role/role_with_privileges').set('kbn-xsrf', 'xxx').expect(204);
+ await supertest.delete('/api/security/role/role_to_update').set('kbn-xsrf', 'xxx').expect(204);
+
+ const emptyRole = await es.shield.getRole({ name: 'empty_role', ignore: [404] });
+ expect(emptyRole).to.eql({});
+ const roleWithPrivileges = await es.shield.getRole({ name: 'role_with_privileges', ignore: [404] });
+ expect(roleWithPrivileges).to.eql({});
+ const roleToUpdate = await es.shield.getRole({ name: 'role_to_update', ignore: [404] });
+ expect(roleToUpdate).to.eql({});
+ });
+ });
+ });
+}
diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js
index ac1d8225e294e8..3368562972cb8a 100644
--- a/x-pack/test/api_integration/config.js
+++ b/x-pack/test/api_integration/config.js
@@ -5,6 +5,7 @@
*/
import {
+ EsProvider,
EsSupertestWithoutAuthProvider,
SupertestWithoutAuthProvider,
UsageAPIProvider,
@@ -24,7 +25,7 @@ export default async function ({ readConfigFile }) {
esSupertest: kibanaAPITestsConfig.get('services.esSupertest'),
supertestWithoutAuth: SupertestWithoutAuthProvider,
esSupertestWithoutAuth: EsSupertestWithoutAuthProvider,
- es: kibanaCommonConfig.get('services.es'),
+ es: EsProvider,
esArchiver: kibanaCommonConfig.get('services.esArchiver'),
usageAPI: UsageAPIProvider,
kibanaServer: kibanaCommonConfig.get('services.kibanaServer'),
diff --git a/x-pack/test/api_integration/services/es.js b/x-pack/test/api_integration/services/es.js
new file mode 100644
index 00000000000000..420541fa7ec5f6
--- /dev/null
+++ b/x-pack/test/api_integration/services/es.js
@@ -0,0 +1,20 @@
+/*
+ * 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';
+
+export function EsProvider({ getService }) {
+ 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/api_integration/services/index.js b/x-pack/test/api_integration/services/index.js
index 9caab94932a1c1..2ae5ceef22496f 100644
--- a/x-pack/test/api_integration/services/index.js
+++ b/x-pack/test/api_integration/services/index.js
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+export { EsProvider } from './es';
export { EsSupertestWithoutAuthProvider } from './es_supertest_without_auth';
export { SupertestWithoutAuthProvider } from './supertest_without_auth';
export { UsageAPIProvider } from './usage_api';
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
index 2c11949d8bb473..013c0df3436019 100644
--- a/x-pack/test/rbac_api_integration/apis/saved_objects/index.js
+++ b/x-pack/test/rbac_api_integration/apis/saved_objects/index.js
@@ -6,65 +6,49 @@
import { AUTHENTICATION } from "./lib/authentication";
-const application = 'kibana-.kibana';
export default function ({ loadTestFile, getService }) {
const es = getService('es');
+ const supertest = getService('supertest');
describe('saved_objects', () => {
before(async () => {
- await es.shield.putRole({
- name: 'kibana_legacy_user',
- body: {
- cluster: [],
- index: [{
- names: ['.kibana'],
- privileges: ['manage', 'read', 'index', 'delete']
- }],
- applications: []
- }
- });
+ await supertest.put('/api/security/role/kibana_legacy_user')
+ .send({
+ elasticsearch: {
+ indices: [{
+ names: ['.kibana'],
+ privileges: ['manage', 'read', 'index', 'delete']
+ }]
+ }
+ });
- await es.shield.putRole({
- name: 'kibana_legacy_dashboard_only_user',
- body: {
- cluster: [],
- index: [{
- names: ['.kibana'],
- privileges: ['read', 'view_index_metadata']
- }],
- applications: []
- }
- });
+ await supertest.put('/api/security/role/kibana_legacy_dashboard_only_user')
+ .send({
+ elasticsearch: {
+ indices: [{
+ names: ['.kibana'],
+ privileges: ['read', 'view_index_metadata']
+ }]
+ }
+ });
- await es.shield.putRole({
- name: 'kibana_rbac_user',
- body: {
- cluster: [],
- index: [],
- applications: [
+ await supertest.put('/api/security/role/kibana_rbac_user')
+ .send({
+ kibana: [
{
- application,
- privileges: [ 'all' ],
- resources: [ '*' ]
+ privileges: ['all']
}
]
- }
- });
+ });
- await es.shield.putRole({
- name: 'kibana_rbac_dashboard_only_user',
- body: {
- cluster: [],
- index: [],
- applications: [
+ await supertest.put('/api/security/role/kibana_rbac_dashboard_only_user')
+ .send({
+ kibana: [
{
- application,
- privileges: [ 'read' ],
- resources: [ '*' ]
+ privileges: ['read']
}
]
- }
- });
+ });
await es.shield.putUser({
username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME,