Skip to content

Commit

Permalink
RBAC Legacy Fallback (elastic#19818)
Browse files Browse the repository at this point in the history
* Basic implementation, rather sloppy

* Cleaning stuff up a bit

* Beginning to write tests, going to refactor how we build the privileges

* Making the buildPrivilegesMap no longer return application name as the
main key

* Using real privileges since we need to use them for the legacy fallback

* Adding more tests

* Fixing spelling

* Fixing test description

* Fixing comment description

* Adding similar line breaks in the has privilege calls

* No more settings

* No more rbac enabled setting, we just do RBAC

* Using describe to cleanup the test cases

* Logging deprecations when using the legacy fallback

* Cleaning up a bit...

* Using the privilegeMap for the legacy fallback tests

* Now with even less duplication

* Removing stray `rbacEnabled` from angularjs
  • Loading branch information
kobelb authored and legrego committed Jun 22, 2018
1 parent f63c512 commit 4ca4c9e
Show file tree
Hide file tree
Showing 19 changed files with 961 additions and 250 deletions.
68 changes: 32 additions & 36 deletions x-pack/plugins/security/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export const security = (kibana) => new kibana.Plugin({
port: Joi.number().integer().min(0).max(65535)
}).default(),
rbac: Joi.object({
enabled: Joi.boolean().default(false),
application: Joi.string().default('kibana').regex(
/[a-zA-Z0-9-_]+/,
`may contain alphanumeric characters (a-z, A-Z, 0-9), underscores and hyphens`
Expand Down Expand Up @@ -81,7 +80,6 @@ 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'),
rbacApplication: config.get('xpack.security.rbac.application'),
};
}
Expand Down Expand Up @@ -116,50 +114,48 @@ export const security = (kibana) => new kibana.Plugin({
server.auth.strategy('session', 'login', 'required');

const auditLogger = new SecurityAuditLogger(server.config(), new AuditLogger(server, 'security'));
if (config.get('xpack.security.rbac.enabled')) {
const hasPrivilegesWithRequest = hasPrivilegesWithServer(server);
const { savedObjects } = server;
const hasPrivilegesWithRequest = hasPrivilegesWithServer(server);
const { savedObjects } = server;

savedObjects.setScopedSavedObjectsClientFactory(({
request,
index,
mappings,
onBeforeWrite
}) => {
const adminCluster = server.plugins.elasticsearch.getCluster('admin');

if (!xpackInfoFeature.getLicenseCheckResults().allowRbac) {
const { callWithRequest } = adminCluster;
const callCluster = (...args) => callWithRequest(request, ...args);

const repository = new savedObjects.SavedObjectsRepository({
index,
mappings,
onBeforeWrite,
callCluster,
});

return new savedObjects.SavedObjectsClient(repository);
}
savedObjects.setScopedSavedObjectsClientFactory(({
request,
index,
mappings,
onBeforeWrite
}) => {
const adminCluster = server.plugins.elasticsearch.getCluster('admin');

const hasPrivileges = hasPrivilegesWithRequest(request);
const { callWithInternalUser } = adminCluster;
if (!xpackInfoFeature.getLicenseCheckResults().allowRbac) {
const { callWithRequest } = adminCluster;
const callCluster = (...args) => callWithRequest(request, ...args);

const repository = new savedObjects.SavedObjectsRepository({
index,
mappings,
onBeforeWrite,
callCluster: callWithInternalUser
callCluster,
});

return new SecureSavedObjectsClient({
repository,
errors: savedObjects.SavedObjectsClient.errors,
hasPrivileges,
auditLogger,
});
return new savedObjects.SavedObjectsClient(repository);
}

const hasPrivileges = hasPrivilegesWithRequest(request);
const { callWithInternalUser } = adminCluster;

const repository = new savedObjects.SavedObjectsRepository({
index,
mappings,
onBeforeWrite,
callCluster: callWithInternalUser
});
}

return new SecureSavedObjectsClient({
repository,
errors: savedObjects.SavedObjectsClient.errors,
hasPrivileges,
auditLogger,
});
});

getUserProvider(server);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ <h1 class="kuiTitle">
</div>

<!-- Kibana custom privileges -->
<div class="kuiFormSection" ng-if="rbacEnabled">
<div class="kuiFormSection">
<label class="kuiFormLabel">
Kibana Privileges
</label>
Expand Down
3 changes: 1 addition & 2 deletions x-pack/plugins/security/public/views/management/edit_role.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
}
},
controllerAs: 'editRole',
controller($injector, $scope, rbacEnabled, rbacApplication) {
controller($injector, $scope, rbacApplication) {
const $route = $injector.get('$route');
const kbnUrl = $injector.get('kbnUrl');
const shieldPrivileges = $injector.get('shieldPrivileges');
Expand All @@ -132,7 +132,6 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
$scope.indexPatterns = $route.current.locals.indexPatterns;
$scope.privileges = shieldPrivileges;

$scope.rbacEnabled = rbacEnabled;
const kibanaApplicationPrivilege = $route.current.locals.kibanaApplicationPrivilege;
const role = $route.current.locals.role;
$scope.kibanaPrivileges = getKibanaPrivileges(kibanaApplicationPrivilege, role, rbacApplication);
Expand Down
134 changes: 108 additions & 26 deletions x-pack/plugins/security/server/lib/authorization/has_privileges.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,87 @@

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

const getMissingPrivileges = (resource, application, privilegeCheck) => {
const privileges = privilegeCheck.application[application][resource];
return Object.keys(privileges).filter(key => privileges[key] === false);
const hasApplicationPrivileges = async (callWithRequest, request, kibanaVersion, application, privileges) => {
const privilegeCheck = await callWithRequest(request, 'shield.hasPrivileges', {
body: {
applications: [{
application,
resources: [DEFAULT_RESOURCE],
privileges
}]
}
});

const hasPrivileges = privilegeCheck.application[application][DEFAULT_RESOURCE];

// We include the login action in 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 (!hasPrivileges[getVersionPrivilege(kibanaVersion)] && hasPrivileges[getLoginPrivilege()]) {
throw new Error('Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.');
}

return {
username: privilegeCheck.username,
hasAllRequested: privilegeCheck.has_all_requested,
privileges: hasPrivileges
};
};

const hasLegacyPrivileges = async (deprecationLogger, callWithRequest, request, kibanaVersion, application, kibanaIndex, privileges) => {
const privilegeCheck = await callWithRequest(request, 'shield.hasPrivileges', {
body: {
index: [{
names: [ kibanaIndex ],
privileges: ['read', 'index']
}]
}
});

const createPrivileges = (cb) => {
return privileges.reduce((acc, name) => {
acc[name] = cb(name);
return acc;
}, {});
};

const logDeprecation = () => {
deprecationLogger(
`Relying on implicit privileges determined from the index privileges is deprecated and will be removed in the next major version`
);
};

// if they have the index privilege, then we grant them all actions
if (privilegeCheck.index[kibanaIndex].index) {
logDeprecation();
const implicitPrivileges = createPrivileges(() => true);
return {
username: privilegeCheck.username,
hasAllRequested: true,
privileges: implicitPrivileges
};
}

// if they have the read privilege, then we only grant them the read actions
if (privilegeCheck.index[kibanaIndex].read) {
logDeprecation();
const privilegeMap = buildPrivilegeMap(application, kibanaVersion);
const implicitPrivileges = createPrivileges(name => privilegeMap.read.actions.includes(name));

return {
username: privilegeCheck.username,
hasAllRequested: Object.values(implicitPrivileges).every(x => x),
privileges: implicitPrivileges,
};
}

return {
username: privilegeCheck.username,
hasAllRequested: false,
privileges: createPrivileges(() => false)
};
};

export function hasPrivilegesWithServer(server) {
Expand All @@ -19,38 +95,44 @@ export function hasPrivilegesWithServer(server) {
const config = server.config();
const kibanaVersion = config.get('pkg.version');
const application = config.get('xpack.security.rbac.application');
const kibanaIndex = config.get('kibana.index');
const deprecationLogger = (msg) => server.log(['warning', 'deprecated', 'security'], msg);

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

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

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.');
const allPrivileges = [versionPrivilege, loginPrivilege, ...privileges];
let privilegesCheck = await hasApplicationPrivileges(
callWithRequest,
request,
kibanaVersion,
application,
allPrivileges
);

if (!privilegesCheck.privileges[loginPrivilege]) {
privilegesCheck = await hasLegacyPrivileges(
deprecationLogger,
callWithRequest,
request,
kibanaVersion,
application,
kibanaIndex,
allPrivileges
);
}

const success = privilegesCheck.hasAllRequested;

return {
success,
// We don't want to expose the version privilege to consumers, as it's an implementation detail only to detect version mismatch
missing: missingPrivileges.filter(p => p !== versionPrivilege),
username: privilegeCheck.username,
missing: Object.keys(privilegesCheck.privileges)
.filter(key => privilegesCheck.privileges[key] === false)
.filter(p => p !== versionPrivilege),
username: privilegesCheck.username,
};
};
};
Expand Down
Loading

0 comments on commit 4ca4c9e

Please sign in to comment.