Skip to content

Commit

Permalink
Authorization engines evaluate privileges for APIs (elastic#38219)
Browse files Browse the repository at this point in the history
This commit moves the evaluation of privileges from a few transport
actions into the authorization engine. The APIs are used by other
applications for making decisions and if a different authorization
engine is used that is not role based, we should still allow these APIs
to work. By moving this evaluation out of the transport action, the
transport actions no longer have a dependency on roles.
  • Loading branch information
jaymode authored Feb 4, 2019
1 parent 54d7b4c commit 34aa55a
Show file tree
Hide file tree
Showing 10 changed files with 967 additions and 851 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,35 @@

import org.elasticsearch.action.ActionListener;
import org.elasticsearch.cluster.metadata.AliasOrIndex;
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest;
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse;
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse.Indices;
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest;
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse;
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse.ResourcePrivileges;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
import org.elasticsearch.xpack.core.security.authz.ResolvedIndices;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges;
import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl;
import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl.IndexAccessControl;
import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions;
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor;
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege;
import org.elasticsearch.xpack.core.security.user.User;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
* A custom implementation of an authorization engine. This engine is extremely basic in that it
Expand Down Expand Up @@ -115,6 +129,89 @@ public void validateIndexPermissionsAreSubset(RequestInfo requestInfo, Authoriza
}
}

@Override
public void checkPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo,
HasPrivilegesRequest hasPrivilegesRequest,
Collection<ApplicationPrivilegeDescriptor> applicationPrivilegeDescriptors,
ActionListener<HasPrivilegesResponse> listener) {
if (isSuperuser(authentication.getUser())) {
listener.onResponse(getHasPrivilegesResponse(authentication, hasPrivilegesRequest, true));
} else {
listener.onResponse(getHasPrivilegesResponse(authentication, hasPrivilegesRequest, false));
}
}

@Override
public void getUserPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, GetUserPrivilegesRequest request,
ActionListener<GetUserPrivilegesResponse> listener) {
if (isSuperuser(authentication.getUser())) {
listener.onResponse(getUserPrivilegesResponse(true));
} else {
listener.onResponse(getUserPrivilegesResponse(false));
}
}

private HasPrivilegesResponse getHasPrivilegesResponse(Authentication authentication, HasPrivilegesRequest hasPrivilegesRequest,
boolean authorized) {
Map<String, Boolean> clusterPrivMap = new HashMap<>();
for (String clusterPriv : hasPrivilegesRequest.clusterPrivileges()) {
clusterPrivMap.put(clusterPriv, authorized);
}
final Map<String, ResourcePrivileges> indices = new LinkedHashMap<>();
for (IndicesPrivileges check : hasPrivilegesRequest.indexPrivileges()) {
for (String index : check.getIndices()) {
final Map<String, Boolean> privileges = new HashMap<>();
final HasPrivilegesResponse.ResourcePrivileges existing = indices.get(index);
if (existing != null) {
privileges.putAll(existing.getPrivileges());
}
for (String privilege : check.getPrivileges()) {
privileges.put(privilege, authorized);
}
indices.put(index, new ResourcePrivileges(index, privileges));
}
}
final Map<String, Collection<ResourcePrivileges>> privilegesByApplication = new HashMap<>();
Set<String> applicationNames = Arrays.stream(hasPrivilegesRequest.applicationPrivileges())
.map(RoleDescriptor.ApplicationResourcePrivileges::getApplication)
.collect(Collectors.toSet());
for (String applicationName : applicationNames) {
final Map<String, HasPrivilegesResponse.ResourcePrivileges> appPrivilegesByResource = new LinkedHashMap<>();
for (RoleDescriptor.ApplicationResourcePrivileges p : hasPrivilegesRequest.applicationPrivileges()) {
if (applicationName.equals(p.getApplication())) {
for (String resource : p.getResources()) {
final Map<String, Boolean> privileges = new HashMap<>();
final HasPrivilegesResponse.ResourcePrivileges existing = appPrivilegesByResource.get(resource);
if (existing != null) {
privileges.putAll(existing.getPrivileges());
}
for (String privilege : p.getPrivileges()) {
privileges.put(privilege, authorized);
}
appPrivilegesByResource.put(resource, new HasPrivilegesResponse.ResourcePrivileges(resource, privileges));
}
}
}
privilegesByApplication.put(applicationName, appPrivilegesByResource.values());
}
return new HasPrivilegesResponse(authentication.getUser().principal(), authorized, clusterPrivMap, indices.values(),
privilegesByApplication);
}

