Skip to content

Commit

Permalink
Beginning to use the ES APIs to insert/check privileges (elastic#18645)
Browse files Browse the repository at this point in the history
* Beginning to use the ES APIs to insert/check privileges

* Removing todo comment, I think we're good with the current check

* Adding ability to edit kibana application privileges

* Introducing DEFAULT_RESOURCE constant

* Removing unused arguments when performing saved objects auth check

* Performing bulkCreate auth more efficiently

* Throwing error in SavedObjectClient.find if type isn't provided

* Fixing Reporting and removing errant console.log

* Introducing a separate hasPrivileges "service"

* Adding tests and fleshing out the has privileges "service"

* Fixing error message

* You can now edit whatever roles you want

* We're gonna throw the find error in another PR

* Changing conflicting version detection to work when user has no
application privileges

* Throwing correct error when user is forbidden

* Removing unused interceptor

* Adding warning if they're editing a role with application privileges we
can't edit

* Fixing filter...

* Beginning to only update privileges when they need to be

* More tests

* One more test...

* Restricting the rbac application name that can be chosen

* Removing DEFAULT_RESOURCE check

* Supporting 1024 characters for the role name

* Renaming some variables, fixing issue with role w/ no kibana privileges

* Throwing decorated general error when appropriate

* Fixing test description

* Dedent does nothing...

* Renaming some functions
  • Loading branch information
kobelb authored and legrego committed Jun 22, 2018
1 parent f396766 commit 7a33bac
Show file tree
Hide file tree
Showing 22 changed files with 708 additions and 239 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ function executeJobFn(server) {

const savedObjects = server.savedObjects;
const savedObjectsClient = savedObjects.getScopedSavedObjectsClient(fakeRequest);

const uiSettings = server.uiSettingsServiceFactory({
savedObjectsClient
});
Expand Down
7 changes: 7 additions & 0 deletions x-pack/plugins/security/common/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export const DEFAULT_RESOURCE = 'default';
16 changes: 12 additions & 4 deletions x-pack/plugins/security/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ import { initAuthenticator } from './server/lib/authentication/authenticator';
import { mirrorStatusAndInitialize } from './server/lib/mirror_status_and_initialize';
import { secureSavedObjectsClientWrapper } from './server/lib/saved_objects_client/saved_objects_client_wrapper';
import { secureSavedObjectsClientOptionsBuilder } from './server/lib/saved_objects_client/secure_options_builder';
import { registerPrivilegesWithCluster } from './server/lib/privileges/privilege_action_registry';
import { registerPrivilegesWithCluster } from './server/lib/privileges';
import { createDefaultRoles } from './server/lib/authorization/create_default_roles';
import { initPrivilegesApi } from './server/routes/api/v1/privileges';
import { hasPrivilegesWithServer } from './server/lib/authorization/has_privileges';

export const security = (kibana) => new kibana.Plugin({
id: 'security',
Expand All @@ -45,7 +46,10 @@ export const security = (kibana) => new kibana.Plugin({
rbac: Joi.object({
enabled: Joi.boolean().default(false),
createDefaultRoles: Joi.boolean().default(true),
application: Joi.string().default('kibana'),
application: Joi.string().default('kibana').regex(
/[a-zA-Z0-9-_]+/,
`may contain alphanumeric characters (a-z, A-Z, 0-9), underscores and hyphens`
),
}).default(),
}).default();
},
Expand Down Expand Up @@ -75,7 +79,8 @@ export const security = (kibana) => new kibana.Plugin({
return {
secureCookies: config.get('xpack.security.secureCookies'),
sessionTimeout: config.get('xpack.security.sessionTimeout'),
rbacEnabled: config.get('xpack.security.rbac.enabled')
rbacEnabled: config.get('xpack.security.rbac.enabled'),
rbacApplication: config.get('xpack.security.rbac.application'),
};
}
},
Expand Down Expand Up @@ -107,8 +112,11 @@ export const security = (kibana) => new kibana.Plugin({
server.auth.strategy('session', 'login', 'required');

if (config.get('xpack.security.rbac.enabled')) {
const hasPrivilegesWithRequest = hasPrivilegesWithServer(server);
const savedObjectsClientProvider = server.getSavedObjectsClientProvider();
savedObjectsClientProvider.addClientOptionBuilder((options) => secureSavedObjectsClientOptionsBuilder(server, options));
savedObjectsClientProvider.addClientOptionBuilder(options =>
secureSavedObjectsClientOptionsBuilder(server, hasPrivilegesWithRequest, options)
);
savedObjectsClientProvider.addClientWrapper(secureSavedObjectsClientWrapper);
}

Expand Down
29 changes: 19 additions & 10 deletions x-pack/plugins/security/public/views/management/edit_role.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<kbn-management-app section="security" omit-breadcrumb-pages="['edit']">
<!-- This content gets injected below the localNav. -->
<div class="kuiViewContent kuiViewContent--constrainedWidth kuiViewContentItem">

<!-- Subheader -->
<div class="kuiBar kuiVerticalRhythm">

<div class="kuiBarSection">
<!-- Title -->
<h1 class="kuiTitle">
Expand Down Expand Up @@ -39,6 +41,18 @@ <h1 class="kuiTitle">
</div>
</div>

<div class="kuiBar kuiVerticalRhythm" ng-if="otherApplications.length > 0">
<div class="kuiInfoPanel kuiInfoPanel--warning">
<div class="kuiInfoPanelHeader">
<span class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--warning fa-warning"></span>
<span class="kuiInfoPanelHeader__title">
This role contains application privileges for the {{ otherApplications.join(', ') }} application(s) that can't be edited.
If they are for other instances of Kibana, you must manage those privileges on that Kibana.
</span>
</div>
</div>
</div>

<!-- Form -->
<form name="form" novalidate class="kuiVerticalRhythm">
<!-- Name -->
Expand All @@ -56,7 +70,7 @@ <h1 class="kuiTitle">
ng-model="role.name"
required
pattern="[a-zA-Z_][a-zA-Z0-9_@\-\$\.]*"
maxlength="30"
maxlength="1024"
data-test-subj="roleFormNameInput"
/>

Expand Down Expand Up @@ -102,20 +116,15 @@ <h1 class="kuiTitle">
Kibana Privileges
</label>

<div class="kuiInputNote kuiInputNote--warning" ng-if="role.hasUnsupportedCustomPrivileges">
Changes to this section are not supported: this role contains application privileges that do not belong to this instance of Kibana.
</div>

<div ng-repeat="privilege in kibanaPrivileges track by privilege.name">
<div ng-repeat="(key, value) in kibanaPrivileges">
<label>
<input
class="kuiCheckBox"
type="checkbox"
ng-checked="includesPermission(role, privilege)"
ng-click="togglePermission(role, privilege)"
ng-disabled="role.metadata._reserved || !isRoleEnabled(role) || role.hasUnsupportedCustomPrivileges"
ng-model="kibanaPrivileges[key]"
ng-disabled="role.metadata._reserved || !isRoleEnabled(role)"
/>
<span class="kuiOptionLabel">{{privilege.metadata.displayName}}</span>
<span class="kuiOptionLabel">{{key}}</span>
</label>
</div>
</div>
Expand Down
71 changes: 62 additions & 9 deletions x-pack/plugins/security/public/views/management/edit_role.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,57 @@ 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 { DEFAULT_RESOURCE } from '../../../common/constants';

const getKibanaPrivileges = (kibanaApplicationPrivilege, role, application) => {
const kibanaPrivileges = kibanaApplicationPrivilege.reduce((acc, p) => {
acc[p.name] = false;
return acc;
}, {});

if (!role.applications || role.applications.length === 0) {
return kibanaPrivileges;
}

const applications = role.applications.filter(x => x.application === application);

const assigned = _.uniq(_.flatten(_.pluck(applications, 'privileges')));
assigned.forEach(a => {
kibanaPrivileges[a] = true;
});

return kibanaPrivileges;
};

const setApplicationPrivileges = (kibanaPrivileges, role, application) => {
if (!role.applications) {
role.applications = [];
}

// we first remove the matching application entries
role.applications = role.applications.filter(x => {
return x.application !== application;
});

const privileges = Object.keys(kibanaPrivileges).filter(key => kibanaPrivileges[key]);

// if we still have them, put the application entry back
if (privileges.length > 0) {
role.applications = [...role.applications, {
application,
privileges,
resources: [ DEFAULT_RESOURCE ]
}];
}
};

const getOtherApplications = (kibanaPrivileges, role, application) => {
if (!role.applications || role.applications.length === 0) {
return [];
}

return role.applications.map(x => x.application).filter(x => x !== application);
};

routes.when(`${EDIT_ROLES_PATH}/:name?`, {
template,
Expand Down Expand Up @@ -48,7 +99,7 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
applications: []
});
},
kibanaPrivileges(ApplicationPrivilege, kbnUrl, Promise, Private) {
kibanaApplicationPrivilege(ApplicationPrivilege, kbnUrl, Promise, Private) {
return ApplicationPrivilege.query().$promise
.catch(checkLicenseError(kbnUrl, Promise, Private));
},
Expand All @@ -64,7 +115,7 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
}
},
controllerAs: 'editRole',
controller($injector, $scope, rbacEnabled) {
controller($injector, $scope, rbacEnabled, rbacApplication) {
const $route = $injector.get('$route');
const kbnUrl = $injector.get('kbnUrl');
const shieldPrivileges = $injector.get('shieldPrivileges');
Expand All @@ -77,8 +128,13 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
$scope.users = $route.current.locals.users;
$scope.indexPatterns = $route.current.locals.indexPatterns;
$scope.privileges = shieldPrivileges;
$scope.kibanaPrivileges = $route.current.locals.kibanaPrivileges;

$scope.rbacEnabled = rbacEnabled;
const kibanaApplicationPrivilege = $route.current.locals.kibanaApplicationPrivilege;
const role = $route.current.locals.role;
$scope.kibanaPrivileges = getKibanaPrivileges(kibanaApplicationPrivilege, role, rbacApplication);
$scope.otherApplications = getOtherApplications(kibanaApplicationPrivilege, role, rbacApplication);

$scope.rolesHref = `#${ROLES_PATH}`;

this.isNewRole = $route.current.params.name == null;
Expand All @@ -103,6 +159,9 @@ 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);

setApplicationPrivileges($scope.kibanaPrivileges, role, rbacApplication);

return role.$save()
.then(() => toastNotifications.addSuccess('Updated role'))
.then($scope.goToRoleList)
Expand Down Expand Up @@ -156,12 +215,6 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
}
};

$scope.hasPermission = (role, permission) => {
// TODO(legrego): faking until ES is implemented
const rolePermissions = role.applications || [];
return rolePermissions.find(rolePermission => permission.name === rolePermission.name);
};

$scope.union = _.flow(_.union, _.compact);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`throws error if missing version privilege and has login privilege 1`] = `"Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user."`;
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
* Licensed under the Elastic License; you may not use this file except in compliance with the Elastic License. */

import { getClient } from '../../../../../server/lib/get_client_shield';
import { DEFAULT_RESOURCE } from '../../../common/constants';


const createRoleIfDoesntExist = async (callCluster, name) => {
const createRoleIfDoesntExist = async (callCluster, { name, application, privilege }) => {
try {
await callCluster('shield.getRole', { name });
} catch (err) {
Expand All @@ -23,7 +24,13 @@ const createRoleIfDoesntExist = async (callCluster, name) => {
body: {
cluster: [],
index: [],
// application: [ { "privileges": [ "kibana:all" ], "resources": [ "*" ] } ]
applications: [
{
application,
privileges: [ privilege ],
resources: [ DEFAULT_RESOURCE ]
}
]
}
});
}
Expand All @@ -40,8 +47,17 @@ export async function createDefaultRoles(server) {

const callCluster = getClient(server).callWithInternalUser;

const createKibanaUserRole = createRoleIfDoesntExist(callCluster, `${application}_rbac_user`);
const createKibanaDashboardOnlyRole = createRoleIfDoesntExist(callCluster, `${application}_rbac_dashboard_only_user`);
const createKibanaUserRole = createRoleIfDoesntExist(callCluster, {
name: `${application}_rbac_user`,
application,
privilege: 'all'
});

const createKibanaDashboardOnlyRole = createRoleIfDoesntExist(callCluster, {
name: `${application}_rbac_dashboard_only_user`,
application,
privilege: 'read'
});

await Promise.all([createKibanaUserRole, createKibanaDashboardOnlyRole]);
}
55 changes: 55 additions & 0 deletions x-pack/plugins/security/server/lib/authorization/has_privileges.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { getClient } from '../../../../../server/lib/get_client_shield';
import { DEFAULT_RESOURCE } from '../../../common/constants';
import { getVersionPrivilege, getLoginPrivilege } from '../privileges';

const getMissingPrivileges = (resource, application, privilegeCheck) => {
const privileges = privilegeCheck.application[application][resource];
return Object.keys(privileges).filter(key => privileges[key] === false);
};

export function hasPrivilegesWithServer(server) {
const callWithRequest = getClient(server).callWithRequest;

const config = server.config();
const kibanaVersion = config.get('pkg.version');
const application = config.get('xpack.security.rbac.application');

return function hasPrivilegesWithRequest(request) {
return async function hasPrivileges(privileges) {

const versionPrivilege = getVersionPrivilege(kibanaVersion);
const loginPrivilege = getLoginPrivilege();

const privilegeCheck = await callWithRequest(request, 'shield.hasPrivileges', {
body: {
applications: [{
application,
resources: [DEFAULT_RESOURCE],
privileges: [versionPrivilege, loginPrivilege, ...privileges]
}]
}
});

const success = privilegeCheck.has_all_requested;
const missingPrivileges = getMissingPrivileges(DEFAULT_RESOURCE, application, privilegeCheck);

// We include the login privilege on all privileges, so the existence of it and not the version privilege
// lets us know that we're running in an incorrect configuration. Without the login privilege check, we wouldn't
// know whether the user just wasn't authorized for this instance of Kibana in general
if (missingPrivileges.includes(versionPrivilege) && !missingPrivileges.includes(loginPrivilege)) {
throw new Error('Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.');
}

return {
success,
missing: missingPrivileges
};
};
};
}
Loading

0 comments on commit 7a33bac

Please sign in to comment.