From fdffc7f411b9070cf5b376c63a08bd565c08841d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 21 Mar 2018 00:25:07 +0100 Subject: [PATCH 01/24] SOLR-12131: ExternalRoleRuleBasedAuthorizationPlugin which gets user's roles from request --- solr/CHANGES.txt | 2 + ...ernalRoleRuleBasedAuthorizationPlugin.java | 56 ++++ .../solr/security/PrincipalWithUserRoles.java | 95 +++++++ .../RuleBasedAuthorizationPlugin.java | 212 +-------------- .../RuleBasedAuthorizationPluginBase.java | 249 ++++++++++++++++++ .../solr/security/VerifiedUserRoles.java | 33 +++ ...hentication-and-authorization-plugins.adoc | 3 +- .../src/rule-based-authorization-plugin.adoc | 36 ++- 8 files changed, 484 insertions(+), 202 deletions(-) create mode 100644 solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java create mode 100644 solr/core/src/java/org/apache/solr/security/PrincipalWithUserRoles.java create mode 100644 solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java create mode 100644 solr/core/src/java/org/apache/solr/security/VerifiedUserRoles.java diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index 9053b5ec4a11..638f3026dd66 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -58,6 +58,8 @@ New Features * SOLR-11670: Implement a periodic house-keeping task. This uses a scheduled autoscaling trigger and currently performs cleanup of old inactive shards. (ab, shalin) +* SOLR-12131: ExternalRoleRuleBasedAuthorizationPlugin which gets user's roles from request (janhoy) + Bug Fixes ---------------------- diff --git a/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java b/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java new file mode 100644 index 000000000000..dd7ec0cda24d --- /dev/null +++ b/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security; + +import java.lang.invoke.MethodHandles; +import java.security.Principal; +import java.util.Map; +import java.util.Set; + +import org.apache.solr.common.SolrException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Rule Based Authz plugin implementation which reads user roles from the request. This requires + * a Principal implementing VerifiedUserRoles interface, e.g. JWTAuthenticationPlugin + */ +public class ExternalRoleRuleBasedAuthorizationPlugin extends RuleBasedAuthorizationPluginBase { + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + @Override + public void init(Map initInfo) { + super.init(initInfo); + if (initInfo.containsKey("user-role")) { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Configuration should not contain 'user-role' mappings"); + } + } + + /** + * Pulls roles from the Principal + * @param principal the user Principal from the request, typically from an Authentication Plugin + * @return set of roles as strings + */ + @Override + protected Set getUserRoles(Principal principal) { + if(principal instanceof VerifiedUserRoles) { + return ((VerifiedUserRoles)principal).getVerifiedRoles(); + } else { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Request does not contain a Principal with roles"); + } + } +} diff --git a/solr/core/src/java/org/apache/solr/security/PrincipalWithUserRoles.java b/solr/core/src/java/org/apache/solr/security/PrincipalWithUserRoles.java new file mode 100644 index 000000000000..185a250939aa --- /dev/null +++ b/solr/core/src/java/org/apache/solr/security/PrincipalWithUserRoles.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security; + +import java.io.Serializable; +import java.security.Principal; +import java.util.List; +import java.util.Set; + +import org.apache.http.util.Args; + +/** + * Type of Principal object that can contain also a list of roles the user has. + * One use case can be to keep track of user-role mappings in an Identity Server + * external to Solr and pass the information to Solr in a signed JWT token or in + * another secure manner. The role information can then be used to authorize + * requests without the need to maintain or lookup what roles each user belongs to. + */ +public class PrincipalWithUserRoles implements Principal, VerifiedUserRoles, Serializable { + private static final long serialVersionUID = 4144666467522831388L; + private final String username; + + private final Set roles; + + /** + * User principal with user name as well as one or more roles that he/she belong to + * @param username string with user name for user + * @param roles a set of roles that we know this user belongs to, or empty list for no roles + */ + public PrincipalWithUserRoles(final String username, Set roles) { + super(); + Args.notNull(username, "User name"); + Args.notNull(roles, "User roles"); + this.username = username; + this.roles = roles; + } + + /** + * Returns the name of this principal. + * + * @return the name of this principal. + */ + @Override + public String getName() { + return this.username; + } + + /** + * Gets the list of roles + */ + @Override + public Set getVerifiedRoles() { + return roles; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PrincipalWithUserRoles that = (PrincipalWithUserRoles) o; + + if (!username.equals(that.username)) return false; + return roles.equals(that.roles); + } + + @Override + public int hashCode() { + int result = username.hashCode(); + result = 31 * result + roles.hashCode(); + return result; + } + + @Override + public String toString() { + return "PrincipalWithUserRoles{" + + "username='" + username + '\'' + + ", roles=" + roles + + '}'; + } +} diff --git a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java index 6bf2822d6240..e69d10a61f7e 100644 --- a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java @@ -16,229 +16,45 @@ */ package org.apache.solr.security; -import java.io.IOException; import java.lang.invoke.MethodHandles; import java.security.Principal; -import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Function; -import org.apache.solr.common.SpecProvider; -import org.apache.solr.common.util.Utils; -import org.apache.solr.common.util.ValidatingJsonMap; -import org.apache.solr.common.util.CommandOperation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static java.util.Arrays.asList; -import static java.util.Collections.unmodifiableMap; -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.toMap; -import static org.apache.solr.handler.admin.SecurityConfHandler.getListValue; import static org.apache.solr.handler.admin.SecurityConfHandler.getMapValue; - -public class RuleBasedAuthorizationPlugin implements AuthorizationPlugin, ConfigEditablePlugin, SpecProvider { +/** + * Original implementation of Rule Based Authz plugin which configures user/role + * mapping in the security.json configuration + */ +public class RuleBasedAuthorizationPlugin extends RuleBasedAuthorizationPluginBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final Map> usersVsRoles = new HashMap<>(); - private final Map mapping = new HashMap<>(); - private final List permissions = new ArrayList<>(); - - - private static class WildCardSupportMap extends HashMap> { - final Set wildcardPrefixes = new HashSet<>(); - - @Override - public List put(String key, List value) { - if (key != null && key.endsWith("/*")) { - key = key.substring(0, key.length() - 2); - wildcardPrefixes.add(key); - } - return super.put(key, value); - } - - @Override - public List get(Object key) { - List result = super.get(key); - if (key == null || result != null) return result; - if (!wildcardPrefixes.isEmpty()) { - for (String s : wildcardPrefixes) { - if (key.toString().startsWith(s)) { - List l = super.get(s); - if (l != null) { - result = result == null ? new ArrayList<>() : new ArrayList<>(result); - result.addAll(l); - } - } - } - } - return result; - } - } - - @Override - public AuthorizationResponse authorize(AuthorizationContext context) { - List collectionRequests = context.getCollectionRequests(); - if (context.getRequestType() == AuthorizationContext.RequestType.ADMIN) { - MatchStatus flag = checkCollPerm(mapping.get(null), context); - return flag.rsp; - } - - for (AuthorizationContext.CollectionRequest collreq : collectionRequests) { - //check permissions for each collection - MatchStatus flag = checkCollPerm(mapping.get(collreq.collectionName), context); - if (flag != MatchStatus.NO_PERMISSIONS_FOUND) return flag.rsp; - } - //check wildcard (all=*) permissions. - MatchStatus flag = checkCollPerm(mapping.get("*"), context); - return flag.rsp; - } - - private MatchStatus checkCollPerm(Map> pathVsPerms, - AuthorizationContext context) { - if (pathVsPerms == null) return MatchStatus.NO_PERMISSIONS_FOUND; - - String path = context.getResource(); - MatchStatus flag = checkPathPerm(pathVsPerms.get(path), context); - if (flag != MatchStatus.NO_PERMISSIONS_FOUND) return flag; - return checkPathPerm(pathVsPerms.get(null), context); - } - - private MatchStatus checkPathPerm(List permissions, AuthorizationContext context) { - if (permissions == null || permissions.isEmpty()) return MatchStatus.NO_PERMISSIONS_FOUND; - Principal principal = context.getUserPrincipal(); - loopPermissions: - for (int i = 0; i < permissions.size(); i++) { - Permission permission = permissions.get(i); - if (PermissionNameProvider.values.containsKey(permission.name)) { - if (context.getHandler() instanceof PermissionNameProvider) { - PermissionNameProvider handler = (PermissionNameProvider) context.getHandler(); - PermissionNameProvider.Name permissionName = handler.getPermissionName(context); - if (permissionName == null || !permission.name.equals(permissionName.name)) { - continue; - } - } else { - //all is special. it can match any - if(permission.wellknownName != PermissionNameProvider.Name.ALL) continue; - } - } else { - if (permission.method != null && !permission.method.contains(context.getHttpMethod())) { - //this permissions HTTP method does not match this rule. try other rules - continue; - } - if (permission.params != null) { - for (Map.Entry> e : permission.params.entrySet()) { - String[] paramVal = context.getParams().getParams(e.getKey()); - if(!e.getValue().apply(paramVal)) continue loopPermissions; - } - } - } - - if (permission.role == null) { - //no role is assigned permission.That means everybody is allowed to access - return MatchStatus.PERMITTED; - } - if (principal == null) { - log.info("request has come without principal. failed permission {} ",permission); - //this resource needs a principal but the request has come without - //any credential. - return MatchStatus.USER_REQUIRED; - } else if (permission.role.contains("*")) { - return MatchStatus.PERMITTED; - } - - for (String role : permission.role) { - Set userRoles = usersVsRoles.get(principal.getName()); - if (userRoles != null && userRoles.contains(role)) return MatchStatus.PERMITTED; - } - log.info("This resource is configured to have a permission {}, The principal {} does not have the right role ", permission, principal); - return MatchStatus.FORBIDDEN; - } - log.debug("No permissions configured for the resource {} . So allowed to access", context.getResource()); - return MatchStatus.NO_PERMISSIONS_FOUND; - } @Override public void init(Map initInfo) { - mapping.put(null, new WildCardSupportMap()); + super.init(initInfo); Map map = getMapValue(initInfo, "user-role"); for (Object o : map.entrySet()) { Map.Entry e = (Map.Entry) o; String roleName = (String) e.getKey(); usersVsRoles.put(roleName, Permission.readValueAsSet(map, roleName)); } - List perms = getListValue(initInfo, "permissions"); - for (Map o : perms) { - Permission p; - try { - p = Permission.load(o); - } catch (Exception exp) { - log.error("Invalid permission ", exp); - continue; - } - permissions.add(p); - add2Mapping(p); - } - } - - //this is to do optimized lookup of permissions for a given collection/path - private void add2Mapping(Permission permission) { - for (String c : permission.collections) { - WildCardSupportMap m = mapping.get(c); - if (m == null) mapping.put(c, m = new WildCardSupportMap()); - for (String path : permission.path) { - List perms = m.get(path); - if (perms == null) m.put(path, perms = new ArrayList<>()); - perms.add(permission); - } - } - } - - - @Override - public void close() throws IOException { } - - enum MatchStatus { - USER_REQUIRED(AuthorizationResponse.PROMPT), - NO_PERMISSIONS_FOUND(AuthorizationResponse.OK), - PERMITTED(AuthorizationResponse.OK), - FORBIDDEN(AuthorizationResponse.FORBIDDEN); - - final AuthorizationResponse rsp; - - MatchStatus(AuthorizationResponse rsp) { - this.rsp = rsp; - } } - - + /** + * Implementers should calculate the users roles + * + * @param principal the user Principal from the request + * @return set of roles as strings + */ @Override - public Map edit(Map latestConf, List commands) { - for (CommandOperation op : commands) { - AutorizationEditOperation operation = ops.get(op.name); - if (operation == null) { - op.unknownOperation(); - return null; - } - latestConf = operation.edit(latestConf, op); - if (latestConf == null) return null; - - } - return latestConf; - } - - private static final Map ops = unmodifiableMap(asList(AutorizationEditOperation.values()).stream().collect(toMap(AutorizationEditOperation::getOperationName, identity()))); - - - @Override - public ValidatingJsonMap getSpec() { - return Utils.getSpec("cluster.security.RuleBasedAuthorization").getSpec(); - + protected Set getUserRoles(Principal principal) { + return usersVsRoles.get(principal.getName()); } } diff --git a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java new file mode 100644 index 000000000000..5b5589a0927a --- /dev/null +++ b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java @@ -0,0 +1,249 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.security.Principal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import org.apache.solr.common.SolrException; +import org.apache.solr.common.SpecProvider; +import org.apache.solr.common.util.CommandOperation; +import org.apache.solr.common.util.Utils; +import org.apache.solr.common.util.ValidatingJsonMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableMap; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; +import static org.apache.solr.handler.admin.SecurityConfHandler.getListValue; + +/** + * Base class for rule based authorization plugins + */ +public abstract class RuleBasedAuthorizationPluginBase implements AuthorizationPlugin, ConfigEditablePlugin, SpecProvider { + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + final Map mapping = new HashMap<>(); + final List permissions = new ArrayList<>(); + + static class WildCardSupportMap extends HashMap> { + final Set wildcardPrefixes = new HashSet<>(); + + @Override + public List put(String key, List value) { + if (key != null && key.endsWith("/*")) { + key = key.substring(0, key.length() - 2); + wildcardPrefixes.add(key); + } + return super.put(key, value); + } + + @Override + public List get(Object key) { + List result = super.get(key); + if (key == null || result != null) return result; + if (!wildcardPrefixes.isEmpty()) { + for (String s : wildcardPrefixes) { + if (key.toString().startsWith(s)) { + List l = super.get(s); + if (l != null) { + result = result == null ? new ArrayList<>() : new ArrayList<>(result); + result.addAll(l); + } + } + } + } + return result; + } + } + + @Override + public void init(Map initInfo) { + mapping.put(null, new WildCardSupportMap()); + List perms = getListValue(initInfo, "permissions"); + for (Map o : perms) { + Permission p; + try { + p = Permission.load(o); + } catch (Exception exp) { + log.error("Invalid permission ", exp); + continue; + } + permissions.add(p); + add2Mapping(p); + } + } + + @Override + public AuthorizationResponse authorize(AuthorizationContext context) { + List collectionRequests = context.getCollectionRequests(); + if (context.getRequestType() == AuthorizationContext.RequestType.ADMIN) { + MatchStatus flag = checkCollPerm(mapping.get(null), context); + return flag.rsp; + } + + for (AuthorizationContext.CollectionRequest collreq : collectionRequests) { + //check permissions for each collection + MatchStatus flag = checkCollPerm(mapping.get(collreq.collectionName), context); + if (flag != MatchStatus.NO_PERMISSIONS_FOUND) return flag.rsp; + } + //check wildcard (all=*) permissions. + MatchStatus flag = checkCollPerm(mapping.get("*"), context); + return flag.rsp; + } + + private MatchStatus checkCollPerm(Map> pathVsPerms, + AuthorizationContext context) { + if (pathVsPerms == null) return MatchStatus.NO_PERMISSIONS_FOUND; + + String path = context.getResource(); + MatchStatus flag = checkPathPerm(pathVsPerms.get(path), context); + if (flag != MatchStatus.NO_PERMISSIONS_FOUND) return flag; + return checkPathPerm(pathVsPerms.get(null), context); + } + + private MatchStatus checkPathPerm(List permissions, AuthorizationContext context) { + if (permissions == null || permissions.isEmpty()) return MatchStatus.NO_PERMISSIONS_FOUND; + Principal principal = context.getUserPrincipal(); + loopPermissions: + for (int i = 0; i < permissions.size(); i++) { + Permission permission = permissions.get(i); + if (PermissionNameProvider.values.containsKey(permission.name)) { + if (context.getHandler() instanceof PermissionNameProvider) { + PermissionNameProvider handler = (PermissionNameProvider) context.getHandler(); + PermissionNameProvider.Name permissionName = handler.getPermissionName(context); + if (permissionName == null || !permission.name.equals(permissionName.name)) { + continue; + } + } else { + //all is special. it can match any + if(permission.wellknownName != PermissionNameProvider.Name.ALL) continue; + } + } else { + if (permission.method != null && !permission.method.contains(context.getHttpMethod())) { + //this permissions HTTP method does not match this rule. try other rules + continue; + } + if (permission.params != null) { + for (Map.Entry> e : permission.params.entrySet()) { + String[] paramVal = context.getParams().getParams(e.getKey()); + if(!e.getValue().apply(paramVal)) continue loopPermissions; + } + } + } + + if (permission.role == null) { + //no role is assigned permission.That means everybody is allowed to access + return MatchStatus.PERMITTED; + } + if (principal == null) { + log.info("request has come without principal. failed permission {} ",permission); + //this resource needs a principal but the request has come without + //any credential. + return MatchStatus.USER_REQUIRED; + } else if (permission.role.contains("*")) { + return MatchStatus.PERMITTED; + } + + for (String role : permission.role) { + try { + Set userRoles = getUserRoles(principal); + if (userRoles != null && userRoles.contains(role)) return MatchStatus.PERMITTED; + } catch (SolrException se) { + log.warn("Problems finding user roles for principal " + principal.getName(), se); + return MatchStatus.FORBIDDEN; + } + } + log.info("This resource is configured to have a permission {}, The principal {} does not have the right role ", permission, principal); + return MatchStatus.FORBIDDEN; + } + log.debug("No permissions configured for the resource {} . So allowed to access", context.getResource()); + return MatchStatus.NO_PERMISSIONS_FOUND; + } + + /** + * Implementers should calculate the users roles + * @param principal the user Principal from the request + * @return set of roles as strings + */ + protected abstract Set getUserRoles(Principal principal); + + //this is to do optimized lookup of permissions for a given collection/path + void add2Mapping(Permission permission) { + for (String c : permission.collections) { + WildCardSupportMap m = mapping.get(c); + if (m == null) mapping.put(c, m = new WildCardSupportMap()); + for (String path : permission.path) { + List perms = m.get(path); + if (perms == null) m.put(path, perms = new ArrayList<>()); + perms.add(permission); + } + } + } + + + @Override + public void close() throws IOException { } + + enum MatchStatus { + USER_REQUIRED(AuthorizationResponse.PROMPT), + NO_PERMISSIONS_FOUND(AuthorizationResponse.OK), + PERMITTED(AuthorizationResponse.OK), + FORBIDDEN(AuthorizationResponse.FORBIDDEN); + + final AuthorizationResponse rsp; + + MatchStatus(AuthorizationResponse rsp) { + this.rsp = rsp; + } + } + + + @Override + public Map edit(Map latestConf, List commands) { + for (CommandOperation op : commands) { + AutorizationEditOperation operation = ops.get(op.name); + if (operation == null) { + op.unknownOperation(); + return null; + } + latestConf = operation.edit(latestConf, op); + if (latestConf == null) return null; + + } + return latestConf; + } + + private static final Map ops = unmodifiableMap(asList(AutorizationEditOperation.values()).stream().collect(toMap(AutorizationEditOperation::getOperationName, identity()))); + + + @Override + public ValidatingJsonMap getSpec() { + return Utils.getSpec("cluster.security.RuleBasedAuthorization").getSpec(); + + } +} diff --git a/solr/core/src/java/org/apache/solr/security/VerifiedUserRoles.java b/solr/core/src/java/org/apache/solr/security/VerifiedUserRoles.java new file mode 100644 index 000000000000..ed574e076340 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/security/VerifiedUserRoles.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security; + +import java.util.Set; + +/** + * Interface used to pass verified user roles in a Principal object. + * An Authorization plugin may check for the presence of verified user + * roles on the Principal and choose to use those roles instead of + * explicitly configuring roles in config. Such roles may e.g. origin + * from a signed and validated JWT token. + */ +public interface VerifiedUserRoles { + /** + * Gets a set of roles that have been verified to belong to a user + */ + Set getVerifiedRoles(); +} diff --git a/solr/solr-ref-guide/src/authentication-and-authorization-plugins.adoc b/solr/solr-ref-guide/src/authentication-and-authorization-plugins.adoc index 971dbcdf3714..fc3ad00e1b33 100644 --- a/solr/solr-ref-guide/src/authentication-and-authorization-plugins.adoc +++ b/solr/solr-ref-guide/src/authentication-and-authorization-plugins.adoc @@ -153,9 +153,10 @@ The authorization plugin is only supported in SolrCloud mode. Also, reloading th === Available Authorization Plugins -Solr has one implementation of an authorization plugin: +Solr has two implementations of authorization plugin: * <> +* <> == Securing Inter-Node Requests diff --git a/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc b/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc index d86097a88c40..572cbf0cfbdf 100644 --- a/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc +++ b/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc @@ -20,9 +20,10 @@ Solr allows configuring roles to control user access to the system. This is accomplished through rule-based permission definitions which are assigned to users. The roles are fully customizable, and provide the ability to limit access to specific collections, request handlers, request parameters, and request methods. -The roles can be used with any of the authentication plugins or with a custom authentication plugin if you have created one. You will only need to ensure that you configure the role-to-user mappings with the proper user IDs that your authentication system provides. +The roles can be used with any of the authentication plugins or with a custom authentication plugin if you have created one. You will only need to ensure that logged-in users are mapped to the roles defined by the plugin. There are two implementaions of the plugin, which only differs in how the user's roles are obtained: -Once defined through the API, roles are stored in `security.json`. +* `RuleBasedAuthorizationPlugin`: The role-to-user mappings must be defined explicitly in `security.json` for every possible authenticated user. +* `ExternalRoleRuleBasedAuthorizationPlugin`: The role-to-user mappings are managed externally. This plugin expects the user's roles to be present on the `Principal` object which is part of the request. == Enable the Authorization Plugin @@ -32,7 +33,8 @@ This file has two parts, the `authentication` part and the `authorization` part. The `authorization` part is not related to Basic authentication, but is a separate authorization plugin designed to support fine-grained user access control. When creating `security.json` you can add the permissions to the file, or you can use the Authorization API described below to add them as needed. -This example `security.json` shows how the <> can work with this authorization plugin: +=== Example for RuleBasedAuthorizationPlugin +This example `security.json` shows how the <> can work with the `RuleBasedAuthorizationPlugin` plugin: [source,json] ---- @@ -59,6 +61,34 @@ There are several things defined in this example: <5> The 'admin' role has been defined, and it has permission to edit security settings. <6> The 'solr' user has been defined to the 'admin' role. +=== Example for ExternalRoleRuleBasedAuthorizationPlugin +This example `security.json` shows how an imagined `JWTAuthPlugin`, which pulls user and user roles from JWT claims, can work with the `ExternalRoleRuleBasedAuthorizationPlugin` plugin: + +[source,json] +---- +{ +"authentication":{ + "class": "solr.JWTAuthPlugin", <1> + "jwk_url": "https://my.identity.provider/my.wks", <2> + "roles_claim": "groups" <3> +}, +"authorization":{ + "class":"solr.ExternalRoleRuleBasedAuthorizationPlugin", <4> + "permissions":[{"name":"security-edit", + "role":"admin"}] <5> +}} +---- + +Let's walk through this example: + +<1> JWT Authentication plugin is enabled +<2> Public key to Identity server is pulled over https +<3> We expect each JWT token to contain a "groups" claim, which will be passed on to Authorization +<4> External Role Rule-based authorization plugin is enabled. +<5> The 'admin' role has been defined, and it has permission to edit security settings. + +Only requests from users having a JWT token with role "admin" will be granted the `security-edit` permission. + == Permission Attributes Each role is comprised of one or more permissions which define what the user is allowed to do. Each permission is made up of several attributes that define the allowed activity. There are some pre-defined permissions which cannot be modified. From cb6dcb0e10e5959c2f5589c4241d5402f400725a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 21 Mar 2018 09:36:00 +0100 Subject: [PATCH 02/24] Pass context to getUserRoles() --- .../ExternalRoleRuleBasedAuthorizationPlugin.java | 8 ++++---- .../solr/security/RuleBasedAuthorizationPlugin.java | 9 ++++----- .../solr/security/RuleBasedAuthorizationPluginBase.java | 8 ++++---- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java b/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java index dd7ec0cda24d..d5f98b8c7867 100644 --- a/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java @@ -42,13 +42,13 @@ public void init(Map initInfo) { /** * Pulls roles from the Principal - * @param principal the user Principal from the request, typically from an Authentication Plugin + * @param context the Authorization context from which to find the user Principal * @return set of roles as strings */ @Override - protected Set getUserRoles(Principal principal) { - if(principal instanceof VerifiedUserRoles) { - return ((VerifiedUserRoles)principal).getVerifiedRoles(); + protected Set getUserRoles(AuthorizationContext context) { + if(context.getUserPrincipal() instanceof VerifiedUserRoles) { + return ((VerifiedUserRoles)context.getUserPrincipal()).getVerifiedRoles(); } else { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Request does not contain a Principal with roles"); } diff --git a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java index e69d10a61f7e..46cd38271117 100644 --- a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java @@ -48,13 +48,12 @@ public void init(Map initInfo) { } /** - * Implementers should calculate the users roles - * - * @param principal the user Principal from the request + * Returns roles of the user based on the configured user-role map + * @param context the Authorization context from which to pull username * @return set of roles as strings */ @Override - protected Set getUserRoles(Principal principal) { - return usersVsRoles.get(principal.getName()); + protected Set getUserRoles(AuthorizationContext context) { + return usersVsRoles.get(context.getUserPrincipal().getName()); } } diff --git a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java index 5b5589a0927a..05d572eee3a8 100644 --- a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java +++ b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java @@ -171,7 +171,7 @@ private MatchStatus checkPathPerm(List permissions, AuthorizationCon for (String role : permission.role) { try { - Set userRoles = getUserRoles(principal); + Set userRoles = getUserRoles(context); if (userRoles != null && userRoles.contains(role)) return MatchStatus.PERMITTED; } catch (SolrException se) { log.warn("Problems finding user roles for principal " + principal.getName(), se); @@ -186,11 +186,11 @@ private MatchStatus checkPathPerm(List permissions, AuthorizationCon } /** - * Implementers should calculate the users roles - * @param principal the user Principal from the request + * Finds users roles + * @param authorizationContext the authorization context, which contains the Principal mm * @return set of roles as strings */ - protected abstract Set getUserRoles(Principal principal); + protected abstract Set getUserRoles(AuthorizationContext authorizationContext); //this is to do optimized lookup of permissions for a given collection/path void add2Mapping(Permission permission) { From 3beba9613d903e9b560885d12e058940d3edd4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 21 Mar 2018 11:21:19 +0100 Subject: [PATCH 03/24] Added test class for ExternalRole --- ...ernalRoleRuleBasedAuthorizationPlugin.java | 87 ++++++++ .../TestRuleBasedAuthorizationPlugin.java | 191 ++++++++++-------- 2 files changed, 195 insertions(+), 83 deletions(-) create mode 100644 solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java diff --git a/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java b/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java new file mode 100644 index 000000000000..ae89194c5a94 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.solr.security; + +import java.security.Principal; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import org.apache.http.auth.BasicUserPrincipal; +import org.apache.solr.common.util.Utils; + +public class TestExternalRoleRuleBasedAuthorizationPlugin extends TestRuleBasedAuthorizationPlugin { + private HashMap principals; + + @Override + public void setUp() throws Exception { + super.setUp(); + permissions = "{" + + " permissions : [" + + " {name:'schema-edit'," + + " role:admin}," + + " {name:'collection-admin-read'," + + " role:null}," + + " {name:collection-admin-edit ," + + " role:admin}," + + " {name:mycoll_update," + + " collection:mycoll," + + " path:'/update/*'," + + " role:[dev,admin]" + + " }," + + "{name:read , role:dev }," + + "{name:freeforall, path:'/foo', role:'*'}]}"; + + rules = (Map) Utils.fromJSONString(permissions); + + principals = new HashMap<>(); + setUserRoles("steve", "dev", "user"); + setUserRoles("tim", "dev", "admin"); + setUserRoles("joe", "user"); + setUserRoles("noble", "dev", "user"); + } + + protected void setUserRoles(String user, String... roles) { + principals.put(user, new PrincipalWithUserRoles(user, new HashSet<>(Arrays.asList(roles)))); + } + + @Override + protected void setUserRole(String user, String role) { + principals.put(user, new PrincipalWithUserRoles(user, Collections.singleton(role))); + } + + @Override + AuthorizationContext getMockContext(Map values) { + return new MockAuthorizationContext(values) { + @Override + public Principal getUserPrincipal() { + String userPrincipal = (String) values.get("userPrincipal"); + return userPrincipal == null ? null : + principals.get(userPrincipal) != null ? principals.get(userPrincipal) : + new BasicUserPrincipal(userPrincipal); + } + }; + } + + @Override + protected RuleBasedAuthorizationPluginBase createPlugin() { + return new ExternalRoleRuleBasedAuthorizationPlugin(); + } +} diff --git a/solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java b/solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java index bacfc10ad022..b9d003110618 100644 --- a/solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java +++ b/solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java @@ -26,6 +26,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.apache.http.auth.BasicUserPrincipal; import org.apache.solr.SolrTestCaseJ4; @@ -42,6 +43,7 @@ import org.apache.solr.security.AuthorizationContext.CollectionRequest; import org.apache.solr.security.AuthorizationContext.RequestType; import org.apache.solr.common.util.CommandOperation; +import org.junit.Test; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; @@ -50,35 +52,41 @@ import static org.apache.solr.common.util.CommandOperation.captureErrors; public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { - String permissions = "{" + - " user-role : {" + - " steve: [dev,user]," + - " tim: [dev,admin]," + - " joe: [user]," + - " noble:[dev,user]" + - " }," + - " permissions : [" + - " {name:'schema-edit'," + - " role:admin}," + - " {name:'collection-admin-read'," + - " role:null}," + - " {name:collection-admin-edit ," + - " role:admin}," + - " {name:mycoll_update," + - " collection:mycoll," + - " path:'/update/*'," + - " role:[dev,admin]" + - " }," + - "{name:read , role:dev }," + - "{name:freeforall, path:'/foo', role:'*'}]}"; - - + protected String permissions; + protected Map rules; + + final int STATUS_OK = 200; + final int FORBIDDEN = 403; + final int PROMPT_FOR_CREDENTIALS = 401; + + @Override + public void setUp() throws Exception { + super.setUp(); + permissions = "{" + + " user-role : {" + + " steve: [dev,user]," + + " tim: [dev,admin]," + + " joe: [user]," + + " noble:[dev,user]" + + " }," + + " permissions : [" + + " {name:'schema-edit'," + + " role:admin}," + + " {name:'collection-admin-read'," + + " role:null}," + + " {name:collection-admin-edit ," + + " role:admin}," + + " {name:mycoll_update," + + " collection:mycoll," + + " path:'/update/*'," + + " role:[dev,admin]" + + " }," + + "{name:read , role:dev }," + + "{name:freeforall, path:'/foo', role:'*'}]}"; + rules = (Map) Utils.fromJSONString(permissions); + } public void testBasicPermissions() { - int STATUS_OK = 200; - int FORBIDDEN = 403; - int PROMPT_FOR_CREDENTIALS = 401; - checkRules(makeMap("resource", "/update/json/docs", "httpMethod", "POST", "userPrincipal", "unknownuser", @@ -93,7 +101,6 @@ public void testBasicPermissions() { "handler", new UpdateRequestHandler()) , STATUS_OK); - checkRules(makeMap("resource", "/update/json/docs", "httpMethod", "POST", "collectionRequests", "mycoll", @@ -111,8 +118,7 @@ public void testBasicPermissions() { "userPrincipal", "somebody", "collectionRequests", "mycoll", "httpMethod", "GET", - "handler", new SchemaHandler() - ) + "handler", new SchemaHandler()) , STATUS_OK); checkRules(makeMap("resource", "/schema/fields", @@ -162,8 +168,7 @@ public void testBasicPermissions() { "handler", new CollectionsHandler(), "params", new MapSolrParams(singletonMap("action", "RELOAD"))) , PROMPT_FOR_CREDENTIALS); - - + checkRules(makeMap("resource", "/admin/collections", "userPrincipal", "somebody", "requestType", RequestType.ADMIN, @@ -188,23 +193,22 @@ public void testBasicPermissions() { , FORBIDDEN); - Map rules = (Map) Utils.fromJSONString(permissions); - ((Map)rules.get("user-role")).put("cio","su"); - ((List)rules.get("permissions")).add( makeMap("name", "all", "role", "su")); - + setUserRole("cio", "su"); + addPermission("all", "su"); + checkRules(makeMap("resource", ReplicationHandler.PATH, "httpMethod", "POST", "userPrincipal", "tim", "handler", new ReplicationHandler(), "collectionRequests", singletonList(new CollectionRequest("mycoll")) ) - , FORBIDDEN, rules); + , FORBIDDEN); checkRules(makeMap("resource", ReplicationHandler.PATH, "httpMethod", "POST", "userPrincipal", "cio", "handler", new ReplicationHandler(), "collectionRequests", singletonList(new CollectionRequest("mycoll")) ) - , STATUS_OK, rules); + , STATUS_OK); checkRules(makeMap("resource", "/admin/collections", "userPrincipal", "tim", @@ -212,14 +216,12 @@ public void testBasicPermissions() { "collectionRequests", null, "handler", new CollectionsHandler(), "params", new MapSolrParams(singletonMap("action", "CREATE"))) - , STATUS_OK, rules); + , STATUS_OK); - rules = (Map) Utils.fromJSONString(permissions); - ((List)rules.get("permissions")).add( makeMap("name", "core-admin-edit", "role", "su")); - ((List)rules.get("permissions")).add( makeMap("name", "core-admin-read", "role", "user")); - ((Map)rules.get("user-role")).put("cio","su"); - ((List)rules.get("permissions")).add( makeMap("name", "all", "role", "su")); - permissions = Utils.toJSONString(rules); + addPermission("core-admin-edit", "su"); + addPermission("core-admin-read", "user"); + setUserRole("cio", "su"); + addPermission("all", "su"); checkRules(makeMap("resource", "/admin/cores", "userPrincipal", null, @@ -237,7 +239,7 @@ public void testBasicPermissions() { "params", new MapSolrParams(singletonMap("action", "CREATE"))) , FORBIDDEN); - checkRules(makeMap("resource", "/admin/cores", + checkRules(makeMap("resource", "/admin/cores", "userPrincipal", "joe", "requestType", RequestType.ADMIN, "collectionRequests", null, @@ -251,14 +253,10 @@ public void testBasicPermissions() { "collectionRequests", null, "handler", new CoreAdminHandler(null), "params", new MapSolrParams(singletonMap("action", "CREATE"))) - ,STATUS_OK ); + ,STATUS_OK); - rules = (Map) Utils.fromJSONString(permissions); - List permissions = (List) rules.get("permissions"); - permissions.remove(permissions.size() -1);//remove the 'all' permission - permissions.add(makeMap("name", "test-params", "role", "admin", "path", "/x", "params", - makeMap("key", Arrays.asList("REGEX:(?i)val1", "VAL2")))); - this.permissions = Utils.toJSONString(rules); + removePermission("all"); + addPermission("test-params", "admin", "/x", makeMap("key", Arrays.asList("REGEX:(?i)val1", "VAL2"))); checkRules(makeMap("resource", "/x", "userPrincipal", null, @@ -283,6 +281,7 @@ public void testBasicPermissions() { "handler", new DumpRequestHandler(), "params", new MapSolrParams(singletonMap("key", "Val1"))) , PROMPT_FOR_CREDENTIALS); + checkRules(makeMap("resource", "/x", "userPrincipal", "joe", "requestType", RequestType.UNKNOWN, @@ -298,6 +297,7 @@ public void testBasicPermissions() { "handler", new DumpRequestHandler(), "params", new MapSolrParams(singletonMap("key", "Val2"))) , STATUS_OK); + checkRules(makeMap("resource", "/x", "userPrincipal", "joe", "requestType", RequestType.UNKNOWN, @@ -306,20 +306,46 @@ public void testBasicPermissions() { "params", new MapSolrParams(singletonMap("key", "VAL2"))) , FORBIDDEN); + Map customRules = (Map) Utils.fromJSONString( + "{permissions:[" + + " {name:update, role:[admin_role,update_role]}," + + " {name:read, role:[admin_role,update_role,read_role]}" + + "]}"); + + clearUserRoles(); + setUserRole("admin", "admin_role"); + setUserRole("update", "update_role"); + setUserRole("solr", "read_role"); + checkRules(makeMap("resource", "/update", "userPrincipal", "solr", "requestType", RequestType.UNKNOWN, "collectionRequests", "go", "handler", new UpdateRequestHandler(), "params", new MapSolrParams(singletonMap("key", "VAL2"))) - , FORBIDDEN, (Map) Utils.fromJSONString( "{user-role:{" + - " admin:[admin_role]," + - " update:[update_role]," + - " solr:[read_role]}," + - " permissions:[" + - " {name:update, role:[admin_role,update_role]}," + - " {name:read, role:[admin_role,update_role,read_role]}" + - "]}")); + , FORBIDDEN, customRules); + } + + void addPermission(String permissionName, String role, String path, Map params) { + ((List)rules.get("permissions")).add( makeMap("name", permissionName, "role", role, "path", path, "params", params)); + } + + void removePermission(String name) { + List> oldPerm = ((List) rules.get("permissions")); + List> newPerm = oldPerm.stream().filter(p -> !p.get("name").equals(name)).collect(Collectors.toList()); + rules.put("permissions", newPerm); + } + + protected void addPermission(String permissionName, String role) { + ((List)rules.get("permissions")).add( makeMap("name", permissionName, "role", role)); + } + + void clearUserRoles() { + rules.put("user-role", new HashMap()); + } + + protected void setUserRole(String user, String role) { + ((Map)rules.get("user-role")).put("cio","su"); } public void testEditRules() throws IOException { @@ -371,13 +397,13 @@ public Object getVal(String path){ } } - private void checkRules(Map values, int expected) { - checkRules(values,expected,(Map) Utils.fromJSONString(permissions)); + void checkRules(Map values, int expected) { + checkRules(values, expected, rules); } - private void checkRules(Map values, int expected, Map permissions) { - AuthorizationContext context = new MockAuthorizationContext(values); - try (RuleBasedAuthorizationPlugin plugin = new RuleBasedAuthorizationPlugin()) { + void checkRules(Map values, int expected, Map permissions) { + AuthorizationContext context = getMockContext(values); + try (RuleBasedAuthorizationPluginBase plugin = createPlugin()) { plugin.init(permissions); AuthorizationResponse authResp = plugin.authorize(context); assertEquals(expected, authResp.statusCode); @@ -386,10 +412,24 @@ private void checkRules(Map values, int expected, Map values) { + return new MockAuthorizationContext(values) { + @Override + public Principal getUserPrincipal() { + Object userPrincipal = values.get("userPrincipal"); + return userPrincipal == null ? null : new BasicUserPrincipal(String.valueOf(userPrincipal)); + } + }; + } + + protected abstract class MockAuthorizationContext extends AuthorizationContext { private final Map values; - private MockAuthorizationContext(Map values) { + public MockAuthorizationContext(Map values) { this.values = values; } @@ -399,12 +439,6 @@ public SolrParams getParams() { return params == null ? new MapSolrParams(new HashMap()) : params; } - @Override - public Principal getUserPrincipal() { - Object userPrincipal = values.get("userPrincipal"); - return userPrincipal == null ? null : new BasicUserPrincipal(String.valueOf(userPrincipal)); - } - @Override public String getHttpHeader(String header) { return null; @@ -456,13 +490,4 @@ public String getResource() { } } -static String testPerms = "{user-role:{" + - " admin:[admin_role]," + - " update:[update_role]," + - " solr:[read_role]}," + - " permissions:[" + - " {name:update,role:[admin_role,update_role]}," + - " {name:read,role:[admin_role,update_role,read_role]" + - "]}"; - } From 191fa50332976845f9a96134e791414f4dcf30e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Mon, 15 Apr 2019 23:01:29 +0200 Subject: [PATCH 04/24] Merge --- .../solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java | 1 - .../org/apache/solr/security/RuleBasedAuthorizationPlugin.java | 1 - 2 files changed, 2 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java b/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java index d5f98b8c7867..a3d7bc1b6da4 100644 --- a/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java @@ -17,7 +17,6 @@ package org.apache.solr.security; import java.lang.invoke.MethodHandles; -import java.security.Principal; import java.util.Map; import java.util.Set; diff --git a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java index 46cd38271117..77155ff058aa 100644 --- a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java @@ -17,7 +17,6 @@ package org.apache.solr.security; import java.lang.invoke.MethodHandles; -import java.security.Principal; import java.util.HashMap; import java.util.Map; import java.util.Set; From 51cbd4fc486d8c72f84d03a6aa93391e59907476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Tue, 16 Apr 2019 10:17:32 +0200 Subject: [PATCH 05/24] Merge in changes from old branch with newer all permission work --- .../solr/security/PrincipalWithUserRoles.java | 1 - .../TestRuleBasedAuthorizationPlugin.java | 181 +++++++++++------- 2 files changed, 108 insertions(+), 74 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/security/PrincipalWithUserRoles.java b/solr/core/src/java/org/apache/solr/security/PrincipalWithUserRoles.java index 185a250939aa..01479e9f2622 100644 --- a/solr/core/src/java/org/apache/solr/security/PrincipalWithUserRoles.java +++ b/solr/core/src/java/org/apache/solr/security/PrincipalWithUserRoles.java @@ -18,7 +18,6 @@ import java.io.Serializable; import java.security.Principal; -import java.util.List; import java.util.Set; import org.apache.http.util.Args; diff --git a/solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java b/solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java index 4e1e9aad3e71..5aeb8a059209 100644 --- a/solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java +++ b/solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java @@ -26,6 +26,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.apache.http.auth.BasicUserPrincipal; import org.apache.solr.SolrTestCaseJ4; @@ -55,35 +56,43 @@ import static org.apache.solr.common.util.Utils.makeMap; import static org.apache.solr.common.util.CommandOperation.captureErrors; +@SuppressWarnings("unchecked") public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { - private static final int STATUS_OK = 200; - private static final int FORBIDDEN = 403; - private static final int PROMPT_FOR_CREDENTIALS = 401; - - String permissions = "{" + - " user-role : {" + - " steve: [dev,user]," + - " tim: [dev,admin]," + - " joe: [user]," + - " noble:[dev,user]" + - " }," + - " permissions : [" + - " {name:'schema-edit'," + - " role:admin}," + - " {name:'collection-admin-read'," + - " role:null}," + - " {name:collection-admin-edit ," + - " role:admin}," + - " {name:mycoll_update," + - " collection:mycoll," + - " path:'/update/*'," + - " role:[dev,admin]" + - " }," + - "{name:read , role:dev }," + - "{name:freeforall, path:'/foo', role:'*'}]}"; - - + protected String permissions; + protected Map rules; + + final int STATUS_OK = 200; + final int FORBIDDEN = 403; + final int PROMPT_FOR_CREDENTIALS = 401; + + @Override + public void setUp() throws Exception { + super.setUp(); + permissions = "{" + + " user-role : {" + + " steve: [dev,user]," + + " tim: [dev,admin]," + + " joe: [user]," + + " noble:[dev,user]" + + " }," + + " permissions : [" + + " {name:'schema-edit'," + + " role:admin}," + + " {name:'collection-admin-read'," + + " role:null}," + + " {name:collection-admin-edit ," + + " role:admin}," + + " {name:mycoll_update," + + " collection:mycoll," + + " path:'/update/*'," + + " role:[dev,admin]" + + " }," + + "{name:read , role:dev }," + + "{name:freeforall, path:'/foo', role:'*'}]}"; + rules = (Map) Utils.fromJSONString(permissions); + } + @SuppressWarnings("unchecked") public void testBasicPermissions() { checkRules(makeMap("resource", "/update/json/docs", "httpMethod", "POST", @@ -99,7 +108,6 @@ public void testBasicPermissions() { "handler", new UpdateRequestHandler()) , STATUS_OK); - checkRules(makeMap("resource", "/update/json/docs", "httpMethod", "POST", "collectionRequests", "mycoll", @@ -117,8 +125,7 @@ public void testBasicPermissions() { "userPrincipal", "somebody", "collectionRequests", "mycoll", "httpMethod", "GET", - "handler", new SchemaHandler() - ) + "handler", new SchemaHandler()) , STATUS_OK); checkRules(makeMap("resource", "/schema/fields", @@ -169,7 +176,6 @@ public void testBasicPermissions() { "params", new MapSolrParams(singletonMap("action", "RELOAD"))) , PROMPT_FOR_CREDENTIALS); - checkRules(makeMap("resource", "/admin/collections", "userPrincipal", "somebody", "requestType", RequestType.ADMIN, @@ -194,23 +200,22 @@ public void testBasicPermissions() { , FORBIDDEN); - Map rules = (Map) Utils.fromJSONString(permissions); - ((Map)rules.get("user-role")).put("cio","su"); - ((List)rules.get("permissions")).add( makeMap("name", "all", "role", "su")); + setUserRole("cio", "su"); + addPermission("all", "su"); checkRules(makeMap("resource", ReplicationHandler.PATH, "httpMethod", "POST", "userPrincipal", "tim", "handler", new ReplicationHandler(), "collectionRequests", singletonList(new CollectionRequest("mycoll")) ) - , FORBIDDEN, rules); + , FORBIDDEN); checkRules(makeMap("resource", ReplicationHandler.PATH, "httpMethod", "POST", "userPrincipal", "cio", "handler", new ReplicationHandler(), "collectionRequests", singletonList(new CollectionRequest("mycoll")) ) - , STATUS_OK, rules); + , STATUS_OK); checkRules(makeMap("resource", "/admin/collections", "userPrincipal", "tim", @@ -218,14 +223,12 @@ public void testBasicPermissions() { "collectionRequests", null, "handler", new CollectionsHandler(), "params", new MapSolrParams(singletonMap("action", "CREATE"))) - , STATUS_OK, rules); + , STATUS_OK); - rules = (Map) Utils.fromJSONString(permissions); - ((List)rules.get("permissions")).add( makeMap("name", "core-admin-edit", "role", "su")); - ((List)rules.get("permissions")).add( makeMap("name", "core-admin-read", "role", "user")); - ((Map)rules.get("user-role")).put("cio","su"); - ((List)rules.get("permissions")).add( makeMap("name", "all", "role", "su")); - permissions = Utils.toJSONString(rules); + addPermission("core-admin-edit", "su"); + addPermission("core-admin-read", "user"); + setUserRole("cio", "su"); + addPermission("all", "su"); checkRules(makeMap("resource", "/admin/cores", "userPrincipal", null, @@ -243,7 +246,7 @@ public void testBasicPermissions() { "params", new MapSolrParams(singletonMap("action", "CREATE"))) , FORBIDDEN); - checkRules(makeMap("resource", "/admin/cores", + checkRules(makeMap("resource", "/admin/cores", "userPrincipal", "joe", "requestType", RequestType.ADMIN, "collectionRequests", null, @@ -257,14 +260,10 @@ public void testBasicPermissions() { "collectionRequests", null, "handler", new CoreAdminHandler(null), "params", new MapSolrParams(singletonMap("action", "CREATE"))) - ,STATUS_OK ); + ,STATUS_OK); - rules = (Map) Utils.fromJSONString(permissions); - List permissions = (List) rules.get("permissions"); - permissions.remove(permissions.size() -1);//remove the 'all' permission - permissions.add(makeMap("name", "test-params", "role", "admin", "path", "/x", "params", - makeMap("key", Arrays.asList("REGEX:(?i)val1", "VAL2")))); - this.permissions = Utils.toJSONString(rules); + removePermission("all"); + addPermission("test-params", "admin", "/x", makeMap("key", Arrays.asList("REGEX:(?i)val1", "VAL2"))); checkRules(makeMap("resource", "/x", "userPrincipal", null, @@ -289,6 +288,7 @@ public void testBasicPermissions() { "handler", new DumpRequestHandler(), "params", new MapSolrParams(singletonMap("key", "Val1"))) , PROMPT_FOR_CREDENTIALS); + checkRules(makeMap("resource", "/x", "userPrincipal", "joe", "requestType", RequestType.UNKNOWN, @@ -304,6 +304,7 @@ public void testBasicPermissions() { "handler", new DumpRequestHandler(), "params", new MapSolrParams(singletonMap("key", "Val2"))) , STATUS_OK); + checkRules(makeMap("resource", "/x", "userPrincipal", "joe", "requestType", RequestType.UNKNOWN, @@ -312,20 +313,24 @@ public void testBasicPermissions() { "params", new MapSolrParams(singletonMap("key", "VAL2"))) , FORBIDDEN); + Map customRules = (Map) Utils.fromJSONString( + "{permissions:[" + + " {name:update, role:[admin_role,update_role]}," + + " {name:read, role:[admin_role,update_role,read_role]}" + + "]}"); + + clearUserRoles(); + setUserRole("admin", "admin_role"); + setUserRole("update", "update_role"); + setUserRole("solr", "read_role"); + checkRules(makeMap("resource", "/update", "userPrincipal", "solr", "requestType", RequestType.UNKNOWN, "collectionRequests", "go", "handler", new UpdateRequestHandler(), "params", new MapSolrParams(singletonMap("key", "VAL2"))) - , FORBIDDEN, (Map) Utils.fromJSONString( "{user-role:{" + - " admin:[admin_role]," + - " update:[update_role]," + - " solr:[read_role]}," + - " permissions:[" + - " {name:update, role:[admin_role,update_role]}," + - " {name:read, role:[admin_role,update_role,read_role]}" + - "]}")); + , FORBIDDEN, customRules); } /* @@ -449,6 +454,28 @@ public void testAllPermissionDeniesActionsWhenUserIsNotCorrectRole() { "]}")); } + void addPermission(String permissionName, String role, String path, Map params) { + ((List)rules.get("permissions")).add( makeMap("name", permissionName, "role", role, "path", path, "params", params)); + } + + void removePermission(String name) { + List> oldPerm = ((List) rules.get("permissions")); + List> newPerm = oldPerm.stream().filter(p -> !p.get("name").equals(name)).collect(Collectors.toList()); + rules.put("permissions", newPerm); + } + + protected void addPermission(String permissionName, String role) { + ((List)rules.get("permissions")).add( makeMap("name", permissionName, "role", role)); + } + + void clearUserRoles() { + rules.put("user-role", new HashMap()); + } + + protected void setUserRole(String user, String role) { + ((Map)rules.get("user-role")).put("cio","su"); + } + public void testEditRules() throws IOException { Perms perms = new Perms(); perms.runCmd("{set-permission : {name: config-edit, role: admin } }", true); @@ -498,13 +525,13 @@ public Object getVal(String path){ } } - private void checkRules(Map values, int expected) { - checkRules(values,expected,(Map) Utils.fromJSONString(permissions)); + void checkRules(Map values, int expected) { + checkRules(values, expected, rules); } - private void checkRules(Map values, int expected, Map permissions) { - AuthorizationContext context = new MockAuthorizationContext(values); - try (RuleBasedAuthorizationPlugin plugin = new RuleBasedAuthorizationPlugin()) { + void checkRules(Map values, int expected, Map permissions) { + AuthorizationContext context = getMockContext(values); + try (RuleBasedAuthorizationPluginBase plugin = createPlugin()) { plugin.init(permissions); AuthorizationResponse authResp = plugin.authorize(context); assertEquals(expected, authResp.statusCode); @@ -513,23 +540,31 @@ private void checkRules(Map values, int expected, Map values) { + return new MockAuthorizationContext(values) { + @Override + public Principal getUserPrincipal() { + Object userPrincipal = values.get("userPrincipal"); + return userPrincipal == null ? null : new BasicUserPrincipal(String.valueOf(userPrincipal)); + } + }; + } + + protected abstract class MockAuthorizationContext extends AuthorizationContext { private final Map values; - private MockAuthorizationContext(Map values) { + public MockAuthorizationContext(Map values) { this.values = values; } @Override public SolrParams getParams() { SolrParams params = (SolrParams) values.get("params"); - return params == null ? new MapSolrParams(new HashMap()) : params; - } - - @Override - public Principal getUserPrincipal() { - Object userPrincipal = values.get("userPrincipal"); - return userPrincipal == null ? null : new BasicUserPrincipal(String.valueOf(userPrincipal)); + return params == null ? new MapSolrParams(new HashMap<>()) : params; } @Override From f8050b6b973099cea7efdffb6ea4d09f0a270eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Tue, 16 Apr 2019 11:18:11 +0200 Subject: [PATCH 06/24] Merge inn the 'all' permission fixes. Change the getUserRoles() method in base class somewhat Tests pass --- ...ernalRoleRuleBasedAuthorizationPlugin.java | 9 +- .../RuleBasedAuthorizationPlugin.java | 7 +- .../RuleBasedAuthorizationPluginBase.java | 131 ++++++++++-------- .../TestRuleBasedAuthorizationPlugin.java | 55 ++++---- 4 files changed, 114 insertions(+), 88 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java b/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java index a3d7bc1b6da4..59924b52bee8 100644 --- a/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java @@ -17,6 +17,7 @@ package org.apache.solr.security; import java.lang.invoke.MethodHandles; +import java.security.Principal; import java.util.Map; import java.util.Set; @@ -41,13 +42,13 @@ public void init(Map initInfo) { /** * Pulls roles from the Principal - * @param context the Authorization context from which to find the user Principal + * @param principal the user Principal which should contain roles * @return set of roles as strings */ @Override - protected Set getUserRoles(AuthorizationContext context) { - if(context.getUserPrincipal() instanceof VerifiedUserRoles) { - return ((VerifiedUserRoles)context.getUserPrincipal()).getVerifiedRoles(); + protected Set getUserRoles(Principal principal) { + if(principal instanceof VerifiedUserRoles) { + return ((VerifiedUserRoles) principal).getVerifiedRoles(); } else { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Request does not contain a Principal with roles"); } diff --git a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java index 77155ff058aa..19ae8fb7259f 100644 --- a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java @@ -17,6 +17,7 @@ package org.apache.solr.security; import java.lang.invoke.MethodHandles; +import java.security.Principal; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -48,11 +49,11 @@ public void init(Map initInfo) { /** * Returns roles of the user based on the configured user-role map - * @param context the Authorization context from which to pull username + * @param principal the Authorization context from which to pull username * @return set of roles as strings */ @Override - protected Set getUserRoles(AuthorizationContext context) { - return usersVsRoles.get(context.getUserPrincipal().getName()); + protected Set getUserRoles(Principal principal) { + return usersVsRoles.get(principal.getName()); } } diff --git a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java index 05d572eee3a8..804f7854cea0 100644 --- a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java +++ b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java @@ -20,6 +20,7 @@ import java.lang.invoke.MethodHandles; import java.security.Principal; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -27,7 +28,6 @@ import java.util.Set; import java.util.function.Function; -import org.apache.solr.common.SolrException; import org.apache.solr.common.SpecProvider; import org.apache.solr.common.util.CommandOperation; import org.apache.solr.common.util.Utils; @@ -35,7 +35,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static java.util.Arrays.asList; import static java.util.Collections.unmodifiableMap; import static java.util.function.Function.identity; import static java.util.stream.Collectors.toMap; @@ -129,68 +128,90 @@ private MatchStatus checkCollPerm(Map> pathVsPerms, private MatchStatus checkPathPerm(List permissions, AuthorizationContext context) { if (permissions == null || permissions.isEmpty()) return MatchStatus.NO_PERMISSIONS_FOUND; Principal principal = context.getUserPrincipal(); - loopPermissions: + + final Permission governingPermission = findFirstGoverningPermission(permissions, context); + if (governingPermission == null) { + log.debug("No permissions configured for the resource {} . So allowed to access", context.getResource()); + return MatchStatus.NO_PERMISSIONS_FOUND; + } + + return determineIfPermissionPermitsPrincipal(principal, governingPermission); + } + + private Permission findFirstGoverningPermission(List permissions, AuthorizationContext context) { for (int i = 0; i < permissions.size(); i++) { Permission permission = permissions.get(i); - if (PermissionNameProvider.values.containsKey(permission.name)) { - if (context.getHandler() instanceof PermissionNameProvider) { - PermissionNameProvider handler = (PermissionNameProvider) context.getHandler(); - PermissionNameProvider.Name permissionName = handler.getPermissionName(context); - if (permissionName == null || !permission.name.equals(permissionName.name)) { - continue; - } - } else { - //all is special. it can match any - if(permission.wellknownName != PermissionNameProvider.Name.ALL) continue; - } - } else { - if (permission.method != null && !permission.method.contains(context.getHttpMethod())) { - //this permissions HTTP method does not match this rule. try other rules - continue; - } - if (permission.params != null) { - for (Map.Entry> e : permission.params.entrySet()) { - String[] paramVal = context.getParams().getParams(e.getKey()); - if(!e.getValue().apply(paramVal)) continue loopPermissions; - } - } - } + if (permissionAppliesToRequest(permission, context)) return permission; + } - if (permission.role == null) { - //no role is assigned permission.That means everybody is allowed to access - return MatchStatus.PERMITTED; - } - if (principal == null) { - log.info("request has come without principal. failed permission {} ",permission); - //this resource needs a principal but the request has come without - //any credential. - return MatchStatus.USER_REQUIRED; - } else if (permission.role.contains("*")) { - return MatchStatus.PERMITTED; - } + return null; + } - for (String role : permission.role) { - try { - Set userRoles = getUserRoles(context); - if (userRoles != null && userRoles.contains(role)) return MatchStatus.PERMITTED; - } catch (SolrException se) { - log.warn("Problems finding user roles for principal " + principal.getName(), se); - return MatchStatus.FORBIDDEN; - } + private boolean permissionAppliesToRequest(Permission permission, AuthorizationContext context) { + if (PermissionNameProvider.values.containsKey(permission.name)) { + return predefinedPermissionAppliesToRequest(permission, context); + } else { + return customPermissionAppliesToRequest(permission, context); + } + } + + private boolean predefinedPermissionAppliesToRequest(Permission predefinedPermission, AuthorizationContext context) { + if (predefinedPermission.wellknownName == PermissionNameProvider.Name.ALL) { + return true; //'ALL' applies to everything! + } else if (! (context.getHandler() instanceof PermissionNameProvider)) { + return false; // We're not 'ALL', and the handler isn't associated with any other predefined permissions + } else { + PermissionNameProvider handler = (PermissionNameProvider) context.getHandler(); + PermissionNameProvider.Name permissionName = handler.getPermissionName(context); + + return permissionName != null && predefinedPermission.name.equals(permissionName.name); + } + } + + private boolean customPermissionAppliesToRequest(Permission customPermission, AuthorizationContext context) { + if (customPermission.method != null && !customPermission.method.contains(context.getHttpMethod())) { + //this permissions HTTP method does not match this rule. try other rules + return false; + } + if (customPermission.params != null) { + for (Map.Entry> e : customPermission.params.entrySet()) { + String[] paramVal = context.getParams().getParams(e.getKey()); + if(!e.getValue().apply(paramVal)) return false; } - log.info("This resource is configured to have a permission {}, The principal {} does not have the right role ", permission, principal); - return MatchStatus.FORBIDDEN; } - log.debug("No permissions configured for the resource {} . So allowed to access", context.getResource()); - return MatchStatus.NO_PERMISSIONS_FOUND; + + return true; + } + + private MatchStatus determineIfPermissionPermitsPrincipal(Principal principal, Permission governingPermission) { + if (governingPermission.role == null) { + //no role is assigned permission.That means everybody is allowed to access + return MatchStatus.PERMITTED; + } + if (principal == null) { + log.info("request has come without principal. failed permission {} ", governingPermission); + //this resource needs a principal but the request has come without + //any credential. + return MatchStatus.USER_REQUIRED; + } else if (governingPermission.role.contains("*")) { + return MatchStatus.PERMITTED; + } + + for (String role : governingPermission.role) { + Set userRoles = getUserRoles(principal); + if (userRoles != null && userRoles.contains(role)) return MatchStatus.PERMITTED; + } + log.info("This resource is configured to have a permission {}, The principal {} does not have the right role ", governingPermission, principal); + return MatchStatus.FORBIDDEN; } + /** * Finds users roles - * @param authorizationContext the authorization context, which contains the Principal mm + * @param principal the user Principal to fetch roles for * @return set of roles as strings */ - protected abstract Set getUserRoles(AuthorizationContext authorizationContext); + protected abstract Set getUserRoles(Principal principal); //this is to do optimized lookup of permissions for a given collection/path void add2Mapping(Permission permission) { @@ -222,7 +243,6 @@ enum MatchStatus { } } - @Override public Map edit(Map latestConf, List commands) { for (CommandOperation op : commands) { @@ -238,8 +258,9 @@ public Map edit(Map latestConf, List ops = unmodifiableMap(asList(AutorizationEditOperation.values()).stream().collect(toMap(AutorizationEditOperation::getOperationName, identity()))); - + private static final Map ops = unmodifiableMap( + Arrays.stream(AutorizationEditOperation.values()) + .collect(toMap(AutorizationEditOperation::getOperationName, identity()))); @Override public ValidatingJsonMap getSpec() { diff --git a/solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java b/solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java index 5aeb8a059209..94401ab96ac7 100644 --- a/solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java +++ b/solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java @@ -68,31 +68,9 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { @Override public void setUp() throws Exception { super.setUp(); - permissions = "{" + - " user-role : {" + - " steve: [dev,user]," + - " tim: [dev,admin]," + - " joe: [user]," + - " noble:[dev,user]" + - " }," + - " permissions : [" + - " {name:'schema-edit'," + - " role:admin}," + - " {name:'collection-admin-read'," + - " role:null}," + - " {name:collection-admin-edit ," + - " role:admin}," + - " {name:mycoll_update," + - " collection:mycoll," + - " path:'/update/*'," + - " role:[dev,admin]" + - " }," + - "{name:read , role:dev }," + - "{name:freeforall, path:'/foo', role:'*'}]}"; - rules = (Map) Utils.fromJSONString(permissions); + resetPermissionsAndRoles(); } - @SuppressWarnings("unchecked") public void testBasicPermissions() { checkRules(makeMap("resource", "/update/json/docs", "httpMethod", "POST", @@ -199,7 +177,6 @@ public void testBasicPermissions() { "userPrincipal", "joe") , FORBIDDEN); - setUserRole("cio", "su"); addPermission("all", "su"); @@ -225,6 +202,7 @@ public void testBasicPermissions() { "params", new MapSolrParams(singletonMap("action", "CREATE"))) , STATUS_OK); + resetPermissionsAndRoles(); addPermission("core-admin-edit", "su"); addPermission("core-admin-read", "user"); setUserRole("cio", "su"); @@ -262,7 +240,7 @@ public void testBasicPermissions() { "params", new MapSolrParams(singletonMap("action", "CREATE"))) ,STATUS_OK); - removePermission("all"); + resetPermissionsAndRoles(); addPermission("test-params", "admin", "/x", makeMap("key", Arrays.asList("REGEX:(?i)val1", "VAL2"))); checkRules(makeMap("resource", "/x", @@ -468,6 +446,31 @@ protected void addPermission(String permissionName, String role) { ((List)rules.get("permissions")).add( makeMap("name", permissionName, "role", role)); } + void resetPermissionsAndRoles() { + permissions = "{" + + " user-role : {" + + " steve: [dev,user]," + + " tim: [dev,admin]," + + " joe: [user]," + + " noble:[dev,user]" + + " }," + + " permissions : [" + + " {name:'schema-edit'," + + " role:admin}," + + " {name:'collection-admin-read'," + + " role:null}," + + " {name:collection-admin-edit ," + + " role:admin}," + + " {name:mycoll_update," + + " collection:mycoll," + + " path:'/update/*'," + + " role:[dev,admin]" + + " }," + + "{name:read, role:dev }," + + "{name:freeforall, path:'/foo', role:'*'}]}"; + rules = (Map) Utils.fromJSONString(permissions); + } + void clearUserRoles() { rules.put("user-role", new HashMap()); } @@ -529,7 +532,7 @@ void checkRules(Map values, int expected) { checkRules(values, expected, rules); } - void checkRules(Map values, int expected, Map permissions) { + void checkRules(Map values, int expected, Map permissions) { AuthorizationContext context = getMockContext(values); try (RuleBasedAuthorizationPluginBase plugin = createPlugin()) { plugin.init(permissions); From 1106ba54fb33431cc627afd026e26859d805ee72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Tue, 16 Apr 2019 11:34:47 +0200 Subject: [PATCH 07/24] WIP --- ...BaseTestRuleBasedAuthorizationPlugin.java} | 72 +++++++++---------- ...ernalRoleRuleBasedAuthorizationPlugin.java | 21 +----- 2 files changed, 34 insertions(+), 59 deletions(-) rename solr/core/src/test/org/apache/solr/security/{TestRuleBasedAuthorizationPlugin.java => BaseTestRuleBasedAuthorizationPlugin.java} (97%) diff --git a/solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java b/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java similarity index 97% rename from solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java rename to solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java index 94401ab96ac7..f5dd974ead02 100644 --- a/solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java +++ b/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java @@ -57,7 +57,7 @@ import static org.apache.solr.common.util.CommandOperation.captureErrors; @SuppressWarnings("unchecked") -public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { +public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { protected String permissions; protected Map rules; @@ -71,6 +71,32 @@ public void setUp() throws Exception { resetPermissionsAndRoles(); } + void resetPermissionsAndRoles() { + permissions = "{" + + " user-role : {" + + " steve: [dev,user]," + + " tim: [dev,admin]," + + " joe: [user]," + + " noble:[dev,user]" + + " }," + + " permissions : [" + + " {name:'schema-edit'," + + " role:admin}," + + " {name:'collection-admin-read'," + + " role:null}," + + " {name:collection-admin-edit ," + + " role:admin}," + + " {name:mycoll_update," + + " collection:mycoll," + + " path:'/update/*'," + + " role:[dev,admin]" + + " }," + + "{name:read, role:dev }," + + "{name:freeforall, path:'/foo', role:'*'}]}"; + rules = (Map) Utils.fromJSONString(permissions); + } + + @Test public void testBasicPermissions() { checkRules(makeMap("resource", "/update/json/docs", "httpMethod", "POST", @@ -401,19 +427,16 @@ public void testAllPermissionAllowsActionsWhenAssociatedRoleIsWildcard() { public void testAllPermissionDeniesActionsWhenUserIsNotCorrectRole() { SolrRequestHandler handler = new UpdateRequestHandler(); assertThat(handler, new IsInstanceOf(PermissionNameProvider.class)); + setUserRole("dev", "dev"); + setUserRole("admin", "admin"); + addPermission("all", "admin"); checkRules(makeMap("resource", "/update", "userPrincipal", "dev", "requestType", RequestType.UNKNOWN, "collectionRequests", "go", "handler", new UpdateRequestHandler(), "params", new MapSolrParams(singletonMap("key", "VAL2"))) - , FORBIDDEN, (Map) Utils.fromJSONString( "{" + - " user-role:{" + - " dev:[dev_role]," + - " admin:[admin_role]}," + - " permissions:[" + - " {name:all, role:'admin_role'}" + - "]}")); + , FORBIDDEN); handler = new PropertiesRequestHandler(); assertThat(handler, new IsNot<>(new IsInstanceOf(PermissionNameProvider.class))); @@ -423,13 +446,7 @@ public void testAllPermissionDeniesActionsWhenUserIsNotCorrectRole() { "collectionRequests", "go", "handler", handler, "params", new MapSolrParams(emptyMap())) - , FORBIDDEN, (Map) Utils.fromJSONString( "{" + - " user-role:{" + - " dev:[dev_role]," + - " admin:[admin_role]}," + - " permissions:[" + - " {name:all, role:'admin_role'}" + - "]}")); + , FORBIDDEN); } void addPermission(String permissionName, String role, String path, Map params) { @@ -446,31 +463,6 @@ protected void addPermission(String permissionName, String role) { ((List)rules.get("permissions")).add( makeMap("name", permissionName, "role", role)); } - void resetPermissionsAndRoles() { - permissions = "{" + - " user-role : {" + - " steve: [dev,user]," + - " tim: [dev,admin]," + - " joe: [user]," + - " noble:[dev,user]" + - " }," + - " permissions : [" + - " {name:'schema-edit'," + - " role:admin}," + - " {name:'collection-admin-read'," + - " role:null}," + - " {name:collection-admin-edit ," + - " role:admin}," + - " {name:mycoll_update," + - " collection:mycoll," + - " path:'/update/*'," + - " role:[dev,admin]" + - " }," + - "{name:read, role:dev }," + - "{name:freeforall, path:'/foo', role:'*'}]}"; - rules = (Map) Utils.fromJSONString(permissions); - } - void clearUserRoles() { rules.put("user-role", new HashMap()); } diff --git a/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java b/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java index ae89194c5a94..0249d5c550e3 100644 --- a/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java +++ b/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java @@ -25,31 +25,14 @@ import java.util.Map; import org.apache.http.auth.BasicUserPrincipal; -import org.apache.solr.common.util.Utils; -public class TestExternalRoleRuleBasedAuthorizationPlugin extends TestRuleBasedAuthorizationPlugin { +public class TestExternalRoleRuleBasedAuthorizationPlugin extends BaseTestRuleBasedAuthorizationPlugin { private HashMap principals; @Override public void setUp() throws Exception { super.setUp(); - permissions = "{" + - " permissions : [" + - " {name:'schema-edit'," + - " role:admin}," + - " {name:'collection-admin-read'," + - " role:null}," + - " {name:collection-admin-edit ," + - " role:admin}," + - " {name:mycoll_update," + - " collection:mycoll," + - " path:'/update/*'," + - " role:[dev,admin]" + - " }," + - "{name:read , role:dev }," + - "{name:freeforall, path:'/foo', role:'*'}]}"; - - rules = (Map) Utils.fromJSONString(permissions); + rules.remove("user-role"); principals = new HashMap<>(); setUserRoles("steve", "dev", "user"); From 8102b9781ad7f0d3f3c28a06a958c5ece6eff6db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Tue, 16 Apr 2019 12:35:54 +0200 Subject: [PATCH 08/24] Fix test bugs start of refactor of test class into base. Base should test generic stuff, new test class for rule-based with user-role mappings --- .../BaseTestRuleBasedAuthorizationPlugin.java | 47 ++++++------------- 1 file changed, 14 insertions(+), 33 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java b/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java index f5dd974ead02..dbbaabd8b59d 100644 --- a/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java +++ b/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java @@ -58,7 +58,6 @@ @SuppressWarnings("unchecked") public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { - protected String permissions; protected Map rules; final int STATUS_OK = 200; @@ -72,7 +71,7 @@ public void setUp() throws Exception { } void resetPermissionsAndRoles() { - permissions = "{" + + String permissions = "{" + " user-role : {" + " steve: [dev,user]," + " tim: [dev,admin]," + @@ -346,19 +345,16 @@ public void testBasicPermissions() { public void testAllPermissionAllowsActionsWhenUserHasCorrectRole() { SolrRequestHandler handler = new UpdateRequestHandler(); assertThat(handler, new IsInstanceOf(PermissionNameProvider.class)); + setUserRole("dev", "dev"); + setUserRole("admin", "admin"); + addPermission("all", "dev", "admin"); checkRules(makeMap("resource", "/update", "userPrincipal", "dev", "requestType", RequestType.UNKNOWN, "collectionRequests", "go", "handler", handler, "params", new MapSolrParams(singletonMap("key", "VAL2"))) - , STATUS_OK, (Map) Utils.fromJSONString( "{" + - " user-role:{" + - " dev:[dev_role]," + - " admin:[admin_role]}," + - " permissions:[" + - " {name:all, role:[dev_role, admin_role]}" + - "]}")); + , STATUS_OK); handler = new PropertiesRequestHandler(); assertThat(handler, new IsNot<>(new IsInstanceOf(PermissionNameProvider.class))); @@ -368,13 +364,7 @@ public void testAllPermissionAllowsActionsWhenUserHasCorrectRole() { "collectionRequests", "go", "handler", handler, "params", new MapSolrParams(emptyMap())) - , STATUS_OK, (Map) Utils.fromJSONString( "{" + - " user-role:{" + - " dev:[dev_role]," + - " admin:[admin_role]}," + - " permissions:[" + - " {name:all, role:[dev_role, admin_role]}" + - "]}")); + , STATUS_OK); } @@ -387,19 +377,16 @@ public void testAllPermissionAllowsActionsWhenUserHasCorrectRole() { public void testAllPermissionAllowsActionsWhenAssociatedRoleIsWildcard() { SolrRequestHandler handler = new UpdateRequestHandler(); assertThat(handler, new IsInstanceOf(PermissionNameProvider.class)); + setUserRole("dev", "dev"); + setUserRole("admin", "admin"); + addPermission("all", "*"); checkRules(makeMap("resource", "/update", "userPrincipal", "dev", "requestType", RequestType.UNKNOWN, "collectionRequests", "go", "handler", new UpdateRequestHandler(), "params", new MapSolrParams(singletonMap("key", "VAL2"))) - , STATUS_OK, (Map) Utils.fromJSONString( "{" + - " user-role:{" + - " dev:[dev_role]," + - " admin:[admin_role]}," + - " permissions:[" + - " {name:all, role:'*'}" + - "]}")); + , STATUS_OK); handler = new PropertiesRequestHandler(); assertThat(handler, new IsNot<>(new IsInstanceOf(PermissionNameProvider.class))); @@ -409,13 +396,7 @@ public void testAllPermissionAllowsActionsWhenAssociatedRoleIsWildcard() { "collectionRequests", "go", "handler", handler, "params", new MapSolrParams(emptyMap())) - , STATUS_OK, (Map) Utils.fromJSONString( "{" + - " user-role:{" + - " dev:[dev_role]," + - " admin:[admin_role]}," + - " permissions:[" + - " {name:all, role:'*'}" + - "]}")); + , STATUS_OK); } /* @@ -459,8 +440,8 @@ void removePermission(String name) { rules.put("permissions", newPerm); } - protected void addPermission(String permissionName, String role) { - ((List)rules.get("permissions")).add( makeMap("name", permissionName, "role", role)); + protected void addPermission(String permissionName, String... roles) { + ((List)rules.get("permissions")).add( makeMap("name", permissionName, "role", Arrays.asList(roles))); } void clearUserRoles() { @@ -468,7 +449,7 @@ void clearUserRoles() { } protected void setUserRole(String user, String role) { - ((Map)rules.get("user-role")).put("cio","su"); + ((Map)rules.get("user-role")).put(user, role); } public void testEditRules() throws IOException { From 9bb915ab86148e288ae2b3bcdd48aa16f21fc197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Tue, 16 Apr 2019 12:48:28 +0200 Subject: [PATCH 09/24] Fix subclass tests --- .../ExternalRoleRuleBasedAuthorizationPlugin.java | 3 ++- .../solr/security/RuleBasedAuthorizationPluginBase.java | 8 ++++---- .../security/BaseTestRuleBasedAuthorizationPlugin.java | 2 +- .../TestExternalRoleRuleBasedAuthorizationPlugin.java | 7 ++++++- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java b/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java index 59924b52bee8..64fccb823920 100644 --- a/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java @@ -18,6 +18,7 @@ import java.lang.invoke.MethodHandles; import java.security.Principal; +import java.util.Collections; import java.util.Map; import java.util.Set; @@ -50,7 +51,7 @@ protected Set getUserRoles(Principal principal) { if(principal instanceof VerifiedUserRoles) { return ((VerifiedUserRoles) principal).getVerifiedRoles(); } else { - throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Request does not contain a Principal with roles"); + return Collections.emptySet(); } } } diff --git a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java index 804f7854cea0..7a5dae4d577e 100644 --- a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java +++ b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java @@ -197,9 +197,9 @@ private MatchStatus determineIfPermissionPermitsPrincipal(Principal principal, P return MatchStatus.PERMITTED; } - for (String role : governingPermission.role) { - Set userRoles = getUserRoles(principal); - if (userRoles != null && userRoles.contains(role)) return MatchStatus.PERMITTED; + Set userRoles = getUserRoles(principal); + if (userRoles != null && governingPermission.role.stream().anyMatch(userRoles::contains)) { + return MatchStatus.PERMITTED; } log.info("This resource is configured to have a permission {}, The principal {} does not have the right role ", governingPermission, principal); return MatchStatus.FORBIDDEN; @@ -209,7 +209,7 @@ private MatchStatus determineIfPermissionPermitsPrincipal(Principal principal, P /** * Finds users roles * @param principal the user Principal to fetch roles for - * @return set of roles as strings + * @return set of roles as strings or empty set if no roles found */ protected abstract Set getUserRoles(Principal principal); diff --git a/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java b/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java index dbbaabd8b59d..475be57987fd 100644 --- a/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java +++ b/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java @@ -70,7 +70,7 @@ public void setUp() throws Exception { resetPermissionsAndRoles(); } - void resetPermissionsAndRoles() { + protected void resetPermissionsAndRoles() { String permissions = "{" + " user-role : {" + " steve: [dev,user]," + diff --git a/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java b/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java index 0249d5c550e3..e8412171de3f 100644 --- a/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java +++ b/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java @@ -32,7 +32,6 @@ public class TestExternalRoleRuleBasedAuthorizationPlugin extends BaseTestRuleBa @Override public void setUp() throws Exception { super.setUp(); - rules.remove("user-role"); principals = new HashMap<>(); setUserRoles("steve", "dev", "user"); @@ -67,4 +66,10 @@ public Principal getUserPrincipal() { protected RuleBasedAuthorizationPluginBase createPlugin() { return new ExternalRoleRuleBasedAuthorizationPlugin(); } + + @Override + protected void resetPermissionsAndRoles() { + super.resetPermissionsAndRoles(); + rules.remove("user-role"); + } } From 22988bf6112942c4429f6d1bb21f026d87c5e05b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 10 Jan 2020 13:20:11 +0100 Subject: [PATCH 10/24] WIP --- solr/CHANGES.txt | 4 ++-- .../apache/solr/security/RuleBasedAuthorizationPlugin.java | 2 +- .../org/apache/solr/security/PrincipalWithUserRoles.java | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename solr/core/src/{java => test}/org/apache/solr/security/PrincipalWithUserRoles.java (100%) diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index c5152b21d4bf..4854adb0e53a 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -158,6 +158,8 @@ New Features Query DSL queries. It's optional and doesn't impact response without explicit referencing these queries by names (Anatolii Siuniaev via Mikhail Khludnev) +* SOLR-12131: ExternalRoleRuleBasedAuthorizationPlugin which gets user's roles from request (janhoy) + Improvements --------------------- * SOLR-14120: Define JavaScript methods 'includes' and 'startsWith' to ensure AdminUI can be displayed when using @@ -1082,8 +1084,6 @@ New Features * SOLR-12120: New AuditLoggerPlugin type allowing custom Audit logger plugins (janhoy) -* SOLR-12131: ExternalRoleRuleBasedAuthorizationPlugin which gets user's roles from request (janhoy) - * SOLR-10436: Add hashRollup Streaming Expression (Joel Bernstein) * SOLR-13276: Adding Http2 equivalent classes of CloudSolrClient and HttpClusterStateProvider (Cao Manh Dat) diff --git a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java index 8fc6d2462e99..53168c1ef9c4 100644 --- a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java @@ -42,7 +42,7 @@ import static org.apache.solr.handler.admin.SecurityConfHandler.getMapValue; -public class RuleBasedAuthorizationPlugin implements AuthorizationPlugin, ConfigEditablePlugin, SpecProvider { +public class RuleBasedAuthorizationPlugin extends RuleBasedAuthorizationPluginBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final Map> usersVsRoles = new HashMap<>(); diff --git a/solr/core/src/java/org/apache/solr/security/PrincipalWithUserRoles.java b/solr/core/src/test/org/apache/solr/security/PrincipalWithUserRoles.java similarity index 100% rename from solr/core/src/java/org/apache/solr/security/PrincipalWithUserRoles.java rename to solr/core/src/test/org/apache/solr/security/PrincipalWithUserRoles.java From 30b000880176d24dba733877b29c90bb11645a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 1 May 2020 22:04:38 +0200 Subject: [PATCH 11/24] Back to basic impl --- .../RuleBasedAuthorizationPlugin.java | 310 +----------------- 1 file changed, 13 insertions(+), 297 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java index 244d5a8fe014..53cc3b9d518d 100644 --- a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java @@ -16,329 +16,45 @@ */ package org.apache.solr.security; -import java.io.IOException; import java.lang.invoke.MethodHandles; import java.security.Principal; -import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Function; -import org.apache.solr.common.SpecProvider; -import org.apache.solr.common.util.Utils; -import org.apache.solr.common.util.ValidatingJsonMap; -import org.apache.solr.common.util.CommandOperation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static java.util.Arrays.asList; -import static java.util.Collections.unmodifiableMap; -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.toMap; -import static org.apache.solr.handler.admin.SecurityConfHandler.getListValue; import static org.apache.solr.handler.admin.SecurityConfHandler.getMapValue; - +/** + * Original implementation of Rule Based Authz plugin which configures user/role + * mapping in the security.json configuration + */ public class RuleBasedAuthorizationPlugin extends RuleBasedAuthorizationPluginBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final Map> usersVsRoles = new HashMap<>(); - private final Map mapping = new HashMap<>(); - private final List permissions = new ArrayList<>(); - - - private static class WildCardSupportMap extends HashMap> { - final Set wildcardPrefixes = new HashSet<>(); - - @Override - public List put(String key, List value) { - if (key != null && key.endsWith("/*")) { - key = key.substring(0, key.length() - 2); - wildcardPrefixes.add(key); - } - return super.put(key, value); - } - - @Override - public List get(Object key) { - List result = super.get(key); - if (key == null || result != null) return result; - if (!wildcardPrefixes.isEmpty()) { - for (String s : wildcardPrefixes) { - if (key.toString().startsWith(s)) { - List l = super.get(s); - if (l != null) { - result = result == null ? new ArrayList<>() : new ArrayList<>(result); - result.addAll(l); - } - } - } - } - return result; - } - } - - @Override - public AuthorizationResponse authorize(AuthorizationContext context) { - List collectionRequests = context.getCollectionRequests(); - if (log.isDebugEnabled()) { - log.debug("Attempting to authorize request to [{}] of type: [{}], associated with collections [{}]", - context.getResource(), context.getRequestType(), collectionRequests); - } - - if (context.getRequestType() == AuthorizationContext.RequestType.ADMIN) { - log.debug("Authorizing an ADMIN request, checking admin permissions"); - MatchStatus flag = checkCollPerm(mapping.get(null), context); - return flag.rsp; - } - - for (AuthorizationContext.CollectionRequest collreq : collectionRequests) { - //check permissions for each collection - log.debug("Authorizing collection-aware request, checking perms applicable to specific collection [{}]", - collreq.collectionName); - MatchStatus flag = checkCollPerm(mapping.get(collreq.collectionName), context); - if (flag != MatchStatus.NO_PERMISSIONS_FOUND) return flag.rsp; - } - - log.debug("Authorizing collection-aware request, checking perms applicable to all (*) collections"); - //check wildcard (all=*) permissions. - MatchStatus flag = checkCollPerm(mapping.get("*"), context); - return flag.rsp; - } - - private MatchStatus checkCollPerm(Map> pathVsPerms, - AuthorizationContext context) { - if (pathVsPerms == null) return MatchStatus.NO_PERMISSIONS_FOUND; - - if (log.isTraceEnabled()) { - log.trace("Following perms are associated with collection"); - for (String pathKey : pathVsPerms.keySet()) { - final List permsAssociatedWithPath = pathVsPerms.get(pathKey); - log.trace("Path: [{}], Perms: [{}]", pathKey, permsAssociatedWithPath); - } - } - - String path = context.getResource(); - MatchStatus flag = checkPathPerm(pathVsPerms.get(path), context); - if (flag != MatchStatus.NO_PERMISSIONS_FOUND) return flag; - return checkPathPerm(pathVsPerms.get(null), context); - } - - private MatchStatus checkPathPerm(List permissions, AuthorizationContext context) { - if (permissions == null || permissions.isEmpty()) { - return MatchStatus.NO_PERMISSIONS_FOUND; - } - Principal principal = context.getUserPrincipal(); - - log.trace("Following perms are associated with this collection and path: [{}]", permissions); - final Permission governingPermission = findFirstGoverningPermission(permissions, context); - if (governingPermission == null) { - if (log.isDebugEnabled()) { - log.debug("No perms configured for the resource {} . So allowed to access", context.getResource()); - } - return MatchStatus.NO_PERMISSIONS_FOUND; - } - if (log.isDebugEnabled()) { - log.debug("Found perm [{}] to govern resource [{}]", governingPermission, context.getResource()); - } - - return determineIfPermissionPermitsPrincipal(principal, governingPermission); - } - - private Permission findFirstGoverningPermission(List permissions, AuthorizationContext context) { - for (int i = 0; i < permissions.size(); i++) { - Permission permission = permissions.get(i); - if (permissionAppliesToRequest(permission, context)) return permission; - } - - return null; - } - - private boolean permissionAppliesToRequest(Permission permission, AuthorizationContext context) { - if (log.isTraceEnabled()) { - log.trace("Testing whether permission [{}] applies to request [{}]", permission, context.getResource()); - } - if (PermissionNameProvider.values.containsKey(permission.name)) { - return predefinedPermissionAppliesToRequest(permission, context); - } else { - return customPermissionAppliesToRequest(permission, context); - } - } - - private boolean predefinedPermissionAppliesToRequest(Permission predefinedPermission, AuthorizationContext context) { - log.trace("Permission [{}] is a predefined perm", predefinedPermission); - if (predefinedPermission.wellknownName == PermissionNameProvider.Name.ALL) { - log.trace("'ALL' perm applies to all requests; perm applies."); - return true; //'ALL' applies to everything! - } else if (! (context.getHandler() instanceof PermissionNameProvider)) { - if (log.isTraceEnabled()) { - log.trace("Request handler [{}] is not a PermissionNameProvider, perm doesnt apply", context.getHandler()); - } - return false; // We're not 'ALL', and the handler isn't associated with any other predefined permissions - } else { - PermissionNameProvider handler = (PermissionNameProvider) context.getHandler(); - PermissionNameProvider.Name permissionName = handler.getPermissionName(context); - - boolean applies = permissionName != null && predefinedPermission.name.equals(permissionName.name); - log.trace("Request handler [{}] is associated with predefined perm [{}]? {}", - handler, predefinedPermission.name, applies); - return applies; - } - } - - private boolean customPermissionAppliesToRequest(Permission customPermission, AuthorizationContext context) { - log.trace("Permission [{}] is a custom permission", customPermission); - if (customPermission.method != null && !customPermission.method.contains(context.getHttpMethod())) { - if (log.isTraceEnabled()) { - log.trace("Custom permission requires method [{}] but request had method [{}]; permission doesn't apply", - customPermission.method, context.getHttpMethod()); - } - //this permissions HTTP method does not match this rule. try other rules - return false; - } - if (customPermission.params != null) { - for (Map.Entry> e : customPermission.params.entrySet()) { - String[] paramVal = context.getParams().getParams(e.getKey()); - if(!e.getValue().apply(paramVal)) { - if (log.isTraceEnabled()) { - log.trace("Request has param [{}] which is incompatible with custom perm [{}]; perm doesnt apply", - e.getKey(), customPermission); - } - return false; - } - } - } - - log.trace("Perm [{}] matches method and params for request; permission applies", customPermission); - return true; - } - - private MatchStatus determineIfPermissionPermitsPrincipal(Principal principal, Permission governingPermission) { - if (governingPermission.role == null) { - log.debug("Governing permission [{}] has no role; permitting access", governingPermission); - return MatchStatus.PERMITTED; - } - if (principal == null) { - log.debug("Governing permission [{}] has role, but request principal cannot be identified; forbidding access", governingPermission); - return MatchStatus.USER_REQUIRED; - } else if (governingPermission.role.contains("*")) { - log.debug("Governing permission [{}] allows all roles; permitting access", governingPermission); - return MatchStatus.PERMITTED; - } - - Set userRoles = usersVsRoles.get(principal.getName()); - for (String role : governingPermission.role) { - if (userRoles != null && userRoles.contains(role)) { - log.debug("Governing permission [{}] allows access to role [{}]; permitting access", governingPermission, role); - return MatchStatus.PERMITTED; - } - } - log.info("This resource is configured to have a permission {}, The principal {} does not have the right role ", governingPermission, principal); - return MatchStatus.FORBIDDEN; - } - - public Set getRoles(String user) { - Set roles = usersVsRoles.get(user); - return roles; - } - - public boolean doesUserHavePermission(String user, PermissionNameProvider.Name permission) { - Set roles = usersVsRoles.get(user); - if (roles != null) { - for (String role: roles) { - if (mapping.get(null) == null) continue; - List permissions = mapping.get(null).get(null); - if (permissions != null) { - for (Permission p: permissions) { - if (permission.equals(p.wellknownName) && p.role.contains(role)) { - return true; - } - } - } - } - } - return false; - } @Override public void init(Map initInfo) { - mapping.put(null, new WildCardSupportMap()); + super.init(initInfo); Map map = getMapValue(initInfo, "user-role"); for (Object o : map.entrySet()) { Map.Entry e = (Map.Entry) o; String roleName = (String) e.getKey(); usersVsRoles.put(roleName, Permission.readValueAsSet(map, roleName)); } - List perms = getListValue(initInfo, "permissions"); - for (Map o : perms) { - Permission p; - try { - p = Permission.load(o); - } catch (Exception exp) { - log.error("Invalid permission ", exp); - continue; - } - permissions.add(p); - add2Mapping(p); - } - } - - //this is to do optimized lookup of permissions for a given collection/path - private void add2Mapping(Permission permission) { - for (String c : permission.collections) { - WildCardSupportMap m = mapping.get(c); - if (m == null) mapping.put(c, m = new WildCardSupportMap()); - for (String path : permission.path) { - List perms = m.get(path); - if (perms == null) m.put(path, perms = new ArrayList<>()); - perms.add(permission); - } - } - } - - - @Override - public void close() throws IOException { } - - enum MatchStatus { - USER_REQUIRED(AuthorizationResponse.PROMPT), - NO_PERMISSIONS_FOUND(AuthorizationResponse.OK), - PERMITTED(AuthorizationResponse.OK), - FORBIDDEN(AuthorizationResponse.FORBIDDEN); - - final AuthorizationResponse rsp; - - MatchStatus(AuthorizationResponse rsp) { - this.rsp = rsp; - } } - - - @Override - public Map edit(Map latestConf, List commands) { - for (CommandOperation op : commands) { - AutorizationEditOperation operation = ops.get(op.name); - if (operation == null) { - op.unknownOperation(); - return null; - } - latestConf = operation.edit(latestConf, op); - if (latestConf == null) return null; - - } - return latestConf; - } - - private static final Map ops = unmodifiableMap(asList(AutorizationEditOperation.values()).stream().collect(toMap(AutorizationEditOperation::getOperationName, identity()))); - - + /** + * Implementers should calculate the users roles + * + * @param principal the user Principal from the request + * @return set of roles as strings + */ @Override - public ValidatingJsonMap getSpec() { - return Utils.getSpec("cluster.security.RuleBasedAuthorization").getSpec(); - + protected Set getUserRoles(Principal principal) { + return usersVsRoles.get(principal.getName()); } } From a50375bffca730a0cba2b4b50c8832e038d6e802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 1 May 2020 22:17:32 +0200 Subject: [PATCH 12/24] Bring RBAC up to date with master. Fix SystemInforHandler to use the getUserRoles(Principal) method --- .../solr/handler/admin/SystemInfoHandler.java | 6 +- .../RuleBasedAuthorizationPlugin.java | 2 +- .../RuleBasedAuthorizationPluginBase.java | 154 +++++++++++++----- 3 files changed, 116 insertions(+), 46 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java index 8016a25a24ff..3aa31ce532da 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java @@ -41,7 +41,7 @@ import org.apache.solr.response.SolrQueryResponse; import org.apache.solr.schema.IndexSchema; import org.apache.solr.security.AuthorizationPlugin; -import org.apache.solr.security.RuleBasedAuthorizationPlugin; +import org.apache.solr.security.RuleBasedAuthorizationPluginBase; import org.apache.solr.util.RTimer; import org.apache.solr.util.RedactionUtils; import org.apache.solr.util.stats.MetricUtils; @@ -341,8 +341,8 @@ public SimpleOrderedMap getSecurityInfo(SolrQueryRequest req) // Mapped roles for this principal AuthorizationPlugin auth = cc==null? null: cc.getAuthorizationPlugin(); if (auth != null) { - RuleBasedAuthorizationPlugin rbap = (RuleBasedAuthorizationPlugin) auth; - Set roles = rbap.getRoles(username); + RuleBasedAuthorizationPluginBase rbap = (RuleBasedAuthorizationPluginBase) auth; + Set roles = rbap.getUserRoles(req.getUserPrincipal()); info.add("roles", roles); } } diff --git a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java index 53cc3b9d518d..6aca3f6fef30 100644 --- a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java @@ -54,7 +54,7 @@ public void init(Map initInfo) { * @return set of roles as strings */ @Override - protected Set getUserRoles(Principal principal) { + public Set getUserRoles(Principal principal) { return usersVsRoles.get(principal.getName()); } } diff --git a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java index 7a5dae4d577e..bc8b02ea4c48 100644 --- a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java +++ b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java @@ -20,7 +20,6 @@ import java.lang.invoke.MethodHandles; import java.security.Principal; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -29,12 +28,13 @@ import java.util.function.Function; import org.apache.solr.common.SpecProvider; -import org.apache.solr.common.util.CommandOperation; import org.apache.solr.common.util.Utils; import org.apache.solr.common.util.ValidatingJsonMap; +import org.apache.solr.common.util.CommandOperation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static java.util.Arrays.asList; import static java.util.Collections.unmodifiableMap; import static java.util.function.Function.identity; import static java.util.stream.Collectors.toMap; @@ -46,10 +46,12 @@ public abstract class RuleBasedAuthorizationPluginBase implements AuthorizationPlugin, ConfigEditablePlugin, SpecProvider { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - final Map mapping = new HashMap<>(); - final List permissions = new ArrayList<>(); + private final Map> usersVsRoles = new HashMap<>(); + private final Map mapping = new HashMap<>(); + private final List permissions = new ArrayList<>(); + - static class WildCardSupportMap extends HashMap> { + private static class WildCardSupportMap extends HashMap> { final Set wildcardPrefixes = new HashSet<>(); @Override @@ -80,36 +82,29 @@ public List get(Object key) { } } - @Override - public void init(Map initInfo) { - mapping.put(null, new WildCardSupportMap()); - List perms = getListValue(initInfo, "permissions"); - for (Map o : perms) { - Permission p; - try { - p = Permission.load(o); - } catch (Exception exp) { - log.error("Invalid permission ", exp); - continue; - } - permissions.add(p); - add2Mapping(p); - } - } - @Override public AuthorizationResponse authorize(AuthorizationContext context) { List collectionRequests = context.getCollectionRequests(); + if (log.isDebugEnabled()) { + log.debug("Attempting to authorize request to [{}] of type: [{}], associated with collections [{}]", + context.getResource(), context.getRequestType(), collectionRequests); + } + if (context.getRequestType() == AuthorizationContext.RequestType.ADMIN) { + log.debug("Authorizing an ADMIN request, checking admin permissions"); MatchStatus flag = checkCollPerm(mapping.get(null), context); return flag.rsp; } for (AuthorizationContext.CollectionRequest collreq : collectionRequests) { //check permissions for each collection + log.debug("Authorizing collection-aware request, checking perms applicable to specific collection [{}]", + collreq.collectionName); MatchStatus flag = checkCollPerm(mapping.get(collreq.collectionName), context); if (flag != MatchStatus.NO_PERMISSIONS_FOUND) return flag.rsp; } + + log.debug("Authorizing collection-aware request, checking perms applicable to all (*) collections"); //check wildcard (all=*) permissions. MatchStatus flag = checkCollPerm(mapping.get("*"), context); return flag.rsp; @@ -119,6 +114,14 @@ private MatchStatus checkCollPerm(Map> pathVsPerms, AuthorizationContext context) { if (pathVsPerms == null) return MatchStatus.NO_PERMISSIONS_FOUND; + if (log.isTraceEnabled()) { + log.trace("Following perms are associated with collection"); + for (String pathKey : pathVsPerms.keySet()) { + final List permsAssociatedWithPath = pathVsPerms.get(pathKey); + log.trace("Path: [{}], Perms: [{}]", pathKey, permsAssociatedWithPath); + } + } + String path = context.getResource(); MatchStatus flag = checkPathPerm(pathVsPerms.get(path), context); if (flag != MatchStatus.NO_PERMISSIONS_FOUND) return flag; @@ -126,14 +129,22 @@ private MatchStatus checkCollPerm(Map> pathVsPerms, } private MatchStatus checkPathPerm(List permissions, AuthorizationContext context) { - if (permissions == null || permissions.isEmpty()) return MatchStatus.NO_PERMISSIONS_FOUND; + if (permissions == null || permissions.isEmpty()) { + return MatchStatus.NO_PERMISSIONS_FOUND; + } Principal principal = context.getUserPrincipal(); + log.trace("Following perms are associated with this collection and path: [{}]", permissions); final Permission governingPermission = findFirstGoverningPermission(permissions, context); if (governingPermission == null) { - log.debug("No permissions configured for the resource {} . So allowed to access", context.getResource()); + if (log.isDebugEnabled()) { + log.debug("No perms configured for the resource {} . So allowed to access", context.getResource()); + } return MatchStatus.NO_PERMISSIONS_FOUND; } + if (log.isDebugEnabled()) { + log.debug("Found perm [{}] to govern resource [{}]", governingPermission, context.getResource()); + } return determineIfPermissionPermitsPrincipal(principal, governingPermission); } @@ -148,6 +159,9 @@ private Permission findFirstGoverningPermission(List permissions, Au } private boolean permissionAppliesToRequest(Permission permission, AuthorizationContext context) { + if (log.isTraceEnabled()) { + log.trace("Testing whether permission [{}] applies to request [{}]", permission, context.getResource()); + } if (PermissionNameProvider.values.containsKey(permission.name)) { return predefinedPermissionAppliesToRequest(permission, context); } else { @@ -156,65 +170,114 @@ private boolean permissionAppliesToRequest(Permission permission, AuthorizationC } private boolean predefinedPermissionAppliesToRequest(Permission predefinedPermission, AuthorizationContext context) { + log.trace("Permission [{}] is a predefined perm", predefinedPermission); if (predefinedPermission.wellknownName == PermissionNameProvider.Name.ALL) { + log.trace("'ALL' perm applies to all requests; perm applies."); return true; //'ALL' applies to everything! } else if (! (context.getHandler() instanceof PermissionNameProvider)) { + if (log.isTraceEnabled()) { + log.trace("Request handler [{}] is not a PermissionNameProvider, perm doesnt apply", context.getHandler()); + } return false; // We're not 'ALL', and the handler isn't associated with any other predefined permissions } else { PermissionNameProvider handler = (PermissionNameProvider) context.getHandler(); PermissionNameProvider.Name permissionName = handler.getPermissionName(context); - return permissionName != null && predefinedPermission.name.equals(permissionName.name); + boolean applies = permissionName != null && predefinedPermission.name.equals(permissionName.name); + log.trace("Request handler [{}] is associated with predefined perm [{}]? {}", + handler, predefinedPermission.name, applies); + return applies; } } private boolean customPermissionAppliesToRequest(Permission customPermission, AuthorizationContext context) { + log.trace("Permission [{}] is a custom permission", customPermission); if (customPermission.method != null && !customPermission.method.contains(context.getHttpMethod())) { + if (log.isTraceEnabled()) { + log.trace("Custom permission requires method [{}] but request had method [{}]; permission doesn't apply", + customPermission.method, context.getHttpMethod()); + } //this permissions HTTP method does not match this rule. try other rules return false; } if (customPermission.params != null) { for (Map.Entry> e : customPermission.params.entrySet()) { String[] paramVal = context.getParams().getParams(e.getKey()); - if(!e.getValue().apply(paramVal)) return false; + if(!e.getValue().apply(paramVal)) { + if (log.isTraceEnabled()) { + log.trace("Request has param [{}] which is incompatible with custom perm [{}]; perm doesnt apply", + e.getKey(), customPermission); + } + return false; + } } } + log.trace("Perm [{}] matches method and params for request; permission applies", customPermission); return true; } private MatchStatus determineIfPermissionPermitsPrincipal(Principal principal, Permission governingPermission) { if (governingPermission.role == null) { - //no role is assigned permission.That means everybody is allowed to access + log.debug("Governing permission [{}] has no role; permitting access", governingPermission); return MatchStatus.PERMITTED; } if (principal == null) { - log.info("request has come without principal. failed permission {} ", governingPermission); - //this resource needs a principal but the request has come without - //any credential. + log.debug("Governing permission [{}] has role, but request principal cannot be identified; forbidding access", governingPermission); return MatchStatus.USER_REQUIRED; } else if (governingPermission.role.contains("*")) { + log.debug("Governing permission [{}] allows all roles; permitting access", governingPermission); return MatchStatus.PERMITTED; } Set userRoles = getUserRoles(principal); - if (userRoles != null && governingPermission.role.stream().anyMatch(userRoles::contains)) { - return MatchStatus.PERMITTED; + for (String role : governingPermission.role) { + if (userRoles != null && userRoles.contains(role)) { + log.debug("Governing permission [{}] allows access to role [{}]; permitting access", governingPermission, role); + return MatchStatus.PERMITTED; + } } log.info("This resource is configured to have a permission {}, The principal {} does not have the right role ", governingPermission, principal); return MatchStatus.FORBIDDEN; } + public boolean doesUserHavePermission(String user, PermissionNameProvider.Name permission) { + Set roles = usersVsRoles.get(user); + if (roles != null) { + for (String role: roles) { + if (mapping.get(null) == null) continue; + List permissions = mapping.get(null).get(null); + if (permissions != null) { + for (Permission p: permissions) { + if (permission.equals(p.wellknownName) && p.role.contains(role)) { + return true; + } + } + } + } + } + return false; + } - /** - * Finds users roles - * @param principal the user Principal to fetch roles for - * @return set of roles as strings or empty set if no roles found - */ - protected abstract Set getUserRoles(Principal principal); + @Override + public void init(Map initInfo) { + mapping.put(null, new WildCardSupportMap()); + List perms = getListValue(initInfo, "permissions"); + for (Map o : perms) { + Permission p; + try { + p = Permission.load(o); + } catch (Exception exp) { + log.error("Invalid permission ", exp); + continue; + } + permissions.add(p); + add2Mapping(p); + } + } //this is to do optimized lookup of permissions for a given collection/path - void add2Mapping(Permission permission) { + private void add2Mapping(Permission permission) { for (String c : permission.collections) { WildCardSupportMap m = mapping.get(c); if (m == null) mapping.put(c, m = new WildCardSupportMap()); @@ -226,6 +289,12 @@ void add2Mapping(Permission permission) { } } + /** + * Finds users roles + * @param principal the user Principal to fetch roles for + * @return set of roles as strings or empty set if no roles found + */ + public abstract Set getUserRoles(Principal principal); @Override public void close() throws IOException { } @@ -243,6 +312,8 @@ enum MatchStatus { } } + + @Override public Map edit(Map latestConf, List commands) { for (CommandOperation op : commands) { @@ -258,9 +329,8 @@ public Map edit(Map latestConf, List ops = unmodifiableMap( - Arrays.stream(AutorizationEditOperation.values()) - .collect(toMap(AutorizationEditOperation::getOperationName, identity()))); + private static final Map ops = unmodifiableMap(asList(AutorizationEditOperation.values()).stream().collect(toMap(AutorizationEditOperation::getOperationName, identity()))); + @Override public ValidatingJsonMap getSpec() { From ed926b96a7bbabf4f13d87888b1395354d520aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 1 May 2020 22:22:12 +0200 Subject: [PATCH 13/24] Not serializable --- .../test/org/apache/solr/security/PrincipalWithUserRoles.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/security/PrincipalWithUserRoles.java b/solr/core/src/test/org/apache/solr/security/PrincipalWithUserRoles.java index 01479e9f2622..2b4601deddcf 100644 --- a/solr/core/src/test/org/apache/solr/security/PrincipalWithUserRoles.java +++ b/solr/core/src/test/org/apache/solr/security/PrincipalWithUserRoles.java @@ -16,7 +16,6 @@ */ package org.apache.solr.security; -import java.io.Serializable; import java.security.Principal; import java.util.Set; @@ -29,8 +28,7 @@ * another secure manner. The role information can then be used to authorize * requests without the need to maintain or lookup what roles each user belongs to. */ -public class PrincipalWithUserRoles implements Principal, VerifiedUserRoles, Serializable { - private static final long serialVersionUID = 4144666467522831388L; +public class PrincipalWithUserRoles implements Principal, VerifiedUserRoles { private final String username; private final Set roles; From db4abcf8b6bf5c8902e8a6b645ae251f5e1bedf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 1 May 2020 22:24:51 +0200 Subject: [PATCH 14/24] Objects.requireNonNull --- .../org/apache/solr/security/PrincipalWithUserRoles.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/security/PrincipalWithUserRoles.java b/solr/core/src/test/org/apache/solr/security/PrincipalWithUserRoles.java index 2b4601deddcf..e8c9fa6bebad 100644 --- a/solr/core/src/test/org/apache/solr/security/PrincipalWithUserRoles.java +++ b/solr/core/src/test/org/apache/solr/security/PrincipalWithUserRoles.java @@ -17,6 +17,7 @@ package org.apache.solr.security; import java.security.Principal; +import java.util.Objects; import java.util.Set; import org.apache.http.util.Args; @@ -40,8 +41,8 @@ public class PrincipalWithUserRoles implements Principal, VerifiedUserRoles { */ public PrincipalWithUserRoles(final String username, Set roles) { super(); - Args.notNull(username, "User name"); - Args.notNull(roles, "User roles"); + Objects.requireNonNull(username, "User name was null"); + Objects.requireNonNull(roles, "User roles was null"); this.username = username; this.roles = roles; } From e148db70db91b6d3ef4bb4f709406885bf85c6c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 1 May 2020 22:28:37 +0200 Subject: [PATCH 15/24] Remove role-user map from base class --- .../src/java/org/apache/solr/security/KerberosFilter.java | 2 +- .../solr/security/RuleBasedAuthorizationPluginBase.java | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/security/KerberosFilter.java b/solr/core/src/java/org/apache/solr/security/KerberosFilter.java index 0937d699e3f0..6dd6c7fd6688 100644 --- a/solr/core/src/java/org/apache/solr/security/KerberosFilter.java +++ b/solr/core/src/java/org/apache/solr/security/KerberosFilter.java @@ -91,7 +91,7 @@ private HttpServletRequest substituteOriginalUserRequest(HttpServletRequest requ if (authzPlugin instanceof RuleBasedAuthorizationPlugin) { RuleBasedAuthorizationPlugin ruleBased = (RuleBasedAuthorizationPlugin) authzPlugin; if (request.getHeader(KerberosPlugin.ORIGINAL_USER_PRINCIPAL_HEADER) != null && - ruleBased.doesUserHavePermission(request.getUserPrincipal().getName(), PermissionNameProvider.Name.ALL)) { + ruleBased.doesUserHavePermission(request.getUserPrincipal(), PermissionNameProvider.Name.ALL)) { request = new HttpServletRequestWrapper(request) { @Override public Principal getUserPrincipal() { diff --git a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java index bc8b02ea4c48..885fc70ea345 100644 --- a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java +++ b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java @@ -46,7 +46,6 @@ public abstract class RuleBasedAuthorizationPluginBase implements AuthorizationPlugin, ConfigEditablePlugin, SpecProvider { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private final Map> usersVsRoles = new HashMap<>(); private final Map mapping = new HashMap<>(); private final List permissions = new ArrayList<>(); @@ -241,8 +240,8 @@ private MatchStatus determineIfPermissionPermitsPrincipal(Principal principal, P return MatchStatus.FORBIDDEN; } - public boolean doesUserHavePermission(String user, PermissionNameProvider.Name permission) { - Set roles = usersVsRoles.get(user); + public boolean doesUserHavePermission(Principal principal, PermissionNameProvider.Name permission) { + Set roles = getUserRoles(principal); if (roles != null) { for (String role: roles) { if (mapping.get(null) == null) continue; From 842d680c4b49413fd8659b4d47563f33c9017d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 1 May 2020 22:54:31 +0200 Subject: [PATCH 16/24] protected -> public --- .../solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java b/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java index 64fccb823920..7575c167d9cc 100644 --- a/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/ExternalRoleRuleBasedAuthorizationPlugin.java @@ -47,7 +47,7 @@ public void init(Map initInfo) { * @return set of roles as strings */ @Override - protected Set getUserRoles(Principal principal) { + public Set getUserRoles(Principal principal) { if(principal instanceof VerifiedUserRoles) { return ((VerifiedUserRoles) principal).getVerifiedRoles(); } else { From bfd8e8913c77b1790d24defc54400fd04c087dcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 1 May 2020 23:54:21 +0200 Subject: [PATCH 17/24] Restore tests --- .../solr/security/BaseTestRuleBasedAuthorizationPlugin.java | 4 ++-- .../TestExternalRoleRuleBasedAuthorizationPlugin.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java b/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java index 475be57987fd..bb00970905b9 100644 --- a/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java +++ b/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java @@ -32,6 +32,7 @@ import org.apache.solr.SolrTestCaseJ4; import org.apache.solr.common.params.MapSolrParams; import org.apache.solr.common.params.SolrParams; +import org.apache.solr.common.util.CommandOperation; import org.apache.solr.common.util.Utils; import org.apache.solr.handler.DumpRequestHandler; import org.apache.solr.handler.ReplicationHandler; @@ -44,7 +45,6 @@ import org.apache.solr.request.SolrRequestHandler; import org.apache.solr.security.AuthorizationContext.CollectionRequest; import org.apache.solr.security.AuthorizationContext.RequestType; -import org.apache.solr.common.util.CommandOperation; import org.hamcrest.core.IsInstanceOf; import org.hamcrest.core.IsNot; import org.junit.Test; @@ -52,9 +52,9 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; +import static org.apache.solr.common.util.CommandOperation.captureErrors; import static org.apache.solr.common.util.Utils.getObjectByPath; import static org.apache.solr.common.util.Utils.makeMap; -import static org.apache.solr.common.util.CommandOperation.captureErrors; @SuppressWarnings("unchecked") public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { diff --git a/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java b/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java index e8412171de3f..97626e2e1f45 100644 --- a/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java +++ b/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java @@ -55,7 +55,7 @@ AuthorizationContext getMockContext(Map values) { @Override public Principal getUserPrincipal() { String userPrincipal = (String) values.get("userPrincipal"); - return userPrincipal == null ? null : + return userPrincipal == null ? null : principals.get(userPrincipal) != null ? principals.get(userPrincipal) : new BasicUserPrincipal(userPrincipal); } From 4cf6759f7a4c920c095aad40914adf0b0c66f441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sat, 2 May 2020 00:10:22 +0200 Subject: [PATCH 18/24] Precommit --- .../test/org/apache/solr/security/PrincipalWithUserRoles.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/security/PrincipalWithUserRoles.java b/solr/core/src/test/org/apache/solr/security/PrincipalWithUserRoles.java index e8c9fa6bebad..8d27d0b00d53 100644 --- a/solr/core/src/test/org/apache/solr/security/PrincipalWithUserRoles.java +++ b/solr/core/src/test/org/apache/solr/security/PrincipalWithUserRoles.java @@ -20,8 +20,6 @@ import java.util.Objects; import java.util.Set; -import org.apache.http.util.Args; - /** * Type of Principal object that can contain also a list of roles the user has. * One use case can be to keep track of user-role mappings in an Identity Server From f5c7b6d67e415ef1bca81218e9573fe9bc33f58c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sat, 2 May 2020 01:08:17 +0200 Subject: [PATCH 19/24] Update refguide docs --- .../src/rule-based-authorization-plugin.adoc | 63 ++++++++++++++----- solr/solr-ref-guide/src/securing-solr.adoc | 1 + 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc b/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc index 99d09874795a..b61856961562 100644 --- a/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc +++ b/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc @@ -16,7 +16,7 @@ // specific language governing permissions and limitations // under the License. -Solr's authentication plugins control whether users can access Solr in a binary fashion. A user is either authenticated, or they aren't. For more fine-grained access control, Solr's Rule-Based Authorization Plugin (hereafter, "RBAP") can be used. +Solr's authentication plugins control whether users can access Solr in a binary fashion. A user is either authenticated, or they aren't. For more fine-grained access control, Solr's Rule-Based Authorization Plugins (hereafter, "RBAP") can be used. [CAUTION] ==== @@ -35,7 +35,10 @@ The users that RBAP sees come from whatever authentication plugin has been confi === Roles -Roles help bridge the gap between users and permissions. Users are assigned one or more roles, and permissions are then given to each of these roles in `security.json` +Roles help bridge the gap between users and permissions. The roles can be used with any of the authentication plugins or with a custom authentication plugin if you have created one. You will only need to ensure that logged-in users are mapped to the roles defined by the plugin. There are two implementaions of the plugin, which only differs in how the user's roles are obtained: + +* `RuleBasedAuthorizationPlugin`: The role-to-user mappings must be defined explicitly in `security.json` for every possible authenticated user. +* `ExternalRoleRuleBasedAuthorizationPlugin`: The role-to-user mappings are managed externally. This plugin expects the user's roles to be present on the `Principal` object which is part of the request. === Permissions @@ -43,7 +46,7 @@ Permissions control which roles (and consequently, which users) have access to p Administrators can use permissions from a list of predefined options or define their own custom permissions, are are free to mix and match both. -== Configuring the Rule-Based Authorization Plugin +== Configuring the Rule-Based Authorization Plugins Like all of Solr's security plugins, configuration for RBAP lives in a file or ZooKeeper node with the name `security.json`. See <> for more information on how to setup `security.json` in your cluster. @@ -54,15 +57,6 @@ Solr offers an <> for making changes to RBAP configuration. RBAP configuration consists of a small number of required configuration properties. Each of these lives under the `authorization` top level property in `security.json` class:: The authorization plugin to use. For RBAP, this value will always be `solr.RuleBasedAuthorizationPlugin` -user-role:: A mapping of individual users to the roles they belong to. The value of this property is a JSON map, where each property name is a user, and each property value is either the name of a single role or a JSON array of multiple roles that the specified user belongs to. For example: -+ -[source,json] ----- -"user-role": { - "user1": "role1", - "user2": ["role1", "role2"] -} ----- permissions:: A JSON array of permission rules used to restrict access to sections of Solr's API. For example: + [source,json] @@ -75,9 +69,21 @@ permissions:: A JSON array of permission rules used to restrict access to sectio + The syntax for individual permissions is more involved and is treated in greater detail <>. -=== Complete Example +User's roles may either come from the request itself, then you will use the `ExternalRoleRuleBasedAuthorizationPlugin` variant of RBAC. If you need to hardcode user-role mappings, then you need to use the `RuleBasedAuthorizationPlugin` and define the user-role mappings in `security.json` like this: -The example below shows how the configuration properties above can be used to achieve a typical (if simple) RBAP use-case. +user-role:: A mapping of individual users to the roles they belong to. The value of this property is a JSON map, where each property name is a user, and each property value is either the name of a single role or a JSON array of multiple roles that the specified user belongs to. For example: ++ +[source,json] +---- +"user-role": { + "user1": "role1", + "user2": ["role1", "role2"] +} +---- + +=== Example for RuleBasedAuthorizationPlugin and BasicAuth + +This example `security.json` shows how the <> can work with the `RuleBasedAuthorizationPlugin` plugin: [source,json] ---- @@ -112,6 +118,35 @@ The example below shows how the configuration properties above can be used to ac Altogether, this example carves out two restricted areas. Only `admin-user` can access Solr's Authentication and Authorization APIs, and only `dev-user` can access their `dev-private` collection. All other APIs are left open, and can be accessed by both users. +=== Example for ExternalRoleRuleBasedAuthorizationPlugin with JWT auth + +This example `security.json` shows how the <>, which pulls user and user roles from JWT claims, can work with the `ExternalRoleRuleBasedAuthorizationPlugin` plugin: + +[source,json] +---- +{ +"authentication":{ + "class": "solr.JWTAuthPlugin", <1> + "jwksUrl": "https://my.key.server/jwk.json", <2> + "rolesClaim": "roles" <3> +}, +"authorization":{ + "class":"solr.ExternalRoleRuleBasedAuthorizationPlugin", <4> + "permissions":[{"name":"security-edit", + "role":"admin"}] <5> +}} +---- + +Let's walk through this example: + +<1> JWT Authentication plugin is enabled +<2> Public keys is pulled over https +<3> We expect each JWT token to contain a "roles" claim, which will be passed on to Authorization +<4> External Role Rule-based authorization plugin is enabled. +<5> The 'admin' role has been defined, and it has permission to edit security settings. + +Only requests from users having a JWT token with role "admin" will be granted the `security-edit` permission. + == Permissions Solr's Rule-Based Authorization plugin supports a flexible and powerful permission syntax. RBAP supports two types of permissions, each with a slightly different syntax. diff --git a/solr/solr-ref-guide/src/securing-solr.adoc b/solr/solr-ref-guide/src/securing-solr.adoc index 1d3baee532f3..837256e88f7a 100644 --- a/solr/solr-ref-guide/src/securing-solr.adoc +++ b/solr/solr-ref-guide/src/securing-solr.adoc @@ -53,6 +53,7 @@ Authorization makes sure that only users with the necessary roles/permissions ca // tag::list-of-authorization-plugins[] * <> +* <> // end::list-of-authorization-plugins[] === Audit Logging Plugins From 0b69d92838e06bde2ed723a41f4b8cef9807c6be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sat, 2 May 2020 01:33:02 +0200 Subject: [PATCH 20/24] Move CHANGES entry to 8.6 Add Javadocs --- solr/CHANGES.txt | 4 ++-- .../apache/solr/security/RuleBasedAuthorizationPlugin.java | 2 +- .../solr/security/BaseTestRuleBasedAuthorizationPlugin.java | 4 ++++ .../TestExternalRoleRuleBasedAuthorizationPlugin.java | 3 +++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index 09b96704d727..53f9cf690f82 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -80,6 +80,8 @@ New Features * SOLR-14237: A new panel with security info in admin UI's dashboard (Ishan Chattopadhyaya, Moshe Bla) +* SOLR-12131: ExternalRoleRuleBasedAuthorizationPlugin which gets user's roles from request (janhoy) + Improvements --------------------- * SOLR-14316: Remove unchecked type conversion warning in JavaBinCodec's readMapEntry's equals() method @@ -220,8 +222,6 @@ New Features the synonyms file, and adding a DelimitedBoostTokenFilter to the analysis chain (Alessandro Benedetti, Alan Woodward) -* SOLR-12131: ExternalRoleRuleBasedAuthorizationPlugin which gets user's roles from request (janhoy) - Improvements --------------------- * SOLR-14120: Define JavaScript methods 'includes' and 'startsWith' to ensure AdminUI can be displayed when using diff --git a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java index 6aca3f6fef30..ef012c0853df 100644 --- a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java @@ -48,7 +48,7 @@ public void init(Map initInfo) { } /** - * Implementers should calculate the users roles + * Look up user's role from the explicit user-role mapping * * @param principal the user Principal from the request * @return set of roles as strings diff --git a/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java b/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java index bb00970905b9..43ff9a8e05a5 100644 --- a/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java +++ b/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java @@ -56,6 +56,10 @@ import static org.apache.solr.common.util.Utils.getObjectByPath; import static org.apache.solr.common.util.Utils.makeMap; +/** + * Base class for testing RBAC. This will test the {@link RuleBasedAuthorizationPlugin} implementation + * but also serves as a base class for testing other sub classes + */ @SuppressWarnings("unchecked") public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { protected Map rules; diff --git a/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java b/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java index 97626e2e1f45..c36cc255307f 100644 --- a/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java +++ b/solr/core/src/test/org/apache/solr/security/TestExternalRoleRuleBasedAuthorizationPlugin.java @@ -26,6 +26,9 @@ import org.apache.http.auth.BasicUserPrincipal; +/** + * Tests {@link ExternalRoleRuleBasedAuthorizationPlugin} through simulating principals with roles attached + */ public class TestExternalRoleRuleBasedAuthorizationPlugin extends BaseTestRuleBasedAuthorizationPlugin { private HashMap principals; From 44009fd999bc6c8dd81c2c0323aa77d82d0e1744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sat, 2 May 2020 16:38:02 +0200 Subject: [PATCH 21/24] Add parsing of 'rolesClaim' in JWTAuthPlugin, and document this. Drop Serializable from JWTPrincipal --- .../apache/solr/security/JWTAuthPlugin.java | 31 ++++++++++++++++--- .../apache/solr/security/JWTPrincipal.java | 3 +- .../src/jwt-authentication-plugin.adoc | 1 + 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java b/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java index 79875864668a..fb7b9db02c9e 100644 --- a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java @@ -74,6 +74,7 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, private static final String PARAM_REQUIRE_SUBJECT = "requireSub"; private static final String PARAM_REQUIRE_ISSUER = "requireIss"; private static final String PARAM_PRINCIPAL_CLAIM = "principalClaim"; + private static final String PARAM_ROLES_CLAIM = "rolesClaim"; private static final String PARAM_REQUIRE_EXPIRATIONTIME = "requireExp"; private static final String PARAM_ALG_WHITELIST = "algWhitelist"; private static final String PARAM_JWK_CACHE_DURATION = "jwkCacheDur"; @@ -92,7 +93,7 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, private static final Set PROPS = ImmutableSet.of(PARAM_BLOCK_UNKNOWN, PARAM_REQUIRE_SUBJECT, PARAM_PRINCIPAL_CLAIM, PARAM_REQUIRE_EXPIRATIONTIME, PARAM_ALG_WHITELIST, - PARAM_JWK_CACHE_DURATION, PARAM_CLAIMS_MATCH, PARAM_SCOPE, PARAM_REALM, + PARAM_JWK_CACHE_DURATION, PARAM_CLAIMS_MATCH, PARAM_SCOPE, PARAM_REALM, PARAM_ROLES_CLAIM, PARAM_ADMINUI_SCOPE, PARAM_REDIRECT_URIS, PARAM_REQUIRE_ISSUER, PARAM_ISSUERS, // These keys are supported for now to enable PRIMARY issuer config through top-level keys JWTIssuerConfig.PARAM_JWK_URL, JWTIssuerConfig.PARAM_JWKS_URL, JWTIssuerConfig.PARAM_JWK, JWTIssuerConfig.PARAM_ISSUER, @@ -103,6 +104,7 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, private boolean requireExpirationTime; private List algWhitelist; private String principalClaim; + private String rolesClaim; private HashMap claimsMatchCompiled; private boolean blockUnknown; private List requiredScopes = new ArrayList<>(); @@ -140,6 +142,8 @@ public void init(Map pluginConfig) { PARAM_REQUIRE_SUBJECT); } principalClaim = (String) pluginConfig.getOrDefault(PARAM_PRINCIPAL_CLAIM, "sub"); + + rolesClaim = (String) pluginConfig.get(PARAM_ROLES_CLAIM); algWhitelist = (List) pluginConfig.get(PARAM_ALG_WHITELIST); realm = (String) pluginConfig.getOrDefault(PARAM_REALM, DEFAULT_AUTH_REALM); @@ -403,6 +407,8 @@ protected JWTAuthenticationResponse authenticate(String authorizationHeader) { // Fail if we require scopes but they don't exist return new JWTAuthenticationResponse(AuthCode.CLAIM_MISMATCH, "Claim " + CLAIM_SCOPE + " is required but does not exist in JWT"); } + + // Find scopes for user Set scopes = Collections.emptySet(); Object scopesObj = jwtClaims.getClaimValue(CLAIM_SCOPE); if (scopesObj != null) { @@ -417,10 +423,27 @@ protected JWTAuthenticationResponse authenticate(String authorizationHeader) { return new JWTAuthenticationResponse(AuthCode.SCOPE_MISSING, "Claim " + CLAIM_SCOPE + " does not contain any of the required scopes: " + requiredScopes); } } - final Set finalScopes = new HashSet<>(scopes); - finalScopes.remove("openid"); // Remove standard scope + } + + // Determine roles of user, either from 'rolesClaim' or from 'scope' as parsed above + final Set finalRoles = new HashSet<>(); + if (rolesClaim == null) { // Pass scopes with principal to signal to any Authorization plugins that user has some verified role claims - return new JWTAuthenticationResponse(AuthCode.AUTHENTICATED, new JWTPrincipalWithUserRoles(principal, jwtCompact, jwtClaims.getClaimsMap(), finalScopes)); + finalRoles.addAll(scopes); + finalRoles.remove("openid"); // Remove standard scope + } else { + // Pull roles from separate claim, either as whitespace separated list or as JSON array + Object rolesObj = jwtClaims.getClaimValue(rolesClaim); + if (rolesObj != null) { + if (rolesObj instanceof String) { + finalRoles.addAll(Arrays.asList(((String) rolesObj).split("\\s+"))); + } else if (rolesObj instanceof List) { + finalRoles.addAll(jwtClaims.getStringListClaimValue(rolesClaim)); + } + } + } + if (finalRoles.size() > 0) { + return new JWTAuthenticationResponse(AuthCode.AUTHENTICATED, new JWTPrincipalWithUserRoles(principal, jwtCompact, jwtClaims.getClaimsMap(), finalRoles)); } else { return new JWTAuthenticationResponse(AuthCode.AUTHENTICATED, new JWTPrincipal(principal, jwtCompact, jwtClaims.getClaimsMap())); } diff --git a/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java b/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java index 737f3fa8e4a8..ee99755a0fea 100644 --- a/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java +++ b/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java @@ -27,8 +27,7 @@ /** * Principal object that carries JWT token and claims for authenticated user. */ -public class JWTPrincipal implements Principal, Serializable { - private static final long serialVersionUID = 4144666467522831388L; +public class JWTPrincipal implements Principal { final String username; String token; Map claims; diff --git a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc index dbe6147c2345..c24e02bc7737 100644 --- a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc +++ b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc @@ -50,6 +50,7 @@ requireExp ; Fails requests that lacks an `exp` (expiry time) claim algWhitelist ; JSON array with algorithms to accept: `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`, `none ; Default is to allow all algorithms jwkCacheDur ; Duration of JWK cache in seconds ; `3600` (1 hour) principalClaim ; What claim id to pull principal from ; `sub` +rolesClaim ; What claim id to pull user roles from. The claim must then either contain a space separated list of roles or a JSON array. The roles can then be used to define fine-grained access in an Authorization plugin ; By default the scopes from `scope` claim are passed on as user roles claimsMatch ; JSON object of claims (key) that must match a regular expression (value). Example: `{ "foo" : "A|B" }` will require the `foo` claim to be either "A" or "B". ; adminUiScope ; Define what scope is requested when logging in from Admin UI ; If not defined, the first scope from `scope` parameter is used redirectUris ; Valid location(s) for redirect after external authentication. Takes a string or array of strings. Must be the base URL of Solr, e.g., https://solr1.example.com:8983/solr/ and must match the list of redirect URIs registered with the Identity Provider beforehand. ; Defaults to empty list, i.e., any node is assumed to be a valid redirect target. From 90540885e6e8777e4c3530daecae7c7a1024b2fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sun, 3 May 2020 23:26:21 +0200 Subject: [PATCH 22/24] Add test for rolesClaim --- .../solr/security/JWTAuthPluginTest.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java index 5ed1032c2f34..7b04c95daefe 100644 --- a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java +++ b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java @@ -113,8 +113,8 @@ protected static JwtClaims generateClaims() { claims.setClaim("claim1", "foo"); // additional claims/attributes about the subject can be added claims.setClaim("claim2", "bar"); // additional claims/attributes about the subject can be added claims.setClaim("claim3", "foo"); // additional claims/attributes about the subject can be added - List groups = Arrays.asList("group-one", "other-group", "group-three"); - claims.setStringListClaim("groups", groups); // multi-valued claims work too and will end up as a JSON array + List roles = Arrays.asList("group-one", "other-group", "group-three"); + claims.setStringListClaim("roles", roles); // multi-valued claims work too and will end up as a JSON array return claims; } @@ -325,6 +325,7 @@ public void scope() { JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader); assertTrue(resp.getErrorMessage(), resp.isAuthenticated()); + // When 'rolesClaim' is not defined in config, then all scopes are registered as roles Principal principal = resp.getPrincipal(); assertTrue(principal instanceof VerifiedUserRoles); Set roles = ((VerifiedUserRoles)principal).getVerifiedRoles(); @@ -332,6 +333,23 @@ public void scope() { assertTrue(roles.contains("solr:read")); } + @Test + public void roles() { + testConfig.put("rolesClaim", "roles"); + plugin.init(testConfig); + JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader); + assertTrue(resp.getErrorMessage(), resp.isAuthenticated()); + + // When 'rolesClaim' is defined in config, then roles from that claim are used instead of claims + Principal principal = resp.getPrincipal(); + assertTrue(principal instanceof VerifiedUserRoles); + Set roles = ((VerifiedUserRoles)principal).getVerifiedRoles(); + assertEquals(3, roles.size()); + assertTrue(roles.contains("group-one")); + assertTrue(roles.contains("other-group")); + assertTrue(roles.contains("group-three")); + } + @Test public void wrongScope() { testConfig.put("scope", "wrong"); From 26cf0ba72c1e67cf29ef13800b99f6a5474d393e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sun, 3 May 2020 23:26:42 +0200 Subject: [PATCH 23/24] Address noble's documentation feedback --- solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc b/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc index b61856961562..6eb63f55ef2a 100644 --- a/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc +++ b/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc @@ -35,10 +35,10 @@ The users that RBAP sees come from whatever authentication plugin has been confi === Roles -Roles help bridge the gap between users and permissions. The roles can be used with any of the authentication plugins or with a custom authentication plugin if you have created one. You will only need to ensure that logged-in users are mapped to the roles defined by the plugin. There are two implementaions of the plugin, which only differs in how the user's roles are obtained: +Roles help bridge the gap between users and permissions. The roles can be used with any of the authentication plugins or with a custom authentication plugin if you have created one. You will only need to ensure that logged-in users are mapped to the roles defined by the plugin. There are two implementations of the plugin, which only differs in how the user's roles are obtained: * `RuleBasedAuthorizationPlugin`: The role-to-user mappings must be defined explicitly in `security.json` for every possible authenticated user. -* `ExternalRoleRuleBasedAuthorizationPlugin`: The role-to-user mappings are managed externally. This plugin expects the user's roles to be present on the `Principal` object which is part of the request. +* `ExternalRoleRuleBasedAuthorizationPlugin`: The role-to-user mappings are managed externally. This plugin expects the AuthenticationPlugin to provide a Principal that has the roles information as well, implementing the `VerifiedUserRoles` interface. === Permissions From 2e31fde1a350cc2faa5e9e79513e93ab2bda6c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sun, 3 May 2020 23:38:15 +0200 Subject: [PATCH 24/24] Precommit --- solr/core/src/java/org/apache/solr/security/JWTPrincipal.java | 1 - 1 file changed, 1 deletion(-) diff --git a/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java b/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java index ee99755a0fea..810e49ce83c6 100644 --- a/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java +++ b/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java @@ -17,7 +17,6 @@ package org.apache.solr.security; -import java.io.Serializable; import java.security.Principal; import java.util.Map; import java.util.Objects;