private GetUserPrivilegesResponse getUserPrivilegesResponse(boolean isSuperuser) {
final Set<String> cluster = isSuperuser ? Collections.singleton("ALL") : Collections.emptySet();
final Set<ConditionalClusterPrivilege> conditionalCluster = Collections.emptySet();
final Set<GetUserPrivilegesResponse.Indices> indices = isSuperuser ? Collections.singleton(new Indices(Collections.singleton("*"),
Collections.singleton("*"), Collections.emptySet(), Collections.emptySet(), true)) : Collections.emptySet();

final Set<RoleDescriptor.ApplicationResourcePrivileges> application = isSuperuser ?
Collections.singleton(
RoleDescriptor.ApplicationResourcePrivileges.builder().application("*").privileges("*").resources("*").build()) :
Collections.emptySet();
final Set<String> runAs = isSuperuser ? Collections.singleton("*") : Collections.emptySet();
return new GetUserPrivilegesResponse(cluster, conditionalCluster, indices, application, runAs);
}

public static class CustomAuthorizationInfo implements AuthorizationInfo {

private final String[] roles;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.cluster.metadata.AliasOrIndex;
import org.elasticsearch.transport.TransportRequest;
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest;
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse;
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest;
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl;
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor;
import org.elasticsearch.xpack.core.security.user.User;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -141,6 +147,7 @@ void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo authorizati
void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizationInfo,
Map<String, AliasOrIndex> aliasAndIndexLookup, ActionListener<List<String>> listener);


/**
* Asynchronously checks that the permissions a user would have for a given list of names do
* not exceed their permissions for a given name. This is used to ensure that a user cannot
Expand All @@ -161,6 +168,35 @@ void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizat
void validateIndexPermissionsAreSubset(RequestInfo requestInfo, AuthorizationInfo authorizationInfo,
Map<String, List<String>> indexNameToNewNames, ActionListener<AuthorizationResult> listener);

/**
* Checks the current user's privileges against those that being requested to check in the
* request. This provides a way for an application to ask if a user has permission to perform
* an action or if they have permissions to an application resource.
*
* @param authentication the authentication that is associated with this request
* @param authorizationInfo information needed from authorization that was previously retrieved
* from {@link #resolveAuthorizationInfo(RequestInfo, ActionListener)}
* @param hasPrivilegesRequest the request that contains the privileges to check for the user
* @param applicationPrivilegeDescriptors a collection of application privilege descriptors
* @param listener the listener to be notified of the has privileges response
*/
void checkPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, HasPrivilegesRequest hasPrivilegesRequest,
Collection<ApplicationPrivilegeDescriptor> applicationPrivilegeDescriptors,
ActionListener<HasPrivilegesResponse> listener);

/**
* Retrieve's the current user's privileges in a standard format that can be rendered via an
* API for an application to understand the privileges that the current user has.
*
* @param authentication the authentication that is associated with this request
* @param authorizationInfo information needed from authorization that was previously retrieved
* from {@link #resolveAuthorizationInfo(RequestInfo, ActionListener)}
* @param request the request for retrieving the user's privileges
* @param listener the listener to be notified of the has privileges response
*/
void getUserPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, GetUserPrivilegesRequest request,
ActionListener<GetUserPrivilegesResponse> listener);

/**
* Interface for objects that contains the information needed to authorize a request
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,132 +5,47 @@
*/
package org.elasticsearch.xpack.security.action.user;

import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.lucene.util.automaton.Operations;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.HandledTransportAction;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesAction;
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest;
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache;
import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition;
import org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission;
import org.elasticsearch.xpack.core.security.authz.permission.Role;
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege;
import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege;
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege;
import org.elasticsearch.xpack.core.security.authz.privilege.Privilege;
import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;

import static org.elasticsearch.common.Strings.arrayToCommaDelimitedString;
import org.elasticsearch.xpack.security.authz.AuthorizationService;

