diff --git a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java index c16e21b984d6f..da8bcf8b4ef44 100644 --- a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java +++ b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java @@ -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 @@ -115,6 +129,89 @@ public void validateIndexPermissionsAreSubset(RequestInfo requestInfo, Authoriza } } + @Override + public void checkPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, + HasPrivilegesRequest hasPrivilegesRequest, + Collection applicationPrivilegeDescriptors, + ActionListener 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 listener) { + if (isSuperuser(authentication.getUser())) { + listener.onResponse(getUserPrivilegesResponse(true)); + } else { + listener.onResponse(getUserPrivilegesResponse(false)); + } + } + + private HasPrivilegesResponse getHasPrivilegesResponse(Authentication authentication, HasPrivilegesRequest hasPrivilegesRequest, + boolean authorized) { + Map clusterPrivMap = new HashMap<>(); + for (String clusterPriv : hasPrivilegesRequest.clusterPrivileges()) { + clusterPrivMap.put(clusterPriv, authorized); + } + final Map indices = new LinkedHashMap<>(); + for (IndicesPrivileges check : hasPrivilegesRequest.indexPrivileges()) { + for (String index : check.getIndices()) { + final Map 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> privilegesByApplication = new HashMap<>(); + Set applicationNames = Arrays.stream(hasPrivilegesRequest.applicationPrivileges()) + .map(RoleDescriptor.ApplicationResourcePrivileges::getApplication) + .collect(Collectors.toSet()); + for (String applicationName : applicationNames) { + final Map appPrivilegesByResource = new LinkedHashMap<>(); + for (RoleDescriptor.ApplicationResourcePrivileges p : hasPrivilegesRequest.applicationPrivileges()) { + if (applicationName.equals(p.getApplication())) { + for (String resource : p.getResources()) { + final Map 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 cluster = isSuperuser ? Collections.singleton("ALL") : Collections.emptySet(); + final Set conditionalCluster = Collections.emptySet(); + final Set indices = isSuperuser ? Collections.singleton(new Indices(Collections.singleton("*"), + Collections.singleton("*"), Collections.emptySet(), Collections.emptySet(), true)) : Collections.emptySet(); + + final Set application = isSuperuser ? + Collections.singleton( + RoleDescriptor.ApplicationResourcePrivileges.builder().application("*").privileges("*").resources("*").build()) : + Collections.emptySet(); + final Set runAs = isSuperuser ? Collections.singleton("*") : Collections.emptySet(); + return new GetUserPrivilegesResponse(cluster, conditionalCluster, indices, application, runAs); + } + public static class CustomAuthorizationInfo implements AuthorizationInfo { private final String[] roles; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index 51e1f409771d8..19fb501f5843b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -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; @@ -141,6 +147,7 @@ void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo authorizati void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, Map aliasAndIndexLookup, ActionListener> 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 @@ -161,6 +168,35 @@ void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizat void validateIndexPermissionsAreSubset(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, Map> indexNameToNewNames, ActionListener 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 applicationPrivilegeDescriptors, + ActionListener 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 listener); + /** * Interface for objects that contains the information needed to authorize a request */ diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesAction.java index e85b3870c95f3..00232033b89ef 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesAction.java @@ -5,15 +5,10 @@ */ 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; @@ -21,26 +16,8 @@ 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} @@ -48,89 +25,27 @@ public class TransportGetUserPrivilegesAction extends HandledTransportAction { 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 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 cluster = new TreeSet<>(); - // But we don't have a meaningful ordering for objects like ConditionalClusterPrivilege, so the tests work with "random" ordering - final Set conditionalCluster = new HashSet<>(); - for (Tuple 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 indices = new LinkedHashSet<>(); - for (IndicesPermission.Group group : userRole.indices().groups()) { - final Set queries = group.getQuery() == null ? Collections.emptySet() : group.getQuery(); - final Set 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 application = new LinkedHashSet<>(); - for (String applicationName : userRole.application().getApplicationNames()) { - for (ApplicationPrivilege privilege : userRole.application().getPrivileges(applicationName)) { - final Set 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 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); } - } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java index deb3a37ee2e9d..ae400172bf110 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java @@ -5,15 +5,10 @@ */ package org.elasticsearch.xpack.security.action.user; -import org.apache.logging.log4j.message.ParameterizedMessage; -import org.apache.lucene.util.automaton.Automaton; -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.Strings; 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; @@ -22,27 +17,13 @@ import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse; 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.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.ApplicationPrivilegeDescriptor; -import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; -import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.user.User; -import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; +import org.elasticsearch.xpack.security.authz.AuthorizationService; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; -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.stream.Collectors; @@ -53,16 +34,16 @@ public class TransportHasPrivilegesAction extends HandledTransportAction { private final ThreadPool threadPool; - private final CompositeRolesStore rolesStore; + private final AuthorizationService authorizationService; private final NativePrivilegeStore privilegeStore; @Inject public TransportHasPrivilegesAction(ThreadPool threadPool, TransportService transportService, - ActionFilters actionFilters, CompositeRolesStore rolesStore, + ActionFilters actionFilters, AuthorizationService authorizationService, NativePrivilegeStore privilegeStore) { super(HasPrivilegesAction.NAME, transportService, actionFilters, HasPrivilegesRequest::new); this.threadPool = threadPool; - this.rolesStore = rolesStore; + this.authorizationService = authorizationService; this.privilegeStore = privilegeStore; } @@ -70,17 +51,15 @@ public TransportHasPrivilegesAction(ThreadPool threadPool, TransportService tran protected void doExecute(Task task, HasPrivilegesRequest request, ActionListener 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 check the privileges of their own account")); return; } - // FIXME - rolesStore.getRoles(user, new FieldPermissionsCache(Settings.EMPTY), ActionListener.wrap( - role -> resolveApplicationPrivileges(request, ActionListener.wrap( - applicationPrivilegeLookup -> checkPrivileges(request, role, applicationPrivilegeLookup, listener), - listener::onFailure)), + resolveApplicationPrivileges(request, ActionListener.wrap(applicationPrivilegeDescriptors -> + authorizationService.checkPrivileges(authentication, request, applicationPrivilegeDescriptors, listener), listener::onFailure)); } @@ -90,125 +69,9 @@ private void resolveApplicationPrivileges(HasPrivilegesRequest request, privilegeStore.getPrivileges(applications, null, listener); } - private Set getApplicationNames(HasPrivilegesRequest request) { + public static Set getApplicationNames(HasPrivilegesRequest request) { return Arrays.stream(request.applicationPrivileges()) .map(RoleDescriptor.ApplicationResourcePrivileges::getApplication) .collect(Collectors.toSet()); } - - private void checkPrivileges(HasPrivilegesRequest request, Role userRole, - Collection applicationPrivileges, - ActionListener listener) { - logger.trace(() -> new ParameterizedMessage("Check whether role [{}] has privileges cluster=[{}] index=[{}] application=[{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), - Strings.arrayToCommaDelimitedString(request.clusterPrivileges()), - Strings.arrayToCommaDelimitedString(request.indexPrivileges()), - Strings.arrayToCommaDelimitedString(request.applicationPrivileges()) - )); - - Map cluster = new HashMap<>(); - for (String checkAction : request.clusterPrivileges()) { - final ClusterPrivilege checkPrivilege = ClusterPrivilege.get(Collections.singleton(checkAction)); - final ClusterPrivilege rolePrivilege = userRole.cluster().privilege(); - cluster.put(checkAction, testPrivilege(checkPrivilege, rolePrivilege.getAutomaton())); - } - boolean allMatch = cluster.values().stream().allMatch(Boolean::booleanValue); - - final Map predicateCache = new HashMap<>(); - - final Map indices = new LinkedHashMap<>(); - for (RoleDescriptor.IndicesPrivileges check : request.indexPrivileges()) { - for (String index : check.getIndices()) { - final Map privileges = new HashMap<>(); - final HasPrivilegesResponse.ResourcePrivileges existing = indices.get(index); - if (existing != null) { - privileges.putAll(existing.getPrivileges()); - } - for (String privilege : check.getPrivileges()) { - if (testIndexMatch(index, check.allowRestrictedIndices(), privilege, userRole, predicateCache)) { - logger.debug(() -> new ParameterizedMessage("Role [{}] has [{}] on index [{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), privilege, index)); - privileges.put(privilege, true); - } else { - logger.debug(() -> new ParameterizedMessage("Role [{}] does not have [{}] on index [{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), privilege, index)); - privileges.put(privilege, false); - allMatch = false; - } - } - indices.put(index, new HasPrivilegesResponse.ResourcePrivileges(index, privileges)); - } - } - - final Map> privilegesByApplication = new HashMap<>(); - for (String applicationName : getApplicationNames(request)) { - logger.debug("Checking privileges for application {}", applicationName); - final Map appPrivilegesByResource = new LinkedHashMap<>(); - for (RoleDescriptor.ApplicationResourcePrivileges p : request.applicationPrivileges()) { - if (applicationName.equals(p.getApplication())) { - for (String resource : p.getResources()) { - final Map privileges = new HashMap<>(); - final HasPrivilegesResponse.ResourcePrivileges existing = appPrivilegesByResource.get(resource); - if (existing != null) { - privileges.putAll(existing.getPrivileges()); - } - for (String privilege : p.getPrivileges()) { - if (testResourceMatch(applicationName, resource, privilege, userRole, applicationPrivileges)) { - logger.debug(() -> new ParameterizedMessage("Role [{}] has [{} {}] on resource [{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), applicationName, privilege, resource)); - privileges.put(privilege, true); - } else { - logger.debug(() -> new ParameterizedMessage("Role [{}] does not have [{} {}] on resource [{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), applicationName, privilege, resource)); - privileges.put(privilege, false); - allMatch = false; - } - } - appPrivilegesByResource.put(resource, new HasPrivilegesResponse.ResourcePrivileges(resource, privileges)); - } - } - } - privilegesByApplication.put(applicationName, appPrivilegesByResource.values()); - } - - listener.onResponse(new HasPrivilegesResponse(request.username(), allMatch, cluster, indices.values(), privilegesByApplication)); - } - - private boolean testIndexMatch(String checkIndexPattern, boolean allowRestrictedIndices, String checkPrivilegeName, Role userRole, - Map predicateCache) { - final IndexPrivilege checkPrivilege = IndexPrivilege.get(Collections.singleton(checkPrivilegeName)); - - final Automaton checkIndexAutomaton = IndicesPermission.Group.buildIndexMatcherAutomaton(allowRestrictedIndices, checkIndexPattern); - - List privilegeAutomatons = new ArrayList<>(); - for (IndicesPermission.Group group : userRole.indices().groups()) { - final Automaton groupIndexAutomaton = predicateCache.computeIfAbsent(group, - g -> IndicesPermission.Group.buildIndexMatcherAutomaton(g.allowRestrictedIndices(), g.indices())); - if (Operations.subsetOf(checkIndexAutomaton, groupIndexAutomaton)) { - final IndexPrivilege rolePrivilege = group.privilege(); - if (rolePrivilege.name().contains(checkPrivilegeName)) { - return true; - } - privilegeAutomatons.add(rolePrivilege.getAutomaton()); - } - } - return testPrivilege(checkPrivilege, Automatons.unionAndMinimize(privilegeAutomatons)); - } - - private static boolean testPrivilege(Privilege checkPrivilege, Automaton roleAutomaton) { - return Operations.subsetOf(checkPrivilege.getAutomaton(), roleAutomaton); - } - - private boolean testResourceMatch(String application, String checkResource, String checkPrivilegeName, Role userRole, - Collection privileges) { - final Set nameSet = Collections.singleton(checkPrivilegeName); - final ApplicationPrivilege checkPrivilege = ApplicationPrivilege.get(application, nameSet, privileges); - assert checkPrivilege.getApplication().equals(application) - : "Privilege " + checkPrivilege + " should have application " + application; - assert checkPrivilege.name().equals(nameSet) - : "Privilege " + checkPrivilege + " should have name " + nameSet; - - return userRole.application().grants(checkPrivilege, checkResource); - } - } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index bd27bd24fdd85..ec302f1003af8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -35,6 +35,10 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportActionProxy; 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.authc.AuthenticationFailureHandler; import org.elasticsearch.xpack.core.security.authc.esnative.ClientReservedRealm; @@ -48,6 +52,7 @@ import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.user.AnonymousUser; @@ -68,6 +73,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; @@ -118,6 +124,22 @@ public AuthorizationService(Settings settings, CompositeRolesStore rolesStore, C this.settings = settings; } + public void checkPrivileges(Authentication authentication, HasPrivilegesRequest request, + Collection applicationPrivilegeDescriptors, + ActionListener listener) { + getAuthorizationEngine(authentication).checkPrivileges(authentication, getAuthorizationInfoFromContext(), request, + applicationPrivilegeDescriptors, wrapPreservingContext(listener, threadContext)); + } + + public void retrieveUserPrivileges(Authentication authentication, GetUserPrivilegesRequest request, + ActionListener listener) { + getAuthorizationEngine(authentication).getUserPrivileges(authentication, getAuthorizationInfoFromContext(), request, listener); + } + + private AuthorizationInfo getAuthorizationInfoFromContext() { + return Objects.requireNonNull(threadContext.getTransient(AUTHORIZATION_INFO_KEY), "authorization info is missing from context"); + } + /** * Verifies that the given user can execute the given request (and action). If the user doesn't * have the appropriate privileges for this action/request, an {@link ElasticsearchSecurityException} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index 0d2974e4c8abc..8a8cd3dbe93af 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -6,6 +6,9 @@ package org.elasticsearch.xpack.security.authz; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.lucene.util.automaton.Automaton; import org.apache.lucene.util.automaton.Operations; import org.elasticsearch.action.ActionListener; @@ -23,6 +26,9 @@ import org.elasticsearch.action.search.SearchTransportService; import org.elasticsearch.action.termvectors.MultiTermVectorsAction; import org.elasticsearch.cluster.metadata.AliasOrIndex; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.transport.TransportActionProxy; @@ -30,30 +36,51 @@ import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; import org.elasticsearch.xpack.core.security.action.user.ChangePasswordAction; 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.action.user.HasPrivilegesAction; +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.UserRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; 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.accesscontrol.IndicesAccessControl; 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.ApplicationPrivilegeDescriptor; +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.IndexPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.TreeSet; import java.util.function.Function; import java.util.function.Predicate; +import static org.elasticsearch.common.Strings.arrayToCommaDelimitedString; +import static org.elasticsearch.xpack.security.action.user.TransportHasPrivilegesAction.getApplicationNames; import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME; public class RBACEngine implements AuthorizationEngine { @@ -65,10 +92,12 @@ public class RBACEngine implements AuthorizationEngine { private static final String DELETE_SUB_REQUEST_PRIMARY = DeleteAction.NAME + "[p]"; private static final String DELETE_SUB_REQUEST_REPLICA = DeleteAction.NAME + "[r]"; + private static final Logger logger = LogManager.getLogger(RBACEngine.class); + private final CompositeRolesStore rolesStore; private final FieldPermissionsCache fieldPermissionsCache; - RBACEngine(Settings settings, CompositeRolesStore rolesStore) { + public RBACEngine(Settings settings, CompositeRolesStore rolesStore) { this.rolesStore = rolesStore; this.fieldPermissionsCache = new FieldPermissionsCache(settings); } @@ -310,7 +339,200 @@ public void validateIndexPermissionsAreSubset(RequestInfo requestInfo, Authoriza listener.onFailure( new IllegalArgumentException("unsupported authorization info:" + authorizationInfo.getClass().getSimpleName())); } + } + + @Override + public void checkPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, + HasPrivilegesRequest request, + Collection applicationPrivileges, + ActionListener listener) { + if (authorizationInfo instanceof RBACAuthorizationInfo == false) { + listener.onFailure( + new IllegalArgumentException("unsupported authorization info:" + authorizationInfo.getClass().getSimpleName())); + return; + } + final Role userRole = ((RBACAuthorizationInfo) authorizationInfo).getRole(); + logger.trace(() -> new ParameterizedMessage("Check whether role [{}] has privileges cluster=[{}] index=[{}] application=[{}]", + Strings.arrayToCommaDelimitedString(userRole.names()), + Strings.arrayToCommaDelimitedString(request.clusterPrivileges()), + Strings.arrayToCommaDelimitedString(request.indexPrivileges()), + Strings.arrayToCommaDelimitedString(request.applicationPrivileges()) + )); + + Map cluster = new HashMap<>(); + for (String checkAction : request.clusterPrivileges()) { + final ClusterPrivilege checkPrivilege = ClusterPrivilege.get(Collections.singleton(checkAction)); + final ClusterPrivilege rolePrivilege = userRole.cluster().privilege(); + cluster.put(checkAction, testPrivilege(checkPrivilege, rolePrivilege.getAutomaton())); + } + boolean allMatch = cluster.values().stream().allMatch(Boolean::booleanValue); + + final Map predicateCache = new HashMap<>(); + + final Map indices = new LinkedHashMap<>(); + for (RoleDescriptor.IndicesPrivileges check : request.indexPrivileges()) { + for (String index : check.getIndices()) { + final Map privileges = new HashMap<>(); + final HasPrivilegesResponse.ResourcePrivileges existing = indices.get(index); + if (existing != null) { + privileges.putAll(existing.getPrivileges()); + } + for (String privilege : check.getPrivileges()) { + if (testIndexMatch(index, check.allowRestrictedIndices(), privilege, userRole, predicateCache)) { + logger.debug(() -> new ParameterizedMessage("Role [{}] has [{}] on index [{}]", + Strings.arrayToCommaDelimitedString(userRole.names()), privilege, index)); + privileges.put(privilege, true); + } else { + logger.debug(() -> new ParameterizedMessage("Role [{}] does not have [{}] on index [{}]", + Strings.arrayToCommaDelimitedString(userRole.names()), privilege, index)); + privileges.put(privilege, false); + allMatch = false; + } + } + indices.put(index, new HasPrivilegesResponse.ResourcePrivileges(index, privileges)); + } + } + + final Map> privilegesByApplication = new HashMap<>(); + for (String applicationName : getApplicationNames(request)) { + logger.debug("Checking privileges for application {}", applicationName); + final Map appPrivilegesByResource = new LinkedHashMap<>(); + for (RoleDescriptor.ApplicationResourcePrivileges p : request.applicationPrivileges()) { + if (applicationName.equals(p.getApplication())) { + for (String resource : p.getResources()) { + final Map privileges = new HashMap<>(); + final HasPrivilegesResponse.ResourcePrivileges existing = appPrivilegesByResource.get(resource); + if (existing != null) { + privileges.putAll(existing.getPrivileges()); + } + for (String privilege : p.getPrivileges()) { + if (testResourceMatch(applicationName, resource, privilege, userRole, applicationPrivileges)) { + logger.debug(() -> new ParameterizedMessage("Role [{}] has [{} {}] on resource [{}]", + Strings.arrayToCommaDelimitedString(userRole.names()), applicationName, privilege, resource)); + privileges.put(privilege, true); + } else { + logger.debug(() -> new ParameterizedMessage("Role [{}] does not have [{} {}] on resource [{}]", + Strings.arrayToCommaDelimitedString(userRole.names()), applicationName, privilege, resource)); + privileges.put(privilege, false); + allMatch = false; + } + } + appPrivilegesByResource.put(resource, new HasPrivilegesResponse.ResourcePrivileges(resource, privileges)); + } + } + } + privilegesByApplication.put(applicationName, appPrivilegesByResource.values()); + } + + listener.onResponse(new HasPrivilegesResponse(request.username(), allMatch, cluster, indices.values(), privilegesByApplication)); + } + + + @Override + public void getUserPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, GetUserPrivilegesRequest request, + ActionListener listener) { + if (authorizationInfo instanceof RBACAuthorizationInfo == false) { + listener.onFailure( + new IllegalArgumentException("unsupported authorization info:" + authorizationInfo.getClass().getSimpleName())); + } else { + final Role role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); + listener.onResponse(buildUserPrivilegesResponseObject(role)); + } + } + + GetUserPrivilegesResponse buildUserPrivilegesResponseObject(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 cluster = new TreeSet<>(); + // But we don't have a meaningful ordering for objects like ConditionalClusterPrivilege, so the tests work with "random" ordering + final Set conditionalCluster = new HashSet<>(); + for (Tuple 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 indices = new LinkedHashSet<>(); + for (IndicesPermission.Group group : userRole.indices().groups()) { + final Set queries = group.getQuery() == null ? Collections.emptySet() : group.getQuery(); + final Set 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 application = new LinkedHashSet<>(); + for (String applicationName : userRole.application().getApplicationNames()) { + for (ApplicationPrivilege privilege : userRole.application().getPrivileges(applicationName)) { + final Set 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 runAs; + if (Operations.isEmpty(runAsPrivilege.getAutomaton())) { + runAs = Collections.emptySet(); + } else { + runAs = runAsPrivilege.name(); + } + + return new GetUserPrivilegesResponse(cluster, conditionalCluster, indices, application, runAs); + } + + private boolean testIndexMatch(String checkIndexPattern, boolean allowRestrictedIndices, String checkPrivilegeName, Role userRole, + Map predicateCache) { + final IndexPrivilege checkPrivilege = IndexPrivilege.get(Collections.singleton(checkPrivilegeName)); + + final Automaton checkIndexAutomaton = IndicesPermission.Group.buildIndexMatcherAutomaton(allowRestrictedIndices, checkIndexPattern); + + List privilegeAutomatons = new ArrayList<>(); + for (IndicesPermission.Group group : userRole.indices().groups()) { + final Automaton groupIndexAutomaton = predicateCache.computeIfAbsent(group, + g -> IndicesPermission.Group.buildIndexMatcherAutomaton(g.allowRestrictedIndices(), g.indices())); + if (Operations.subsetOf(checkIndexAutomaton, groupIndexAutomaton)) { + final IndexPrivilege rolePrivilege = group.privilege(); + if (rolePrivilege.name().contains(checkPrivilegeName)) { + return true; + } + privilegeAutomatons.add(rolePrivilege.getAutomaton()); + } + } + return testPrivilege(checkPrivilege, Automatons.unionAndMinimize(privilegeAutomatons)); + } + + private static boolean testPrivilege(Privilege checkPrivilege, Automaton roleAutomaton) { + return Operations.subsetOf(checkPrivilege.getAutomaton(), roleAutomaton); + } + + private boolean testResourceMatch(String application, String checkResource, String checkPrivilegeName, Role userRole, + Collection privileges) { + final Set nameSet = Collections.singleton(checkPrivilegeName); + final ApplicationPrivilege checkPrivilege = ApplicationPrivilege.get(application, nameSet, privileges); + assert checkPrivilege.getApplication().equals(application) + : "Privilege " + checkPrivilege + " should have application " + application; + assert checkPrivilege.name().equals(nameSet) + : "Privilege " + checkPrivilege + " should have name " + nameSet; + return userRole.application().grants(checkPrivilege, checkResource); } static List resolveAuthorizedIndicesFromRole(Role role, String action, Map aliasAndIndexLookup) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesActionTests.java deleted file mode 100644 index 68cd414e0441d..0000000000000 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesActionTests.java +++ /dev/null @@ -1,86 +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. - */ - -package org.elasticsearch.xpack.security.action.user; - -import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.common.bytes.BytesArray; -import org.elasticsearch.common.util.set.Sets; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.transport.TransportService; -import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; -import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; -import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; -import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; -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.ConditionalClusterPrivileges.ManageApplicationPrivileges; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; -import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; - -import java.util.Collections; -import java.util.Set; - -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.emptyIterable; -import static org.hamcrest.Matchers.iterableWithSize; -import static org.mockito.Mockito.mock; - -public class TransportGetUserPrivilegesActionTests extends ESTestCase { - - public void testBuildResponseObject() { - final ManageApplicationPrivileges manageApplicationPrivileges = new ManageApplicationPrivileges(Sets.newHashSet("app01", "app02")); - final BytesArray query = new BytesArray("{\"term\":{\"public\":true}}"); - final Role role = Role.builder("test", "role") - .cluster(Sets.newHashSet("monitor", "manage_watcher"), Collections.singleton(manageApplicationPrivileges)) - .add(IndexPrivilege.get(Sets.newHashSet("read", "write")), "index-1") - .add(IndexPrivilege.ALL, "index-2", "index-3") - .add( - new FieldPermissions(new FieldPermissionsDefinition(new String[]{ "public.*" }, new String[0])), - Collections.singleton(query), - IndexPrivilege.READ, randomBoolean(), "index-4", "index-5") - .addApplicationPrivilege(new ApplicationPrivilege("app01", "read", "data:read"), Collections.singleton("*")) - .runAs(new Privilege(Sets.newHashSet("user01", "user02"), "user01", "user02")) - .build(); - - final TransportGetUserPrivilegesAction action = new TransportGetUserPrivilegesAction(mock(ThreadPool.class), - mock(TransportService.class), mock(ActionFilters.class), mock(CompositeRolesStore.class)); - final GetUserPrivilegesResponse response = action.buildResponseObject(role); - - assertThat(response.getClusterPrivileges(), containsInAnyOrder("monitor", "manage_watcher")); - assertThat(response.getConditionalClusterPrivileges(), containsInAnyOrder(manageApplicationPrivileges)); - - assertThat(response.getIndexPrivileges(), iterableWithSize(3)); - final GetUserPrivilegesResponse.Indices index1 = findIndexPrivilege(response.getIndexPrivileges(), "index-1"); - assertThat(index1.getIndices(), containsInAnyOrder("index-1")); - assertThat(index1.getPrivileges(), containsInAnyOrder("read", "write")); - assertThat(index1.getFieldSecurity(), emptyIterable()); - assertThat(index1.getQueries(), emptyIterable()); - final GetUserPrivilegesResponse.Indices index2 = findIndexPrivilege(response.getIndexPrivileges(), "index-2"); - assertThat(index2.getIndices(), containsInAnyOrder("index-2", "index-3")); - assertThat(index2.getPrivileges(), containsInAnyOrder("all")); - assertThat(index2.getFieldSecurity(), emptyIterable()); - assertThat(index2.getQueries(), emptyIterable()); - final GetUserPrivilegesResponse.Indices index4 = findIndexPrivilege(response.getIndexPrivileges(), "index-4"); - assertThat(index4.getIndices(), containsInAnyOrder("index-4", "index-5")); - assertThat(index4.getPrivileges(), containsInAnyOrder("read")); - assertThat(index4.getFieldSecurity(), containsInAnyOrder( - new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[]{ "public.*" }, new String[0]))); - assertThat(index4.getQueries(), containsInAnyOrder(query)); - - assertThat(response.getApplicationPrivileges(), containsInAnyOrder( - RoleDescriptor.ApplicationResourcePrivileges.builder().application("app01").privileges("read").resources("*").build()) - ); - - assertThat(response.getRunAs(), containsInAnyOrder("user01", "user02")); - } - - private GetUserPrivilegesResponse.Indices findIndexPrivilege(Set indices, String name) { - return indices.stream().filter(i -> i.getIndices().contains(name)).findFirst().get(); - } -} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java deleted file mode 100644 index 8d734d5d79925..0000000000000 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java +++ /dev/null @@ -1,526 +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. - */ -package org.elasticsearch.xpack.security.action.user; - -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; -import org.elasticsearch.action.delete.DeleteAction; -import org.elasticsearch.action.index.IndexAction; -import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.PlainActionFuture; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.collect.MapBuilder; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.common.util.set.Sets; -import org.elasticsearch.mock.orig.Mockito; -import org.elasticsearch.tasks.Task; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.test.junit.annotations.TestLogging; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.transport.Transport; -import org.elasticsearch.transport.TransportService; -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.authc.AuthenticationField; -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.Role; -import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; -import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; -import org.elasticsearch.xpack.core.security.user.User; -import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; -import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; -import org.hamcrest.Matchers; -import org.junit.Before; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Set; - -import static java.util.Collections.emptyMap; -import static org.elasticsearch.common.util.set.Sets.newHashSet; -import static org.hamcrest.Matchers.arrayWithSize; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.iterableWithSize; -import static org.hamcrest.Matchers.notNullValue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -@TestLogging("org.elasticsearch.xpack.security.action.user.TransportHasPrivilegesAction:TRACE," + - "org.elasticsearch.xpack.core.security.authz.permission.ApplicationPermission:DEBUG") -public class TransportHasPrivilegesActionTests extends ESTestCase { - - private User user; - private Role role; - private TransportHasPrivilegesAction action; - private List applicationPrivileges; - - @Before - public void setup() { - user = new User(randomAlphaOfLengthBetween(4, 12)); - final ThreadPool threadPool = mock(ThreadPool.class); - final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); - final TransportService transportService = new TransportService(Settings.EMPTY, mock(Transport.class), null, - TransportService.NOOP_TRANSPORT_INTERCEPTOR, x -> null, null, Collections.emptySet()); - - final Authentication authentication = mock(Authentication.class); - threadContext.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication); - when(threadPool.getThreadContext()).thenReturn(threadContext); - - when(authentication.getUser()).thenReturn(user); - - CompositeRolesStore rolesStore = mock(CompositeRolesStore.class); - Mockito.doAnswer(invocationOnMock -> { - ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; - listener.onResponse(role); - return null; - }).when(rolesStore).getRoles(eq(user), any(FieldPermissionsCache.class), any(ActionListener.class)); - - applicationPrivileges = new ArrayList<>(); - NativePrivilegeStore privilegeStore = mock(NativePrivilegeStore.class); - Mockito.doAnswer(inv -> { - assertThat(inv.getArguments(), arrayWithSize(3)); - ActionListener> listener - = (ActionListener>) inv.getArguments()[2]; - logger.info("Privileges for ({}) are {}", Arrays.toString(inv.getArguments()), applicationPrivileges); - listener.onResponse(applicationPrivileges); - return null; - }).when(privilegeStore).getPrivileges(any(Collection.class), any(Collection.class), any(ActionListener.class)); - - action = new TransportHasPrivilegesAction(threadPool, transportService, mock(ActionFilters.class), rolesStore, - privilegeStore); - } - - /** - * This tests that action names in the request are considered "matched" by the relevant named privilege - * (in this case that {@link DeleteAction} and {@link IndexAction} are satisfied by {@link IndexPrivilege#WRITE}). - */ - public void testNamedIndexPrivilegesMatchApplicableActions() throws Exception { - role = Role.builder("test1") - .cluster(Collections.singleton("all"), Collections.emptyList()) - .add(IndexPrivilege.WRITE, "academy") - .build(); - - final HasPrivilegesRequest request = new HasPrivilegesRequest(); - request.username(user.principal()); - request.clusterPrivileges(ClusterHealthAction.NAME); - request.indexPrivileges(RoleDescriptor.IndicesPrivileges.builder() - .indices("academy") - .privileges(DeleteAction.NAME, IndexAction.NAME) - .build()); - request.applicationPrivileges(new RoleDescriptor.ApplicationResourcePrivileges[0]); - final PlainActionFuture future = new PlainActionFuture(); - action.doExecute(mock(Task.class), request, future); - - final HasPrivilegesResponse response = future.get(); - assertThat(response, notNullValue()); - assertThat(response.getUsername(), is(user.principal())); - assertThat(response.isCompleteMatch(), is(true)); - - assertThat(response.getClusterPrivileges().size(), equalTo(1)); - assertThat(response.getClusterPrivileges().get(ClusterHealthAction.NAME), equalTo(true)); - - assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(1)); - final ResourcePrivileges result = response.getIndexPrivileges().iterator().next(); - assertThat(result.getResource(), equalTo("academy")); - assertThat(result.getPrivileges().size(), equalTo(2)); - assertThat(result.getPrivileges().get(DeleteAction.NAME), equalTo(true)); - assertThat(result.getPrivileges().get(IndexAction.NAME), equalTo(true)); - } - - /** - * This tests that the action responds correctly when the user/role has some, but not all - * of the privileges being checked. - */ - public void testMatchSubsetOfPrivileges() throws Exception { - role = Role.builder("test2") - .cluster(ClusterPrivilege.MONITOR) - .add(IndexPrivilege.INDEX, "academy") - .add(IndexPrivilege.WRITE, "initiative") - .build(); - - final HasPrivilegesRequest request = new HasPrivilegesRequest(); - request.username(user.principal()); - request.clusterPrivileges("monitor", "manage"); - request.indexPrivileges(RoleDescriptor.IndicesPrivileges.builder() - .indices("academy", "initiative", "school") - .privileges("delete", "index", "manage") - .build()); - request.applicationPrivileges(new RoleDescriptor.ApplicationResourcePrivileges[0]); - final PlainActionFuture future = new PlainActionFuture(); - action.doExecute(mock(Task.class), request, future); - - final HasPrivilegesResponse response = future.get(); - assertThat(response, notNullValue()); - assertThat(response.getUsername(), is(user.principal())); - assertThat(response.isCompleteMatch(), is(false)); - assertThat(response.getClusterPrivileges().size(), equalTo(2)); - assertThat(response.getClusterPrivileges().get("monitor"), equalTo(true)); - assertThat(response.getClusterPrivileges().get("manage"), equalTo(false)); - assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(3)); - - final Iterator indexPrivilegesIterator = response.getIndexPrivileges().iterator(); - final ResourcePrivileges academy = indexPrivilegesIterator.next(); - final ResourcePrivileges initiative = indexPrivilegesIterator.next(); - final ResourcePrivileges school = indexPrivilegesIterator.next(); - - assertThat(academy.getResource(), equalTo("academy")); - assertThat(academy.getPrivileges().size(), equalTo(3)); - assertThat(academy.getPrivileges().get("index"), equalTo(true)); // explicit - assertThat(academy.getPrivileges().get("delete"), equalTo(false)); - assertThat(academy.getPrivileges().get("manage"), equalTo(false)); - - assertThat(initiative.getResource(), equalTo("initiative")); - assertThat(initiative.getPrivileges().size(), equalTo(3)); - assertThat(initiative.getPrivileges().get("index"), equalTo(true)); // implied by write - assertThat(initiative.getPrivileges().get("delete"), equalTo(true)); // implied by write - assertThat(initiative.getPrivileges().get("manage"), equalTo(false)); - - assertThat(school.getResource(), equalTo("school")); - assertThat(school.getPrivileges().size(), equalTo(3)); - assertThat(school.getPrivileges().get("index"), equalTo(false)); - assertThat(school.getPrivileges().get("delete"), equalTo(false)); - assertThat(school.getPrivileges().get("manage"), equalTo(false)); - } - - /** - * This tests that the action responds correctly when the user/role has none - * of the privileges being checked. - */ - public void testMatchNothing() throws Exception { - role = Role.builder("test3") - .cluster(ClusterPrivilege.MONITOR) - .build(); - - final HasPrivilegesResponse response = hasPrivileges(RoleDescriptor.IndicesPrivileges.builder() - .indices("academy") - .privileges("read", "write") - .build(), Strings.EMPTY_ARRAY); - assertThat(response.getUsername(), is(user.principal())); - assertThat(response.isCompleteMatch(), is(false)); - assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(1)); - final ResourcePrivileges result = response.getIndexPrivileges().iterator().next(); - assertThat(result.getResource(), equalTo("academy")); - assertThat(result.getPrivileges().size(), equalTo(2)); - assertThat(result.getPrivileges().get("read"), equalTo(false)); - assertThat(result.getPrivileges().get("write"), equalTo(false)); - } - - /** - * Wildcards in the request are treated as - * does the user have ___ privilege on every possible index that matches this pattern? - * Or, expressed differently, - * does the user have ___ privilege on a wildcard that covers (is a superset of) this pattern? - */ - public void testWildcardHandling() throws Exception { - final ApplicationPrivilege kibanaRead = defineApplicationPrivilege("kibana", "read", - "data:read/*", "action:login", "action:view/dashboard"); - final ApplicationPrivilege kibanaWrite = defineApplicationPrivilege("kibana", "write", - "data:write/*", "action:login", "action:view/dashboard"); - final ApplicationPrivilege kibanaAdmin = defineApplicationPrivilege("kibana", "admin", - "action:login", "action:manage/*"); - final ApplicationPrivilege kibanaViewSpace = defineApplicationPrivilege("kibana", "view-space", - "action:login", "space:view/*"); - role = Role.builder("test3") - .add(IndexPrivilege.ALL, "logstash-*", "foo?") - .add(IndexPrivilege.READ, "abc*") - .add(IndexPrivilege.WRITE, "*xyz") - .addApplicationPrivilege(kibanaRead, Collections.singleton("*")) - .addApplicationPrivilege(kibanaViewSpace, newHashSet("space/engineering/*", "space/builds")) - .build(); - - final HasPrivilegesRequest request = new HasPrivilegesRequest(); - request.username(user.principal()); - request.clusterPrivileges(Strings.EMPTY_ARRAY); - request.indexPrivileges( - RoleDescriptor.IndicesPrivileges.builder() - .indices("logstash-2016-*") - .privileges("write") // Yes, because (ALL,"logstash-*") - .build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices("logstash-*") - .privileges("read") // Yes, because (ALL,"logstash-*") - .build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices("log*") - .privileges("manage") // No, because "log*" includes indices that "logstash-*" does not - .build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices("foo*", "foo?") - .privileges("read") // Yes, "foo?", but not "foo*", because "foo*" > "foo?" - .build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices("abcd*") - .privileges("read", "write") // read = Yes, because (READ, "abc*"), write = No - .build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices("abc*xyz") - .privileges("read", "write", "manage") // read = Yes ( READ "abc*"), write = Yes (WRITE, "*xyz"), manage = No - .build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices("a*xyz") - .privileges("read", "write", "manage") // read = No, write = Yes (WRITE, "*xyz"), manage = No - .build() - ); - - request.applicationPrivileges( - RoleDescriptor.ApplicationResourcePrivileges.builder() - .resources("*") - .application("kibana") - .privileges(Sets.union(kibanaRead.name(), kibanaWrite.name())) // read = Yes, write = No - .build(), - RoleDescriptor.ApplicationResourcePrivileges.builder() - .resources("space/engineering/project-*", "space/*") // project-* = Yes, space/* = Not - .application("kibana") - .privileges("space:view/dashboard") - .build() - ); - - final PlainActionFuture future = new PlainActionFuture(); - action.doExecute(mock(Task.class), request, future); - - final HasPrivilegesResponse response = future.get(); - assertThat(response, notNullValue()); - assertThat(response.getUsername(), is(user.principal())); - assertThat(response.isCompleteMatch(), is(false)); - assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(8)); - assertThat(response.getIndexPrivileges(), containsInAnyOrder( - new ResourcePrivileges("logstash-2016-*", Collections.singletonMap("write", true)), - new ResourcePrivileges("logstash-*", Collections.singletonMap("read", true)), - new ResourcePrivileges("log*", Collections.singletonMap("manage", false)), - new ResourcePrivileges("foo?", Collections.singletonMap("read", true)), - new ResourcePrivileges("foo*", Collections.singletonMap("read", false)), - new ResourcePrivileges("abcd*", mapBuilder().put("read", true).put("write", false).map()), - new ResourcePrivileges("abc*xyz", mapBuilder().put("read", true).put("write", true).put("manage", false).map()), - new ResourcePrivileges("a*xyz", mapBuilder().put("read", false).put("write", true).put("manage", false).map()) - )); - assertThat(response.getApplicationPrivileges().entrySet(), Matchers.iterableWithSize(1)); - final Set kibanaPrivileges = response.getApplicationPrivileges().get("kibana"); - assertThat(kibanaPrivileges, Matchers.iterableWithSize(3)); - assertThat(Strings.collectionToCommaDelimitedString(kibanaPrivileges), kibanaPrivileges, containsInAnyOrder( - new ResourcePrivileges("*", mapBuilder().put("read", true).put("write", false).map()), - new ResourcePrivileges("space/engineering/project-*", Collections.singletonMap("space:view/dashboard", true)), - new ResourcePrivileges("space/*", Collections.singletonMap("space:view/dashboard", false)) - )); - } - - private ApplicationPrivilege defineApplicationPrivilege(String app, String name, String ... actions) { - this.applicationPrivileges.add(new ApplicationPrivilegeDescriptor(app, name, newHashSet(actions), emptyMap())); - return new ApplicationPrivilege(app, name, actions); - } - - public void testCheckingIndexPermissionsDefinedOnDifferentPatterns() throws Exception { - role = Role.builder("test-write") - .add(IndexPrivilege.INDEX, "apache-*") - .add(IndexPrivilege.DELETE, "apache-2016-*") - .build(); - - final HasPrivilegesResponse response = hasPrivileges(RoleDescriptor.IndicesPrivileges.builder() - .indices("apache-2016-12", "apache-2017-01") - .privileges("index", "delete") - .build(), Strings.EMPTY_ARRAY); - assertThat(response.isCompleteMatch(), is(false)); - assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(2)); - assertThat(response.getIndexPrivileges(), containsInAnyOrder( - new ResourcePrivileges("apache-2016-12", - MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("index", true).put("delete", true).map()), - new ResourcePrivileges("apache-2017-01", - MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("index", true).put("delete", false).map() - ) - )); - } - - public void testCheckingApplicationPrivilegesOnDifferentApplicationsAndResources() throws Exception { - final ApplicationPrivilege app1Read = defineApplicationPrivilege("app1", "read", "data:read/*"); - final ApplicationPrivilege app1Write = defineApplicationPrivilege("app1", "write", "data:write/*"); - final ApplicationPrivilege app1All = defineApplicationPrivilege("app1", "all", "*"); - final ApplicationPrivilege app2Read = defineApplicationPrivilege("app2", "read", "data:read/*"); - final ApplicationPrivilege app2Write = defineApplicationPrivilege("app2", "write", "data:write/*"); - final ApplicationPrivilege app2All = defineApplicationPrivilege("app2", "all", "*"); - - role = Role.builder("test-role") - .addApplicationPrivilege(app1Read, Collections.singleton("foo/*")) - .addApplicationPrivilege(app1All, Collections.singleton("foo/bar/baz")) - .addApplicationPrivilege(app2Read, Collections.singleton("foo/bar/*")) - .addApplicationPrivilege(app2Write, Collections.singleton("*/bar/*")) - .build(); - - final HasPrivilegesResponse response = hasPrivileges(new RoleDescriptor.IndicesPrivileges[0], - new RoleDescriptor.ApplicationResourcePrivileges[]{ - RoleDescriptor.ApplicationResourcePrivileges.builder() - .application("app1") - .resources("foo/1", "foo/bar/2", "foo/bar/baz", "baz/bar/foo") - .privileges("read", "write", "all") - .build(), - RoleDescriptor.ApplicationResourcePrivileges.builder() - .application("app2") - .resources("foo/1", "foo/bar/2", "foo/bar/baz", "baz/bar/foo") - .privileges("read", "write", "all") - .build() - }, Strings.EMPTY_ARRAY); - - assertThat(response.isCompleteMatch(), is(false)); - assertThat(response.getIndexPrivileges(), Matchers.emptyIterable()); - assertThat(response.getApplicationPrivileges().entrySet(), Matchers.iterableWithSize(2)); - final Set app1 = response.getApplicationPrivileges().get("app1"); - assertThat(app1, Matchers.iterableWithSize(4)); - assertThat(Strings.collectionToCommaDelimitedString(app1), app1, containsInAnyOrder( - new ResourcePrivileges("foo/1", MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("read", true).put("write", false).put("all", false).map()), - new ResourcePrivileges("foo/bar/2", MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("read", true).put("write", false).put("all", false).map()), - new ResourcePrivileges("foo/bar/baz", MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("read", true).put("write", true).put("all", true).map()), - new ResourcePrivileges("baz/bar/foo", MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("read", false).put("write", false).put("all", false).map()) - )); - final Set app2 = response.getApplicationPrivileges().get("app2"); - assertThat(app2, Matchers.iterableWithSize(4)); - assertThat(Strings.collectionToCommaDelimitedString(app2), app2, containsInAnyOrder( - new ResourcePrivileges("foo/1", MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("read", false).put("write", false).put("all", false).map()), - new ResourcePrivileges("foo/bar/2", MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("read", true).put("write", true).put("all", false).map()), - new ResourcePrivileges("foo/bar/baz", MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("read", true).put("write", true).put("all", false).map()), - new ResourcePrivileges("baz/bar/foo", MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("read", false).put("write", true).put("all", false).map()) - )); - } - - public void testCheckingApplicationPrivilegesWithComplexNames() throws Exception { - final String appName = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(3, 10); - final String action1 = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(2, 5); - final String action2 = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(6, 9); - - final ApplicationPrivilege priv1 = defineApplicationPrivilege(appName, action1, "DATA:read/*", "ACTION:" + action1); - final ApplicationPrivilege priv2 = defineApplicationPrivilege(appName, action2, "DATA:read/*", "ACTION:" + action2); - - role = Role.builder("test-write") - .addApplicationPrivilege(priv1, Collections.singleton("user/*/name")) - .build(); - - final HasPrivilegesResponse response = hasPrivileges( - new RoleDescriptor.IndicesPrivileges[0], - new RoleDescriptor.ApplicationResourcePrivileges[]{ - RoleDescriptor.ApplicationResourcePrivileges.builder() - .application(appName) - .resources("user/hawkeye/name") - .privileges("DATA:read/user/*", "ACTION:" + action1, "ACTION:" + action2, action1, action2) - .build() - }, - "monitor"); - assertThat(response.isCompleteMatch(), is(false)); - assertThat(response.getApplicationPrivileges().keySet(), containsInAnyOrder(appName)); - assertThat(response.getApplicationPrivileges().get(appName), iterableWithSize(1)); - assertThat(response.getApplicationPrivileges().get(appName), containsInAnyOrder( - new ResourcePrivileges("user/hawkeye/name", MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("DATA:read/user/*", true) - .put("ACTION:" + action1, true) - .put("ACTION:" + action2, false) - .put(action1, true) - .put(action2, false) - .map()) - )); - } - - public void testIsCompleteMatch() throws Exception { - final ApplicationPrivilege kibanaRead = defineApplicationPrivilege("kibana", "read", "data:read/*"); - final ApplicationPrivilege kibanaWrite = defineApplicationPrivilege("kibana", "write", "data:write/*"); - role = Role.builder("test-write") - .cluster(ClusterPrivilege.MONITOR) - .add(IndexPrivilege.READ, "read-*") - .add(IndexPrivilege.ALL, "all-*") - .addApplicationPrivilege(kibanaRead, Collections.singleton("*")) - .build(); - - assertThat(hasPrivileges(indexPrivileges("read", "read-123", "read-456", "all-999"), "monitor").isCompleteMatch(), is(true)); - assertThat(hasPrivileges(indexPrivileges("read", "read-123", "read-456", "all-999"), "manage").isCompleteMatch(), is(false)); - assertThat(hasPrivileges(indexPrivileges("write", "read-123", "read-456", "all-999"), "monitor").isCompleteMatch(), is(false)); - assertThat(hasPrivileges(indexPrivileges("write", "read-123", "read-456", "all-999"), "manage").isCompleteMatch(), is(false)); - assertThat(hasPrivileges( - new RoleDescriptor.IndicesPrivileges[]{ - RoleDescriptor.IndicesPrivileges.builder() - .indices("read-a") - .privileges("read") - .build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices("all-b") - .privileges("read", "write") - .build() - }, - new RoleDescriptor.ApplicationResourcePrivileges[]{ - RoleDescriptor.ApplicationResourcePrivileges.builder() - .application("kibana") - .resources("*") - .privileges("read") - .build() - }, - "monitor").isCompleteMatch(), is(true)); - assertThat(hasPrivileges( - new RoleDescriptor.IndicesPrivileges[]{indexPrivileges("read", "read-123", "read-456", "all-999")}, - new RoleDescriptor.ApplicationResourcePrivileges[]{ - RoleDescriptor.ApplicationResourcePrivileges.builder() - .application("kibana").resources("*").privileges("read").build(), - RoleDescriptor.ApplicationResourcePrivileges.builder() - .application("kibana").resources("*").privileges("write").build() - }, - "monitor").isCompleteMatch(), is(false)); - } - - private RoleDescriptor.IndicesPrivileges indexPrivileges(String priv, String... indices) { - return RoleDescriptor.IndicesPrivileges.builder() - .indices(indices) - .privileges(priv) - .build(); - } - - private HasPrivilegesResponse hasPrivileges(RoleDescriptor.IndicesPrivileges indicesPrivileges, String... clusterPrivileges) - throws Exception { - return hasPrivileges( - new RoleDescriptor.IndicesPrivileges[]{indicesPrivileges}, - new RoleDescriptor.ApplicationResourcePrivileges[0], - clusterPrivileges - ); - } - - private HasPrivilegesResponse hasPrivileges(RoleDescriptor.IndicesPrivileges[] indicesPrivileges, - RoleDescriptor.ApplicationResourcePrivileges[] appPrivileges, - String... clusterPrivileges) throws Exception { - final HasPrivilegesRequest request = new HasPrivilegesRequest(); - request.username(user.principal()); - request.clusterPrivileges(clusterPrivileges); - request.indexPrivileges(indicesPrivileges); - request.applicationPrivileges(appPrivileges); - final PlainActionFuture future = new PlainActionFuture(); - action.doExecute(mock(Task.class), request, future); - final HasPrivilegesResponse response = future.get(); - assertThat(response, notNullValue()); - return response; - } - - private static MapBuilder mapBuilder() { - return MapBuilder.newMapBuilder(); - } - -} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index 55d6f4342b147..79316df55c4e8 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -93,6 +93,10 @@ import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesRequest; import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequest; +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.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.DefaultAuthenticationFailureHandler; @@ -107,6 +111,7 @@ import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; 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.ApplicationPrivilegeDescriptor; 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.store.ReservedRolesStore; @@ -1370,6 +1375,20 @@ public void validateIndexPermissionsAreSubset(RequestInfo requestInfo, Authoriza ActionListener listener) { throw new UnsupportedOperationException("not implemented"); } + + @Override + public void checkPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, + HasPrivilegesRequest hasPrivilegesRequest, + Collection applicationPrivilegeDescriptors, + ActionListener listener) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void getUserPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, + GetUserPrivilegesRequest request, ActionListener listener) { + throw new UnsupportedOperationException("not implemented"); + } }; authorizationService = new AuthorizationService(Settings.EMPTY, rolesStore, clusterService, diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index cf0a5a909a1d4..f7403ce74313f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -9,8 +9,15 @@ import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; import org.elasticsearch.action.admin.cluster.stats.ClusterStatsAction; +import org.elasticsearch.action.delete.DeleteAction; +import org.elasticsearch.action.index.IndexAction; +import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.client.Client; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.license.GetLicenseAction; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.transport.TransportRequest; @@ -21,6 +28,10 @@ import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequest; import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequestBuilder; import org.elasticsearch.xpack.core.security.action.user.DeleteUserAction; +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.action.user.HasPrivilegesResponse.ResourcePrivileges; import org.elasticsearch.xpack.core.security.action.user.PutUserAction; import org.elasticsearch.xpack.core.security.action.user.UserRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; @@ -28,12 +39,41 @@ import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; +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.ApplicationPrivilegeDescriptor; +import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges.ManageApplicationPrivileges; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; +import org.elasticsearch.xpack.security.authz.RBACEngine.RBACAuthorizationInfo; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; +import org.hamcrest.Matchers; import org.junit.Before; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import static java.util.Collections.emptyMap; +import static org.elasticsearch.common.util.set.Sets.newHashSet; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.emptyIterable; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.iterableWithSize; +import static org.hamcrest.Matchers.notNullValue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -191,4 +231,518 @@ public void testSameUserPermissionDoesNotAllowChangePasswordForLookedUpByOtherRe verify(lookedUpBy).getType(); verifyNoMoreInteractions(authentication, lookedUpBy, authenticatedBy); } + + /** + * This tests that action names in the request are considered "matched" by the relevant named privilege + * (in this case that {@link DeleteAction} and {@link IndexAction} are satisfied by {@link IndexPrivilege#WRITE}). + */ + public void testNamedIndexPrivilegesMatchApplicableActions() throws Exception { + User user = new User(randomAlphaOfLengthBetween(4, 12)); + Authentication authentication = mock(Authentication.class); + when(authentication.getUser()).thenReturn(user); + Role role = Role.builder("test1") + .cluster(Collections.singleton("all"), Collections.emptyList()) + .add(IndexPrivilege.WRITE, "academy") + .build(); + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + + final HasPrivilegesRequest request = new HasPrivilegesRequest(); + request.username(user.principal()); + request.clusterPrivileges(ClusterHealthAction.NAME); + request.indexPrivileges(RoleDescriptor.IndicesPrivileges.builder() + .indices("academy") + .privileges(DeleteAction.NAME, IndexAction.NAME) + .build()); + request.applicationPrivileges(new RoleDescriptor.ApplicationResourcePrivileges[0]); + + final PlainActionFuture future = new PlainActionFuture<>(); + engine.checkPrivileges(authentication, authzInfo, request, Collections.emptyList(), future); + + final HasPrivilegesResponse response = future.get(); + assertThat(response, notNullValue()); + assertThat(response.getUsername(), is(user.principal())); + assertThat(response.isCompleteMatch(), is(true)); + + assertThat(response.getClusterPrivileges().size(), equalTo(1)); + assertThat(response.getClusterPrivileges().get(ClusterHealthAction.NAME), equalTo(true)); + + assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(1)); + final ResourcePrivileges result = response.getIndexPrivileges().iterator().next(); + assertThat(result.getResource(), equalTo("academy")); + assertThat(result.getPrivileges().size(), equalTo(2)); + assertThat(result.getPrivileges().get(DeleteAction.NAME), equalTo(true)); + assertThat(result.getPrivileges().get(IndexAction.NAME), equalTo(true)); + } + + /** + * This tests that the action responds correctly when the user/role has some, but not all + * of the privileges being checked. + */ + public void testMatchSubsetOfPrivileges() throws Exception { + User user = new User(randomAlphaOfLengthBetween(4, 12)); + Authentication authentication = mock(Authentication.class); + when(authentication.getUser()).thenReturn(user); + Role role = Role.builder("test2") + .cluster(ClusterPrivilege.MONITOR) + .add(IndexPrivilege.INDEX, "academy") + .add(IndexPrivilege.WRITE, "initiative") + .build(); + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + + final HasPrivilegesRequest request = new HasPrivilegesRequest(); + request.username(user.principal()); + request.clusterPrivileges("monitor", "manage"); + request.indexPrivileges(RoleDescriptor.IndicesPrivileges.builder() + .indices("academy", "initiative", "school") + .privileges("delete", "index", "manage") + .build()); + request.applicationPrivileges(new RoleDescriptor.ApplicationResourcePrivileges[0]); + final PlainActionFuture future = new PlainActionFuture<>(); + engine.checkPrivileges(authentication, authzInfo, request, Collections.emptyList(), future); + + final HasPrivilegesResponse response = future.get(); + assertThat(response, notNullValue()); + assertThat(response.getUsername(), is(user.principal())); + assertThat(response.isCompleteMatch(), is(false)); + assertThat(response.getClusterPrivileges().size(), equalTo(2)); + assertThat(response.getClusterPrivileges().get("monitor"), equalTo(true)); + assertThat(response.getClusterPrivileges().get("manage"), equalTo(false)); + assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(3)); + + final Iterator indexPrivilegesIterator = response.getIndexPrivileges().iterator(); + final ResourcePrivileges academy = indexPrivilegesIterator.next(); + final ResourcePrivileges initiative = indexPrivilegesIterator.next(); + final ResourcePrivileges school = indexPrivilegesIterator.next(); + + assertThat(academy.getResource(), equalTo("academy")); + assertThat(academy.getPrivileges().size(), equalTo(3)); + assertThat(academy.getPrivileges().get("index"), equalTo(true)); // explicit + assertThat(academy.getPrivileges().get("delete"), equalTo(false)); + assertThat(academy.getPrivileges().get("manage"), equalTo(false)); + + assertThat(initiative.getResource(), equalTo("initiative")); + assertThat(initiative.getPrivileges().size(), equalTo(3)); + assertThat(initiative.getPrivileges().get("index"), equalTo(true)); // implied by write + assertThat(initiative.getPrivileges().get("delete"), equalTo(true)); // implied by write + assertThat(initiative.getPrivileges().get("manage"), equalTo(false)); + + assertThat(school.getResource(), equalTo("school")); + assertThat(school.getPrivileges().size(), equalTo(3)); + assertThat(school.getPrivileges().get("index"), equalTo(false)); + assertThat(school.getPrivileges().get("delete"), equalTo(false)); + assertThat(school.getPrivileges().get("manage"), equalTo(false)); + } + + /** + * This tests that the action responds correctly when the user/role has none + * of the privileges being checked. + */ + public void testMatchNothing() throws Exception { + User user = new User(randomAlphaOfLengthBetween(4, 12)); + Authentication authentication = mock(Authentication.class); + when(authentication.getUser()).thenReturn(user); + Role role = Role.builder("test3") + .cluster(ClusterPrivilege.MONITOR) + .build(); + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + + final HasPrivilegesResponse response = hasPrivileges(RoleDescriptor.IndicesPrivileges.builder() + .indices("academy") + .privileges("read", "write") + .build(), + authentication, authzInfo, Collections.emptyList(), Strings.EMPTY_ARRAY); + assertThat(response.getUsername(), is(user.principal())); + assertThat(response.isCompleteMatch(), is(false)); + assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(1)); + final ResourcePrivileges result = response.getIndexPrivileges().iterator().next(); + assertThat(result.getResource(), equalTo("academy")); + assertThat(result.getPrivileges().size(), equalTo(2)); + assertThat(result.getPrivileges().get("read"), equalTo(false)); + assertThat(result.getPrivileges().get("write"), equalTo(false)); + } + + /** + * Wildcards in the request are treated as + * does the user have ___ privilege on every possible index that matches this pattern? + * Or, expressed differently, + * does the user have ___ privilege on a wildcard that covers (is a superset of) this pattern? + */ + public void testWildcardHandling() throws Exception { + List privs = new ArrayList<>(); + final ApplicationPrivilege kibanaRead = defineApplicationPrivilege(privs, "kibana", "read", + "data:read/*", "action:login", "action:view/dashboard"); + final ApplicationPrivilege kibanaWrite = defineApplicationPrivilege(privs, "kibana", "write", + "data:write/*", "action:login", "action:view/dashboard"); + final ApplicationPrivilege kibanaAdmin = defineApplicationPrivilege(privs, "kibana", "admin", + "action:login", "action:manage/*"); + final ApplicationPrivilege kibanaViewSpace = defineApplicationPrivilege(privs, "kibana", "view-space", + "action:login", "space:view/*"); + User user = new User(randomAlphaOfLengthBetween(4, 12)); + Authentication authentication = mock(Authentication.class); + when(authentication.getUser()).thenReturn(user); + Role role = Role.builder("test3") + .add(IndexPrivilege.ALL, "logstash-*", "foo?") + .add(IndexPrivilege.READ, "abc*") + .add(IndexPrivilege.WRITE, "*xyz") + .addApplicationPrivilege(kibanaRead, Collections.singleton("*")) + .addApplicationPrivilege(kibanaViewSpace, newHashSet("space/engineering/*", "space/builds")) + .build(); + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + + final HasPrivilegesRequest request = new HasPrivilegesRequest(); + request.username(user.principal()); + request.clusterPrivileges(Strings.EMPTY_ARRAY); + request.indexPrivileges( + RoleDescriptor.IndicesPrivileges.builder() + .indices("logstash-2016-*") + .privileges("write") // Yes, because (ALL,"logstash-*") + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("logstash-*") + .privileges("read") // Yes, because (ALL,"logstash-*") + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("log*") + .privileges("manage") // No, because "log*" includes indices that "logstash-*" does not + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("foo*", "foo?") + .privileges("read") // Yes, "foo?", but not "foo*", because "foo*" > "foo?" + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("abcd*") + .privileges("read", "write") // read = Yes, because (READ, "abc*"), write = No + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("abc*xyz") + .privileges("read", "write", "manage") // read = Yes ( READ "abc*"), write = Yes (WRITE, "*xyz"), manage = No + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("a*xyz") + .privileges("read", "write", "manage") // read = No, write = Yes (WRITE, "*xyz"), manage = No + .build() + ); + + request.applicationPrivileges( + RoleDescriptor.ApplicationResourcePrivileges.builder() + .resources("*") + .application("kibana") + .privileges(Sets.union(kibanaRead.name(), kibanaWrite.name())) // read = Yes, write = No + .build(), + RoleDescriptor.ApplicationResourcePrivileges.builder() + .resources("space/engineering/project-*", "space/*") // project-* = Yes, space/* = Not + .application("kibana") + .privileges("space:view/dashboard") + .build() + ); + + final PlainActionFuture future = new PlainActionFuture<>(); + engine.checkPrivileges(authentication, authzInfo, request, privs, future); + + final HasPrivilegesResponse response = future.get(); + assertThat(response, notNullValue()); + assertThat(response.getUsername(), is(user.principal())); + assertThat(response.isCompleteMatch(), is(false)); + assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(8)); + assertThat(response.getIndexPrivileges(), containsInAnyOrder( + new ResourcePrivileges("logstash-2016-*", Collections.singletonMap("write", true)), + new ResourcePrivileges("logstash-*", Collections.singletonMap("read", true)), + new ResourcePrivileges("log*", Collections.singletonMap("manage", false)), + new ResourcePrivileges("foo?", Collections.singletonMap("read", true)), + new ResourcePrivileges("foo*", Collections.singletonMap("read", false)), + new ResourcePrivileges("abcd*", mapBuilder().put("read", true).put("write", false).map()), + new ResourcePrivileges("abc*xyz", mapBuilder().put("read", true).put("write", true).put("manage", false).map()), + new ResourcePrivileges("a*xyz", mapBuilder().put("read", false).put("write", true).put("manage", false).map()) + )); + assertThat(response.getApplicationPrivileges().entrySet(), Matchers.iterableWithSize(1)); + final Set kibanaPrivileges = response.getApplicationPrivileges().get("kibana"); + assertThat(kibanaPrivileges, Matchers.iterableWithSize(3)); + assertThat(Strings.collectionToCommaDelimitedString(kibanaPrivileges), kibanaPrivileges, containsInAnyOrder( + new ResourcePrivileges("*", mapBuilder().put("read", true).put("write", false).map()), + new ResourcePrivileges("space/engineering/project-*", Collections.singletonMap("space:view/dashboard", true)), + new ResourcePrivileges("space/*", Collections.singletonMap("space:view/dashboard", false)) + )); + } + + public void testCheckingIndexPermissionsDefinedOnDifferentPatterns() throws Exception { + User user = new User(randomAlphaOfLengthBetween(4, 12)); + Authentication authentication = mock(Authentication.class); + when(authentication.getUser()).thenReturn(user); + Role role = Role.builder("test-write") + .add(IndexPrivilege.INDEX, "apache-*") + .add(IndexPrivilege.DELETE, "apache-2016-*") + .build(); + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + + final HasPrivilegesResponse response = hasPrivileges(RoleDescriptor.IndicesPrivileges.builder() + .indices("apache-2016-12", "apache-2017-01") + .privileges("index", "delete") + .build(), authentication, authzInfo, Collections.emptyList(), Strings.EMPTY_ARRAY); + assertThat(response.isCompleteMatch(), is(false)); + assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(2)); + assertThat(response.getIndexPrivileges(), containsInAnyOrder( + new ResourcePrivileges("apache-2016-12", + MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("index", true).put("delete", true).map()), + new ResourcePrivileges("apache-2017-01", + MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("index", true).put("delete", false).map() + ) + )); + } + + public void testCheckingApplicationPrivilegesOnDifferentApplicationsAndResources() throws Exception { + List privs = new ArrayList<>(); + final ApplicationPrivilege app1Read = defineApplicationPrivilege(privs, "app1", "read", "data:read/*"); + final ApplicationPrivilege app1Write = defineApplicationPrivilege(privs, "app1", "write", "data:write/*"); + final ApplicationPrivilege app1All = defineApplicationPrivilege(privs, "app1", "all", "*"); + final ApplicationPrivilege app2Read = defineApplicationPrivilege(privs, "app2", "read", "data:read/*"); + final ApplicationPrivilege app2Write = defineApplicationPrivilege(privs, "app2", "write", "data:write/*"); + final ApplicationPrivilege app2All = defineApplicationPrivilege(privs, "app2", "all", "*"); + + User user = new User(randomAlphaOfLengthBetween(4, 12)); + Authentication authentication = mock(Authentication.class); + when(authentication.getUser()).thenReturn(user); + Role role = Role.builder("test-role") + .addApplicationPrivilege(app1Read, Collections.singleton("foo/*")) + .addApplicationPrivilege(app1All, Collections.singleton("foo/bar/baz")) + .addApplicationPrivilege(app2Read, Collections.singleton("foo/bar/*")) + .addApplicationPrivilege(app2Write, Collections.singleton("*/bar/*")) + .build(); + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + + final HasPrivilegesResponse response = hasPrivileges(new RoleDescriptor.IndicesPrivileges[0], + new RoleDescriptor.ApplicationResourcePrivileges[]{ + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("app1") + .resources("foo/1", "foo/bar/2", "foo/bar/baz", "baz/bar/foo") + .privileges("read", "write", "all") + .build(), + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("app2") + .resources("foo/1", "foo/bar/2", "foo/bar/baz", "baz/bar/foo") + .privileges("read", "write", "all") + .build() + }, authentication, authzInfo, privs, Strings.EMPTY_ARRAY); + + assertThat(response.isCompleteMatch(), is(false)); + assertThat(response.getIndexPrivileges(), Matchers.emptyIterable()); + assertThat(response.getApplicationPrivileges().entrySet(), Matchers.iterableWithSize(2)); + final Set app1 = response.getApplicationPrivileges().get("app1"); + assertThat(app1, Matchers.iterableWithSize(4)); + assertThat(Strings.collectionToCommaDelimitedString(app1), app1, containsInAnyOrder( + new ResourcePrivileges("foo/1", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", true).put("write", false).put("all", false).map()), + new ResourcePrivileges("foo/bar/2", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", true).put("write", false).put("all", false).map()), + new ResourcePrivileges("foo/bar/baz", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", true).put("write", true).put("all", true).map()), + new ResourcePrivileges("baz/bar/foo", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", false).put("write", false).put("all", false).map()) + )); + final Set app2 = response.getApplicationPrivileges().get("app2"); + assertThat(app2, Matchers.iterableWithSize(4)); + assertThat(Strings.collectionToCommaDelimitedString(app2), app2, containsInAnyOrder( + new ResourcePrivileges("foo/1", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", false).put("write", false).put("all", false).map()), + new ResourcePrivileges("foo/bar/2", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", true).put("write", true).put("all", false).map()), + new ResourcePrivileges("foo/bar/baz", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", true).put("write", true).put("all", false).map()), + new ResourcePrivileges("baz/bar/foo", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", false).put("write", true).put("all", false).map()) + )); + } + + public void testCheckingApplicationPrivilegesWithComplexNames() throws Exception { + final String appName = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(3, 10); + final String action1 = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(2, 5); + final String action2 = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(6, 9); + + final List privs = new ArrayList<>(); + final ApplicationPrivilege priv1 = defineApplicationPrivilege(privs, appName, action1, "DATA:read/*", "ACTION:" + action1); + final ApplicationPrivilege priv2 = defineApplicationPrivilege(privs, appName, action2, "DATA:read/*", "ACTION:" + action2); + + User user = new User(randomAlphaOfLengthBetween(4, 12)); + Authentication authentication = mock(Authentication.class); + when(authentication.getUser()).thenReturn(user); + Role role = Role.builder("test-write") + .addApplicationPrivilege(priv1, Collections.singleton("user/*/name")) + .build(); + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + + final HasPrivilegesResponse response = hasPrivileges( + new RoleDescriptor.IndicesPrivileges[0], + new RoleDescriptor.ApplicationResourcePrivileges[]{ + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application(appName) + .resources("user/hawkeye/name") + .privileges("DATA:read/user/*", "ACTION:" + action1, "ACTION:" + action2, action1, action2) + .build() + }, authentication, authzInfo, privs, "monitor"); + assertThat(response.isCompleteMatch(), is(false)); + assertThat(response.getApplicationPrivileges().keySet(), containsInAnyOrder(appName)); + assertThat(response.getApplicationPrivileges().get(appName), iterableWithSize(1)); + assertThat(response.getApplicationPrivileges().get(appName), containsInAnyOrder( + new ResourcePrivileges("user/hawkeye/name", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("DATA:read/user/*", true) + .put("ACTION:" + action1, true) + .put("ACTION:" + action2, false) + .put(action1, true) + .put(action2, false) + .map()) + )); + } + + public void testIsCompleteMatch() throws Exception { + final List privs = new ArrayList<>(); + final ApplicationPrivilege kibanaRead = defineApplicationPrivilege(privs, "kibana", "read", "data:read/*"); + final ApplicationPrivilege kibanaWrite = defineApplicationPrivilege(privs, "kibana", "write", "data:write/*"); + User user = new User(randomAlphaOfLengthBetween(4, 12)); + Authentication authentication = mock(Authentication.class); + when(authentication.getUser()).thenReturn(user); + Role role = Role.builder("test-write") + .cluster(ClusterPrivilege.MONITOR) + .add(IndexPrivilege.READ, "read-*") + .add(IndexPrivilege.ALL, "all-*") + .addApplicationPrivilege(kibanaRead, Collections.singleton("*")) + .build(); + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + + + assertThat(hasPrivileges( + indexPrivileges("read", "read-123", "read-456", "all-999"), authentication, authzInfo, privs, "monitor").isCompleteMatch(), + is(true)); + assertThat(hasPrivileges( + indexPrivileges("read", "read-123", "read-456", "all-999"), authentication, authzInfo, privs, "manage").isCompleteMatch(), + is(false)); + assertThat(hasPrivileges( + indexPrivileges("write", "read-123", "read-456", "all-999"), authentication, authzInfo, privs, "monitor").isCompleteMatch(), + is(false)); + assertThat(hasPrivileges( + indexPrivileges("write", "read-123", "read-456", "all-999"), authentication, authzInfo, privs, "manage").isCompleteMatch(), + is(false)); + assertThat(hasPrivileges( + new RoleDescriptor.IndicesPrivileges[]{ + RoleDescriptor.IndicesPrivileges.builder() + .indices("read-a") + .privileges("read") + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("all-b") + .privileges("read", "write") + .build() + }, + new RoleDescriptor.ApplicationResourcePrivileges[]{ + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("kibana") + .resources("*") + .privileges("read") + .build() + }, authentication, authzInfo, privs, "monitor").isCompleteMatch(), is(true)); + assertThat(hasPrivileges( + new RoleDescriptor.IndicesPrivileges[]{indexPrivileges("read", "read-123", "read-456", "all-999")}, + new RoleDescriptor.ApplicationResourcePrivileges[]{ + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("kibana").resources("*").privileges("read").build(), + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("kibana").resources("*").privileges("write").build() + }, authentication, authzInfo, privs, "monitor").isCompleteMatch(), is(false)); + } + + public void testBuildUserPrivilegeResponse() { + final ManageApplicationPrivileges manageApplicationPrivileges = new ManageApplicationPrivileges(Sets.newHashSet("app01", "app02")); + final BytesArray query = new BytesArray("{\"term\":{\"public\":true}}"); + final Role role = Role.builder("test", "role") + .cluster(Sets.newHashSet("monitor", "manage_watcher"), Collections.singleton(manageApplicationPrivileges)) + .add(IndexPrivilege.get(Sets.newHashSet("read", "write")), "index-1") + .add(IndexPrivilege.ALL, "index-2", "index-3") + .add( + new FieldPermissions(new FieldPermissionsDefinition(new String[]{ "public.*" }, new String[0])), + Collections.singleton(query), + IndexPrivilege.READ, randomBoolean(), "index-4", "index-5") + .addApplicationPrivilege(new ApplicationPrivilege("app01", "read", "data:read"), Collections.singleton("*")) + .runAs(new Privilege(Sets.newHashSet("user01", "user02"), "user01", "user02")) + .build(); + + final GetUserPrivilegesResponse response = engine.buildUserPrivilegesResponseObject(role); + + assertThat(response.getClusterPrivileges(), containsInAnyOrder("monitor", "manage_watcher")); + assertThat(response.getConditionalClusterPrivileges(), containsInAnyOrder(manageApplicationPrivileges)); + + assertThat(response.getIndexPrivileges(), iterableWithSize(3)); + final GetUserPrivilegesResponse.Indices index1 = findIndexPrivilege(response.getIndexPrivileges(), "index-1"); + assertThat(index1.getIndices(), containsInAnyOrder("index-1")); + assertThat(index1.getPrivileges(), containsInAnyOrder("read", "write")); + assertThat(index1.getFieldSecurity(), emptyIterable()); + assertThat(index1.getQueries(), emptyIterable()); + final GetUserPrivilegesResponse.Indices index2 = findIndexPrivilege(response.getIndexPrivileges(), "index-2"); + assertThat(index2.getIndices(), containsInAnyOrder("index-2", "index-3")); + assertThat(index2.getPrivileges(), containsInAnyOrder("all")); + assertThat(index2.getFieldSecurity(), emptyIterable()); + assertThat(index2.getQueries(), emptyIterable()); + final GetUserPrivilegesResponse.Indices index4 = findIndexPrivilege(response.getIndexPrivileges(), "index-4"); + assertThat(index4.getIndices(), containsInAnyOrder("index-4", "index-5")); + assertThat(index4.getPrivileges(), containsInAnyOrder("read")); + assertThat(index4.getFieldSecurity(), containsInAnyOrder( + new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[]{ "public.*" }, new String[0]))); + assertThat(index4.getQueries(), containsInAnyOrder(query)); + + assertThat(response.getApplicationPrivileges(), containsInAnyOrder( + RoleDescriptor.ApplicationResourcePrivileges.builder().application("app01").privileges("read").resources("*").build()) + ); + + assertThat(response.getRunAs(), containsInAnyOrder("user01", "user02")); + } + + private GetUserPrivilegesResponse.Indices findIndexPrivilege(Set indices, String name) { + return indices.stream().filter(i -> i.getIndices().contains(name)).findFirst().get(); + } + + private RoleDescriptor.IndicesPrivileges indexPrivileges(String priv, String... indices) { + return RoleDescriptor.IndicesPrivileges.builder() + .indices(indices) + .privileges(priv) + .build(); + } + + private ApplicationPrivilege defineApplicationPrivilege(List privs, String app, String name, + String ... actions) { + privs.add(new ApplicationPrivilegeDescriptor(app, name, newHashSet(actions), emptyMap())); + return new ApplicationPrivilege(app, name, actions); + } + + private HasPrivilegesResponse hasPrivileges(RoleDescriptor.IndicesPrivileges indicesPrivileges, Authentication authentication, + AuthorizationInfo authorizationInfo, + List applicationPrivilegeDescriptors, + String... clusterPrivileges) throws Exception { + return hasPrivileges( + new RoleDescriptor.IndicesPrivileges[]{indicesPrivileges}, + new RoleDescriptor.ApplicationResourcePrivileges[0], + authentication, authorizationInfo, applicationPrivilegeDescriptors, + clusterPrivileges + ); + } + + private HasPrivilegesResponse hasPrivileges(RoleDescriptor.IndicesPrivileges[] indicesPrivileges, + RoleDescriptor.ApplicationResourcePrivileges[] appPrivileges, + Authentication authentication, + AuthorizationInfo authorizationInfo, + List applicationPrivilegeDescriptors, + String... clusterPrivileges) throws Exception { + final HasPrivilegesRequest request = new HasPrivilegesRequest(); + request.username(authentication.getUser().principal()); + request.clusterPrivileges(clusterPrivileges); + request.indexPrivileges(indicesPrivileges); + request.applicationPrivileges(appPrivileges); + final PlainActionFuture future = new PlainActionFuture<>(); + engine.checkPrivileges(authentication, authorizationInfo, request, applicationPrivilegeDescriptors, future); + final HasPrivilegesResponse response = future.get(); + assertThat(response, notNullValue()); + return response; + } + + private static MapBuilder mapBuilder() { + return MapBuilder.newMapBuilder(); + } }