/**
* Transport action for {@link GetUserPrivilegesAction}
*/
public class TransportGetUserPrivilegesAction extends HandledTransportAction<GetUserPrivilegesRequest, GetUserPrivilegesResponse> {

private final ThreadPool threadPool;
private final CompositeRolesStore rolesStore;
private final AuthorizationService authorizationService;

@Inject
public TransportGetUserPrivilegesAction(ThreadPool threadPool, TransportService transportService,
ActionFilters actionFilters, CompositeRolesStore rolesStore) {
ActionFilters actionFilters, AuthorizationService authorizationService) {
super(GetUserPrivilegesAction.NAME, transportService, actionFilters, GetUserPrivilegesRequest::new);
this.threadPool = threadPool;
this.rolesStore = rolesStore;
this.authorizationService = authorizationService;
}

@Override
protected void doExecute(Task task, GetUserPrivilegesRequest request, ActionListener<GetUserPrivilegesResponse> listener) {
final String username = request.username();

final User user = Authentication.getAuthentication(threadPool.getThreadContext()).getUser();
final Authentication authentication = Authentication.getAuthentication(threadPool.getThreadContext());
final User user = authentication.getUser();
if (user.principal().equals(username) == false) {
listener.onFailure(new IllegalArgumentException("users may only list the privileges of their own account"));
return;
}

// FIXME reuse field permissions cache!
rolesStore.getRoles(user, new FieldPermissionsCache(Settings.EMPTY), ActionListener.wrap(
role -> listener.onResponse(buildResponseObject(role)),
listener::onFailure));
}

// package protected for testing
GetUserPrivilegesResponse buildResponseObject(Role userRole) {
logger.trace(() -> new ParameterizedMessage("List privileges for role [{}]", arrayToCommaDelimitedString(userRole.names())));

// We use sorted sets for Strings because they will typically be small, and having a predictable order allows for simpler testing
final Set<String> cluster = new TreeSet<>();
// But we don't have a meaningful ordering for objects like ConditionalClusterPrivilege, so the tests work with "random" ordering
final Set<ConditionalClusterPrivilege> conditionalCluster = new HashSet<>();
for (Tuple<ClusterPrivilege, ConditionalClusterPrivilege> tup : userRole.cluster().privileges()) {
if (tup.v2() == null) {
if (ClusterPrivilege.NONE.equals(tup.v1()) == false) {
cluster.addAll(tup.v1().name());
}
} else {
conditionalCluster.add(tup.v2());
}
}

final Set<GetUserPrivilegesResponse.Indices> indices = new LinkedHashSet<>();
for (IndicesPermission.Group group : userRole.indices().groups()) {
final Set<BytesReference> queries = group.getQuery() == null ? Collections.emptySet() : group.getQuery();
final Set<FieldPermissionsDefinition.FieldGrantExcludeGroup> fieldSecurity = group.getFieldPermissions().hasFieldLevelSecurity()
? group.getFieldPermissions().getFieldPermissionsDefinition().getFieldGrantExcludeGroups() : Collections.emptySet();
indices.add(new GetUserPrivilegesResponse.Indices(
Arrays.asList(group.indices()),
group.privilege().name(),
fieldSecurity,
queries,
group.allowRestrictedIndices()
));
}

final Set<RoleDescriptor.ApplicationResourcePrivileges> application = new LinkedHashSet<>();
for (String applicationName : userRole.application().getApplicationNames()) {
for (ApplicationPrivilege privilege : userRole.application().getPrivileges(applicationName)) {
final Set<String> resources = userRole.application().getResourcePatterns(privilege);
if (resources.isEmpty()) {
logger.trace("No resources defined in application privilege {}", privilege);
} else {
application.add(RoleDescriptor.ApplicationResourcePrivileges.builder()
.application(applicationName)
.privileges(privilege.name())
.resources(resources)
.build());
}
}
}

final Privilege runAsPrivilege = userRole.runAs().getPrivilege();
final Set<String> runAs;
if (Operations.isEmpty(runAsPrivilege.getAutomaton())) {
runAs = Collections.emptySet();
} else {
runAs = runAsPrivilege.name();
}

return new GetUserPrivilegesResponse(cluster, conditionalCluster, indices, application, runAs);
authorizationService.retrieveUserPrivileges(authentication, request, listener);
}

}
Loading

0 comments on commit 34aa55a

Please sign in to comment.