SOLR-10814 Add short-name feature to RuleBasedAuthz plugin

Additional-Author: Hrishikesh Gadre <hgadre@cloudera.com>
This commit is contained in:
Mike Drob 2020-05-07 14:02:36 -05:00
parent 06b1f3e866
commit d3f4b21deb
No known key found for this signature in database
GPG Key ID: 3E48C0C6EF362B9E
15 changed files with 415 additions and 207 deletions

View File

@ -96,11 +96,20 @@ public abstract class AuthenticationPlugin implements SolrInfoBean {
}
HttpServletRequest wrapWithPrincipal(HttpServletRequest request, Principal principal) {
return wrapWithPrincipal(request, principal, principal.getName());
}
HttpServletRequest wrapWithPrincipal(HttpServletRequest request, Principal principal, String username) {
return new HttpServletRequestWrapper(request) {
@Override
public Principal getUserPrincipal() {
return principal;
}
@Override
public String getRemoteUser() {
return username;
}
};
}

View File

@ -42,12 +42,34 @@ public abstract class AuthorizationContext {
public abstract SolrParams getParams() ;
/**
* This method returns the {@link Principal} corresponding to
* the authenticated user for the current request. The value returned by
* {@link Principal#getName()} depends on the authentication mechanism
* used (e.g. for user "foo" with BASIC authentication the result would be
* "foo". On the other hand with KERBEROS it would be foo@REALMNAME).
* The {@link #getUserName()} method may be preferred to extract the identity
* of the authenticated user instead of this method.
*
* @return user principal in case of an authenticated request
* null in case of unauthenticated request
*/
public abstract Principal getUserPrincipal() ;
/**
* This method returns the name of the authenticated user for the current request.
* The return value of this method is agnostic of the underlying authentication
* mechanism used.
*
* @return user name in case of an authenticated user
* null in case of unauthenticated request
*/
public abstract String getUserName();
public abstract String getHttpHeader(String header);
@SuppressWarnings({"rawtypes"})
public abstract Enumeration getHeaderNames();
public abstract Enumeration<String> getHeaderNames();
public abstract String getRemoteAddr();

View File

@ -145,7 +145,7 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
return false;
} else {
Principal principal = new BasicAuthUserPrincipal(username, pwd);
request = wrapWithPrincipal(request, principal);
request = wrapWithPrincipal(request, principal, username);
numAuthenticated.inc();
filterChain.doFilter(request, response);
return true;

View File

@ -17,7 +17,6 @@
package org.apache.solr.security;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
@ -49,8 +48,6 @@ import org.apache.solr.common.cloud.ZkCredentialsProvider;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.data.ACL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is an authentication filter based on Hadoop's {@link DelegationTokenAuthenticationFilter}.
@ -58,8 +55,6 @@ import org.slf4j.LoggerFactory;
* application to reuse the authentication of an end-user or another application.
*/
public class DelegationTokenKerberosFilter extends DelegationTokenAuthenticationFilter {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private CuratorFramework curatorFramework;
private final Locale defaultLocale = Locale.getDefault();
@ -83,13 +78,12 @@ public class DelegationTokenKerberosFilter extends DelegationTokenAuthentication
* "solr.impersonator.user.name" will be added to the configuration.
*/
@Override
protected Configuration getProxyuserConfiguration(FilterConfig filterConf)
throws ServletException {
protected Configuration getProxyuserConfiguration(FilterConfig filterConf) {
Configuration conf = new Configuration(false);
Enumeration<?> names = filterConf.getInitParameterNames();
Enumeration<String> names = filterConf.getInitParameterNames();
while (names.hasMoreElements()) {
String name = (String) names.nextElement();
String name = names.nextElement();
if (name.startsWith(KerberosPlugin.IMPERSONATOR_PREFIX)) {
String value = filterConf.getInitParameter(name);
conf.set(PROXYUSER_PREFIX + "." + name.substring(KerberosPlugin.IMPERSONATOR_PREFIX.length()), value);
@ -163,11 +157,8 @@ public class DelegationTokenKerberosFilter extends DelegationTokenAuthentication
// without the appropriate ACL configuration. This issue is possibly related to HADOOP-11973
try {
zkClient.makePath(SecurityAwareZkACLProvider.SECURITY_ZNODE_PATH, CreateMode.PERSISTENT, true);
} catch (KeeperException ex) {
if (ex.code() != KeeperException.Code.NODEEXISTS) {
throw ex;
}
} catch (KeeperException.NodeExistsException ex) {
// ignore?
}
curatorFramework = CuratorFrameworkFactory.builder()

View File

@ -16,23 +16,18 @@
*/
package org.apache.solr.security;
import java.lang.invoke.MethodHandles;
import java.security.Principal;
import java.util.Collections;
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<String, Object> initInfo) {
super.init(initInfo);

View File

@ -114,9 +114,7 @@ public class KerberosPlugin extends AuthenticationPlugin implements HttpClientBu
putParamOptional(params, "kerberos.keytab", KEYTAB_PARAM);
}
String delegationTokenStr = System.getProperty(DELEGATION_TOKEN_ENABLED, null);
boolean delegationTokenEnabled =
(delegationTokenStr == null) ? false : Boolean.parseBoolean(delegationTokenStr);
boolean delegationTokenEnabled = Boolean.getBoolean(DELEGATION_TOKEN_ENABLED);
ZkController controller = coreContainer.getZkController();
if (delegationTokenEnabled) {
@ -162,7 +160,7 @@ public class KerberosPlugin extends AuthenticationPlugin implements HttpClientBu
}
// check impersonator config
for (@SuppressWarnings({"rawtypes"})Enumeration e = System.getProperties().propertyNames(); e.hasMoreElements();) {
for (Enumeration<?> e = System.getProperties().propertyNames(); e.hasMoreElements();) {
String key = e.nextElement().toString();
if (key.startsWith(IMPERSONATOR_PREFIX)) {
if (!delegationTokenEnabled) {

View File

@ -16,15 +16,11 @@
*/
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.solr.handler.admin.SecurityConfHandler.getMapValue;
/**
@ -32,24 +28,33 @@ import static org.apache.solr.handler.admin.SecurityConfHandler.getMapValue;
* mapping in the security.json configuration
*/
public class RuleBasedAuthorizationPlugin extends RuleBasedAuthorizationPluginBase {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final Map<String, Set<String>> usersVsRoles = new HashMap<>();
private boolean useShortName;
@Override
public void init(Map<String, Object> initInfo) {
super.init(initInfo);
Map<String, Object> map = getMapValue(initInfo, "user-role");
for (Object o : map.entrySet()) {
@SuppressWarnings({"rawtypes"})
Map.Entry e = (Map.Entry) o;
String roleName = (String) e.getKey();
@SuppressWarnings("unchecked")
Map.Entry<String, ?> e = (Map.Entry<String, ?>) o;
String roleName = e.getKey();
usersVsRoles.put(roleName, Permission.readValueAsSet(map, roleName));
}
useShortName = Boolean.parseBoolean(initInfo.getOrDefault("useShortName", Boolean.FALSE).toString());
}
@Override
public Set<String> getUserRoles(AuthorizationContext context) {
if (useShortName) {
return usersVsRoles.get(context.getUserName());
} else {
return getUserRoles(context.getUserPrincipal());
}
}
/**
* Look up user's role from the explicit user-role mapping
* 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

View File

@ -20,6 +20,7 @@ import java.io.IOException;
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;
@ -34,8 +35,6 @@ 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;
@ -47,38 +46,40 @@ public abstract class RuleBasedAuthorizationPluginBase implements AuthorizationP
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final Map<String, WildCardSupportMap> mapping = new HashMap<>();
private final List<Permission> permissions = new ArrayList<>();
private static class WildCardSupportMap extends HashMap<String, List<Permission>> {
// Doesn't implement Map because we violate the contracts of put() and get()
private static class WildCardSupportMap {
final Set<String> wildcardPrefixes = new HashSet<>();
final Map<String, List<Permission>> delegate = new HashMap<>();
@Override
public List<Permission> put(String key, List<Permission> value) {
if (key != null && key.endsWith("/*")) {
key = key.substring(0, key.length() - 2);
wildcardPrefixes.add(key);
}
return super.put(key, value);
return delegate.put(key, value);
}
@Override
public List<Permission> get(Object key) {
List<Permission> result = super.get(key);
public List<Permission> get(String key) {
List<Permission> result = delegate.get(key);
if (key == null || result != null) return result;
if (!wildcardPrefixes.isEmpty()) {
for (String s : wildcardPrefixes) {
if (key.toString().startsWith(s)) {
List<Permission> l = super.get(s);
if (l != null) {
result = result == null ? new ArrayList<>() : new ArrayList<>(result);
result.addAll(l);
}
if (key.startsWith(s)) {
List<Permission> wildcardPermissions = delegate.get(s);
if (wildcardPermissions != null) {
if (result == null) result = new ArrayList<>();
result.addAll(wildcardPermissions);
}
}
}
return result;
}
public Set<String> keySet() {
return delegate.keySet();
}
}
@Override
@ -109,8 +110,7 @@ public abstract class RuleBasedAuthorizationPluginBase implements AuthorizationP
return flag.rsp;
}
private MatchStatus checkCollPerm(Map<String, List<Permission>> pathVsPerms,
AuthorizationContext context) {
private MatchStatus checkCollPerm(WildCardSupportMap pathVsPerms, AuthorizationContext context) {
if (pathVsPerms == null) return MatchStatus.NO_PERMISSIONS_FOUND;
if (log.isTraceEnabled()) {
@ -131,7 +131,6 @@ public abstract class RuleBasedAuthorizationPluginBase implements AuthorizationP
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);
@ -145,7 +144,7 @@ public abstract class RuleBasedAuthorizationPluginBase implements AuthorizationP
log.debug("Found perm [{}] to govern resource [{}]", governingPermission, context.getResource());
}
return determineIfPermissionPermitsPrincipal(principal, governingPermission);
return determineIfPermissionPermitsPrincipal(context, governingPermission);
}
private Permission findFirstGoverningPermission(List<Permission> permissions, AuthorizationContext context) {
@ -216,11 +215,12 @@ public abstract class RuleBasedAuthorizationPluginBase implements AuthorizationP
return true;
}
private MatchStatus determineIfPermissionPermitsPrincipal(Principal principal, Permission governingPermission) {
private MatchStatus determineIfPermissionPermitsPrincipal(AuthorizationContext context, Permission governingPermission) {
if (governingPermission.role == null) {
log.debug("Governing permission [{}] has no role; permitting access", governingPermission);
return MatchStatus.PERMITTED;
}
Principal principal = context.getUserPrincipal();
if (principal == null) {
log.debug("Governing permission [{}] has role, but request principal cannot be identified; forbidding access", governingPermission);
return MatchStatus.USER_REQUIRED;
@ -229,7 +229,7 @@ public abstract class RuleBasedAuthorizationPluginBase implements AuthorizationP
return MatchStatus.PERMITTED;
}
Set<String> userRoles = getUserRoles(principal);
Set<String> userRoles = getUserRoles(context);
for (String role : governingPermission.role) {
if (userRoles != null && userRoles.contains(role)) {
log.debug("Governing permission [{}] allows access to role [{}]; permitting access", governingPermission, role);
@ -260,7 +260,7 @@ public abstract class RuleBasedAuthorizationPluginBase implements AuthorizationP
@Override
@SuppressWarnings({"unchecked"})
public void init(@SuppressWarnings({"rawtypes"})Map<String, Object> initInfo) {
public void init(Map<String, Object> initInfo) {
mapping.put(null, new WildCardSupportMap());
@SuppressWarnings({"rawtypes"})
List<Map> perms = getListValue(initInfo, "permissions");
@ -272,7 +272,6 @@ public abstract class RuleBasedAuthorizationPluginBase implements AuthorizationP
log.error("Invalid permission ", exp);
continue;
}
permissions.add(p);
add2Mapping(p);
}
}
@ -280,8 +279,7 @@ public abstract class RuleBasedAuthorizationPluginBase implements AuthorizationP
//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());
WildCardSupportMap m = mapping.computeIfAbsent(c, k -> new WildCardSupportMap());
for (String path : permission.path) {
List<Permission> perms = m.get(path);
if (perms == null) m.put(path, perms = new ArrayList<>());
@ -290,6 +288,15 @@ public abstract class RuleBasedAuthorizationPluginBase implements AuthorizationP
}
}
/**
* Finds user roles
* @param context the authorization context to load roles from
* @return set of roles as strings or empty set if no roles are found
*/
public Set<String> getUserRoles(AuthorizationContext context) {
return getUserRoles(context.getUserPrincipal());
}
/**
* Finds users roles
* @param principal the user Principal to fetch roles for
@ -330,12 +337,11 @@ public abstract class RuleBasedAuthorizationPluginBase implements AuthorizationP
return latestConf;
}
private static final Map<String, AutorizationEditOperation> ops = unmodifiableMap(asList(AutorizationEditOperation.values()).stream().collect(toMap(AutorizationEditOperation::getOperationName, identity())));
private static final Map<String, AutorizationEditOperation> ops = Arrays.stream(AutorizationEditOperation.values()).collect(toMap(AutorizationEditOperation::getOperationName, identity()));
@Override
public ValidatingJsonMap getSpec() {
return Utils.getSpec("cluster.security.RuleBasedAuthorization").getSpec();
}
}

View File

@ -1133,6 +1133,11 @@ public class HttpSolrCall {
return getReq().getUserPrincipal();
}
@Override
public String getUserName() {
return getReq().getRemoteUser();
}
@Override
public String getHttpHeader(String s) {
return getReq().getHeader(s);

View File

@ -0,0 +1,37 @@
{
"authentication": {
"class": "org.apache.solr.security.ConfigurableInternodeAuthHadoopPlugin",
"sysPropPrefix": "solr.",
"type": "kerberos",
"clientBuilderFactory": "org.apache.solr.client.solrj.impl.Krb5HttpClientBuilder",
"initKerberosZk": "true",
"authConfigs": [
"kerberos.principal",
"kerberos.keytab",
"kerberos.name.rules"
],
"defaultConfigs": {
}
},
"authorization":{
"class":"solr.RuleBasedAuthorizationPlugin",
"useShortName": "true",
"permissions":[
{
"name":"collection-admin-edit",
"role":"admin"
},
{
"name":"read",
"role":"admin"
},
{
"name":"update",
"role":"admin"
}
],
"user-role":{
"solr":"admin"
}
}
}

View File

@ -23,10 +23,8 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
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;
@ -45,8 +43,10 @@ import org.apache.solr.handler.component.SearchHandler;
import org.apache.solr.request.SolrRequestHandler;
import org.apache.solr.security.AuthorizationContext.CollectionRequest;
import org.apache.solr.security.AuthorizationContext.RequestType;
import org.apache.solr.util.LogLevel;
import org.hamcrest.core.IsInstanceOf;
import org.hamcrest.core.IsNot;
import org.junit.Before;
import org.junit.Test;
import static java.util.Collections.emptyMap;
@ -54,16 +54,18 @@ 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.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assume.assumeThat;
/**
* 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")
@LogLevel("org.apache.solr.security=TRACE")
public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
@SuppressWarnings({"rawtypes"})
protected Map rules;
protected Map<String, Object> rules;
final int STATUS_OK = 200;
final int FORBIDDEN = 403;
@ -75,6 +77,11 @@ public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
resetPermissionsAndRoles();
}
@Before
public void setupPermissionsAndRoles() {
resetPermissionsAndRoles();
}
protected void resetPermissionsAndRoles() {
String permissions = "{" +
" user-role : {" +
@ -97,110 +104,101 @@ public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
" }," +
"{name:read, role:dev }," +
"{name:freeforall, path:'/foo', role:'*'}]}";
rules = (Map) Utils.fromJSONString(permissions);
rules = (Map<String,Object>) Utils.fromJSONString(permissions);
}
@Test
public void testBasicPermissions() {
checkRules(makeMap("resource", "/update/json/docs",
checkRules(Map.of("resource", "/update/json/docs",
"httpMethod", "POST",
"userPrincipal", "unknownuser",
"collectionRequests", "freeforall",
"handler", new UpdateRequestHandler())
, STATUS_OK);
checkRules(makeMap("resource", "/update/json/docs",
checkRules(Map.of("resource", "/update/json/docs",
"httpMethod", "POST",
"userPrincipal", "tim",
"collectionRequests", "mycoll",
"handler", new UpdateRequestHandler())
, STATUS_OK);
checkRules(makeMap("resource", "/update/json/docs",
checkRules(Map.of("resource", "/update/json/docs",
"httpMethod", "POST",
"collectionRequests", "mycoll",
"handler", new UpdateRequestHandler())
, PROMPT_FOR_CREDENTIALS);
checkRules(makeMap("resource", "/schema",
checkRules(Map.of("resource", "/schema",
"userPrincipal", "somebody",
"collectionRequests", "mycoll",
"httpMethod", "POST",
"handler", new SchemaHandler())
, FORBIDDEN);
checkRules(makeMap("resource", "/schema",
checkRules(Map.of("resource", "/schema",
"userPrincipal", "somebody",
"collectionRequests", "mycoll",
"httpMethod", "GET",
"handler", new SchemaHandler())
, STATUS_OK);
checkRules(makeMap("resource", "/schema/fields",
checkRules(Map.of("resource", "/schema/fields",
"userPrincipal", "somebody",
"collectionRequests", "mycoll",
"httpMethod", "GET",
"handler", new SchemaHandler())
, STATUS_OK);
checkRules(makeMap("resource", "/schema",
checkRules(Map.of("resource", "/schema",
"userPrincipal", "somebody",
"collectionRequests", "mycoll",
"httpMethod", "POST",
"handler", new SchemaHandler())
, FORBIDDEN);
checkRules(makeMap("resource", "/admin/collections",
checkRules(Map.of("resource", "/admin/collections",
"userPrincipal", "tim",
"requestType", RequestType.ADMIN,
"collectionRequests", null,
"httpMethod", "GET",
"handler", new CollectionsHandler(),
"params", new MapSolrParams(singletonMap("action", "LIST")))
, STATUS_OK);
checkRules(makeMap("resource", "/admin/collections",
"userPrincipal", null,
checkRules(Map.of("resource", "/admin/collections",
"requestType", RequestType.ADMIN,
"collectionRequests", null,
"httpMethod", "GET",
"handler", new CollectionsHandler(),
"params", new MapSolrParams(singletonMap("action", "LIST")))
, STATUS_OK);
checkRules(makeMap("resource", "/admin/collections",
"userPrincipal", null,
checkRules(Map.of("resource", "/admin/collections",
"requestType", RequestType.ADMIN,
"collectionRequests", null,
"handler", new CollectionsHandler(),
"params", new MapSolrParams(singletonMap("action", "CREATE")))
, PROMPT_FOR_CREDENTIALS);
checkRules(makeMap("resource", "/admin/collections",
"userPrincipal", null,
checkRules(Map.of("resource", "/admin/collections",
"requestType", RequestType.ADMIN,
"collectionRequests", null,
"handler", new CollectionsHandler(),
"params", new MapSolrParams(singletonMap("action", "RELOAD")))
, PROMPT_FOR_CREDENTIALS);
checkRules(makeMap("resource", "/admin/collections",
checkRules(Map.of("resource", "/admin/collections",
"userPrincipal", "somebody",
"requestType", RequestType.ADMIN,
"collectionRequests", null,
"handler", new CollectionsHandler(),
"params", new MapSolrParams(singletonMap("action", "CREATE")))
, FORBIDDEN);
checkRules(makeMap("resource", "/admin/collections",
checkRules(Map.of("resource", "/admin/collections",
"userPrincipal", "tim",
"requestType", RequestType.ADMIN,
"collectionRequests", null,
"handler", new CollectionsHandler(),
"params", new MapSolrParams(singletonMap("action", "CREATE")))
, STATUS_OK);
checkRules(makeMap("resource", "/select",
checkRules(Map.of("resource", "/select",
"httpMethod", "GET",
"handler", new SearchHandler(),
"collectionRequests", singletonList(new CollectionRequest("mycoll")),
@ -210,94 +208,89 @@ public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
setUserRole("cio", "su");
addPermission("all", "su");
checkRules(makeMap("resource", ReplicationHandler.PATH,
checkRules(Map.of("resource", ReplicationHandler.PATH,
"httpMethod", "POST",
"userPrincipal", "tim",
"handler", new ReplicationHandler(),
"collectionRequests", singletonList(new CollectionRequest("mycoll")) )
, FORBIDDEN);
checkRules(makeMap("resource", ReplicationHandler.PATH,
checkRules(Map.of("resource", ReplicationHandler.PATH,
"httpMethod", "POST",
"userPrincipal", "cio",
"handler", new ReplicationHandler(),
"collectionRequests", singletonList(new CollectionRequest("mycoll")) )
, STATUS_OK);
checkRules(makeMap("resource", "/admin/collections",
checkRules(Map.of("resource", "/admin/collections",
"userPrincipal", "tim",
"requestType", AuthorizationContext.RequestType.ADMIN,
"collectionRequests", null,
"handler", new CollectionsHandler(),
"params", new MapSolrParams(singletonMap("action", "CREATE")))
, STATUS_OK);
}
resetPermissionsAndRoles();
@Test
public void testCoreAdminPermissions() {
addPermission("core-admin-edit", "su");
addPermission("core-admin-read", "user");
setUserRole("cio", "su");
addPermission("all", "su");
checkRules(makeMap("resource", "/admin/cores",
"userPrincipal", null,
checkRules(Map.of("resource", "/admin/cores",
"requestType", RequestType.ADMIN,
"collectionRequests", null,
"handler", new CoreAdminHandler(null),
"params", new MapSolrParams(singletonMap("action", "CREATE")))
, PROMPT_FOR_CREDENTIALS);
checkRules(makeMap("resource", "/admin/cores",
checkRules(Map.of("resource", "/admin/cores",
"userPrincipal", "joe",
"requestType", RequestType.ADMIN,
"collectionRequests", null,
"handler", new CoreAdminHandler(null),
"params", new MapSolrParams(singletonMap("action", "CREATE")))
, FORBIDDEN);
checkRules(makeMap("resource", "/admin/cores",
checkRules(Map.of("resource", "/admin/cores",
"userPrincipal", "joe",
"requestType", RequestType.ADMIN,
"collectionRequests", null,
"handler", new CoreAdminHandler(null),
"params", new MapSolrParams(singletonMap("action", "STATUS")))
, STATUS_OK);
checkRules(makeMap("resource", "/admin/cores",
checkRules(Map.of("resource", "/admin/cores",
"userPrincipal", "cio",
"requestType", RequestType.ADMIN,
"collectionRequests", null,
"handler", new CoreAdminHandler(null),
"params", new MapSolrParams(singletonMap("action", "CREATE")))
,STATUS_OK);
}
resetPermissionsAndRoles();
addPermission("test-params", "admin", "/x", makeMap("key", Arrays.asList("REGEX:(?i)val1", "VAL2")));
@Test
public void testParamsPermissions() {
addPermission("test-params", "admin", "/x", Map.of("key", Arrays.asList("REGEX:(?i)val1", "VAL2")));
checkRules(makeMap("resource", "/x",
"userPrincipal", null,
checkRules(Map.of("resource", "/x",
"requestType", RequestType.UNKNOWN,
"collectionRequests", "go",
"handler", new DumpRequestHandler(),
"params", new MapSolrParams(singletonMap("key", "VAL1")))
, PROMPT_FOR_CREDENTIALS);
checkRules(makeMap("resource", "/x",
"userPrincipal", null,
checkRules(Map.of("resource", "/x",
"requestType", RequestType.UNKNOWN,
"collectionRequests", "go",
"handler", new DumpRequestHandler(),
"params", new MapSolrParams(singletonMap("key", "Val1")))
, PROMPT_FOR_CREDENTIALS);
checkRules(makeMap("resource", "/x",
"userPrincipal", null,
checkRules(Map.of("resource", "/x",
"requestType", RequestType.UNKNOWN,
"collectionRequests", "go",
"handler", new DumpRequestHandler(),
"params", new MapSolrParams(singletonMap("key", "Val1")))
, PROMPT_FOR_CREDENTIALS);
checkRules(makeMap("resource", "/x",
checkRules(Map.of("resource", "/x",
"userPrincipal", "joe",
"requestType", RequestType.UNKNOWN,
"collectionRequests", "go",
@ -305,7 +298,7 @@ public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
"params", new MapSolrParams(singletonMap("key", "Val1")))
, FORBIDDEN);
checkRules(makeMap("resource", "/x",
checkRules(Map.of("resource", "/x",
"userPrincipal", "joe",
"requestType", RequestType.UNKNOWN,
"collectionRequests", "go",
@ -313,26 +306,24 @@ public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
"params", new MapSolrParams(singletonMap("key", "Val2")))
, STATUS_OK);
checkRules(makeMap("resource", "/x",
checkRules(Map.of("resource", "/x",
"userPrincipal", "joe",
"requestType", RequestType.UNKNOWN,
"collectionRequests", "go",
"handler", new DumpRequestHandler(),
"params", new MapSolrParams(singletonMap("key", "VAL2")))
, FORBIDDEN);
}
@Test
public void testCustomRules() {
Map<String, Object> customRules = (Map<String, Object>) 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",
checkRules(Map.of("resource", "/update",
"userPrincipal", "solr",
"requestType", RequestType.UNKNOWN,
"collectionRequests", "go",
@ -353,7 +344,7 @@ public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
setUserRole("dev", "dev");
setUserRole("admin", "admin");
addPermission("all", "dev", "admin");
checkRules(makeMap("resource", "/update",
checkRules(Map.of("resource", "/update",
"userPrincipal", "dev",
"requestType", RequestType.UNKNOWN,
"collectionRequests", "go",
@ -363,7 +354,7 @@ public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
handler = new PropertiesRequestHandler();
assertThat(handler, new IsNot<>(new IsInstanceOf(PermissionNameProvider.class)));
checkRules(makeMap("resource", "/admin/info/properties",
checkRules(Map.of("resource", "/admin/info/properties",
"userPrincipal", "dev",
"requestType", RequestType.UNKNOWN,
"collectionRequests", "go",
@ -385,7 +376,7 @@ public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
setUserRole("dev", "dev");
setUserRole("admin", "admin");
addPermission("all", "*");
checkRules(makeMap("resource", "/update",
checkRules(Map.of("resource", "/update",
"userPrincipal", "dev",
"requestType", RequestType.UNKNOWN,
"collectionRequests", "go",
@ -395,7 +386,7 @@ public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
handler = new PropertiesRequestHandler();
assertThat(handler, new IsNot<>(new IsInstanceOf(PermissionNameProvider.class)));
checkRules(makeMap("resource", "/admin/info/properties",
checkRules(Map.of("resource", "/admin/info/properties",
"userPrincipal", "dev",
"requestType", RequestType.UNKNOWN,
"collectionRequests", "go",
@ -416,7 +407,7 @@ public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
setUserRole("dev", "dev");
setUserRole("admin", "admin");
addPermission("all", "admin");
checkRules(makeMap("resource", "/update",
checkRules(Map.of("resource", "/update",
"userPrincipal", "dev",
"requestType", RequestType.UNKNOWN,
"collectionRequests", "go",
@ -426,7 +417,7 @@ public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
handler = new PropertiesRequestHandler();
assertThat(handler, new IsNot<>(new IsInstanceOf(PermissionNameProvider.class)));
checkRules(makeMap("resource", "/admin/info/properties",
checkRules(Map.of("resource", "/admin/info/properties",
"userPrincipal", "dev",
"requestType", RequestType.UNKNOWN,
"collectionRequests", "go",
@ -435,22 +426,36 @@ public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
, FORBIDDEN);
}
void addPermission(String permissionName, String role, String path, Map<String, Object> params) {
((List)rules.get("permissions")).add( makeMap("name", permissionName, "role", role, "path", path, "params", params));
@Test
public void testShortNameResolvesPermissions() {
assumeThat("ExternalRBAPlugin doesn't use short name",
createPlugin(), is(instanceOf(RuleBasedAuthorizationPlugin.class)));
setUserRole("admin", "admin");
addPermission("all", "admin");
Map<String, Object> values = Map.of(
"userPrincipal", "admin@EXAMPLE",
"userName", "admin",
"resource", "/admin/info/properties",
"requestType", RequestType.ADMIN,
"handler", new PropertiesRequestHandler());
// Short names disabled, admin should fail, admin@EXAMPLE should succeed
rules.put("useShortName", "false");
checkRules(values, FORBIDDEN);
// Short names enabled, admin should succeed, admin@EXAMPLE should fail
rules.put("useShortName", "true");
checkRules(values, STATUS_OK);
}
void removePermission(String name) {
List<Map<String,Object>> oldPerm = ((List) rules.get("permissions"));
List<Map<String, Object>> newPerm = oldPerm.stream().filter(p -> !p.get("name").equals(name)).collect(Collectors.toList());
rules.put("permissions", newPerm);
void addPermission(String permissionName, String role, String path, Map<String, Object> params) {
((List)rules.get("permissions")).add(Map.of("name", permissionName, "role", role, "path", path, "params", params));
}
protected void addPermission(String permissionName, String... roles) {
((List)rules.get("permissions")).add( makeMap("name", permissionName, "role", Arrays.asList(roles)));
}
void clearUserRoles() {
rules.put("user-role", new HashMap<String,Object>());
((List)rules.get("permissions")).add(Map.of("name", permissionName, "role", Arrays.asList(roles)));
}
protected void setUserRole(String user, String role) {
@ -465,8 +470,7 @@ public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
assertEquals("admin", perms.getVal("permissions[0]/role"));
perms.runCmd("{set-permission : {name: config-edit, role: [admin, dev], index:2 } }", false);
perms.runCmd("{set-permission : {name: config-edit, role: [admin, dev], index:1}}", true);
@SuppressWarnings({"rawtypes"})
Collection roles = (Collection) perms.getVal("permissions[0]/role");
Collection<String> roles = (Collection<String>) perms.getVal("permissions[0]/role");
assertEquals(2, roles.size());
assertTrue(roles.contains("admin"));
assertTrue(roles.contains("dev"));
@ -486,15 +490,12 @@ public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
}
static class Perms {
@SuppressWarnings({"rawtypes"})
Map conf = new HashMap<>();
Map<String, Object> conf = new HashMap<>();
ConfigEditablePlugin plugin = new RuleBasedAuthorizationPlugin();
List<CommandOperation> parsedCommands;
public void runCmd(String cmds, boolean failOnError) throws IOException {
parsedCommands = CommandOperation.parse(new StringReader(cmds));
@SuppressWarnings({"rawtypes"})
LinkedList ll = new LinkedList();
Map<String, Object> edited = plugin.edit(conf, parsedCommands);
if(edited!= null) conf = edited;
@SuppressWarnings({"rawtypes"})
@ -536,10 +537,15 @@ public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
Object userPrincipal = values.get("userPrincipal");
return userPrincipal == null ? null : new BasicUserPrincipal(String.valueOf(userPrincipal));
}
@Override
public String getUserName() {
return String.valueOf(values.get("userName"));
}
};
}
protected abstract class MockAuthorizationContext extends AuthorizationContext {
protected abstract static class MockAuthorizationContext extends AuthorizationContext {
private final Map<String,Object> values;
public MockAuthorizationContext(Map<String, Object> values) {
@ -552,14 +558,19 @@ public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
return params == null ? new MapSolrParams(new HashMap<>()) : params;
}
@Override
public String getUserName() {
Principal userPrincipal = getUserPrincipal();
return userPrincipal == null ? null : userPrincipal.getName();
}
@Override
public String getHttpHeader(String header) {
return null;
}
@Override
@SuppressWarnings({"rawtypes"})
public Enumeration getHeaderNames() {
public Enumeration<String> getHeaderNames() {
return null;
}

View File

@ -0,0 +1,93 @@
/*
* 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.hadoop;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.solr.cloud.KerberosTestServices;
/**
* A utility class which provides common functionality required to test kerberos integration.
*/
public class KerberosUtils {
/**
* This method sets up Hadoop mini-kdc along with relevant Kerberos configuration files
* (e.g. jaas.conf) as well as system properties.
*
* @param baseDir The directory path which should be used by the Hadoop mini-kdc
* @return An instance of {@link KerberosTestServices}
* @throws Exception in case of errors.
*/
static KerberosTestServices setupMiniKdc(Path baseDir) throws Exception {
System.setProperty("solr.jaas.debug", "true");
Path kdcDir = baseDir.resolve("minikdc");
String solrClientPrincipal = "solr";
File keytabFile = kdcDir.resolve("keytabs").toFile();
KerberosTestServices tmp = KerberosTestServices.builder()
.withKdc(kdcDir.toFile())
.withJaasConfiguration(solrClientPrincipal, keytabFile, "SolrClient")
.build();
String solrServerPrincipal = "HTTP/127.0.0.1";
tmp.start();
tmp.getKdc().createPrincipal(keytabFile, solrServerPrincipal, solrClientPrincipal);
String appName = "SolrClient";
String jaas = appName + " {\n"
+ " com.sun.security.auth.module.Krb5LoginModule required\n"
+ " useKeyTab=true\n"
+ " keyTab=\"" + keytabFile.getAbsolutePath() + "\"\n"
+ " storeKey=true\n"
+ " useTicketCache=false\n"
+ " doNotPrompt=true\n"
+ " debug=true\n"
+ " principal=\"" + solrClientPrincipal + "\";\n"
+ "};";
Path jaasFile = kdcDir.resolve("jaas-client.conf");
Files.writeString(jaasFile, jaas);
System.setProperty("java.security.auth.login.config", jaasFile.toString());
System.setProperty("solr.kerberos.jaas.appname", appName);
System.setProperty("solr.kerberos.principal", solrServerPrincipal);
System.setProperty("solr.kerberos.keytab", keytabFile.getAbsolutePath());
// Extracts 127.0.0.1 from HTTP/127.0.0.1@EXAMPLE.COM
System.setProperty("solr.kerberos.name.rules", "RULE:[1:$1@$0](.*EXAMPLE.COM)s/@.*//"
+ "\nRULE:[2:$2@$0](.*EXAMPLE.COM)s/@.*//"
+ "\nDEFAULT"
);
return tmp;
}
/**
* This method stops the Hadoop mini-kdc instance as well as cleanup relevant Java system properties.
*
* @param kerberosTestServices An instance of Hadoop mini-kdc
*/
public static void cleanupMiniKdc(KerberosTestServices kerberosTestServices) {
System.clearProperty("java.security.auth.login.config");
System.clearProperty("solr.kerberos.principal");
System.clearProperty("solr.kerberos.keytab");
System.clearProperty("solr.kerberos.name.rules");
System.clearProperty("solr.jaas.debug");
if (kerberosTestServices != null) {
kerberosTestServices.stop();
}
}
}

View File

@ -0,0 +1,81 @@
/*
* 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.hadoop;
import org.apache.lucene.util.Constants;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.cloud.AbstractDistribZkTestBase;
import org.apache.solr.cloud.KerberosTestServices;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.apache.solr.common.SolrInputDocument;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
public class TestRuleBasedAuthorizationWithKerberos extends SolrCloudTestCase {
protected static final int NUM_SERVERS = 1;
protected static final int NUM_SHARDS = 1;
protected static final int REPLICATION_FACTOR = 1;
private static KerberosTestServices kerberosTestServices;
@BeforeClass
public static void setupClass() throws Exception {
assumeFalse("Hadoop does not work on Windows", Constants.WINDOWS);
kerberosTestServices = KerberosUtils.setupMiniKdc(createTempDir());
configureCluster(NUM_SERVERS)// nodes
.withSecurityJson(TEST_PATH().resolve("security").resolve("hadoop_kerberos_authz_config.json"))
.addConfig("conf1", TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf"))
.configure();
}
@AfterClass
public static void tearDownClass() {
KerberosUtils.cleanupMiniKdc(kerberosTestServices);
kerberosTestServices = null;
}
@Test
public void testCollectionCreateSearchDelete() throws Exception {
CloudSolrClient solrClient = cluster.getSolrClient();
String collectionName = "testkerberoscollection_authz";
// create collection
CollectionAdminRequest.Create create = CollectionAdminRequest.createCollection(collectionName, "conf1",
NUM_SHARDS, REPLICATION_FACTOR);
create.process(solrClient);
SolrInputDocument doc = new SolrInputDocument();
doc.setField("id", "1");
solrClient.add(collectionName, doc);
solrClient.commit(collectionName);
SolrQuery query = new SolrQuery();
query.setQuery("*:*");
QueryResponse rsp = solrClient.query(collectionName, query);
assertEquals(1, rsp.getResults().getNumFound());
CollectionAdminRequest.Delete deleteReq = CollectionAdminRequest.deleteCollection(collectionName);
deleteReq.process(solrClient);
AbstractDistribZkTestBase.waitForCollectionToDisappear(collectionName,
solrClient.getZkStateReader(), true, 330);
}
}

View File

@ -16,10 +16,6 @@
*/
package org.apache.solr.security.hadoop;
import java.io.File;
import java.nio.charset.StandardCharsets;
import org.apache.commons.io.FileUtils;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
@ -43,7 +39,7 @@ public class TestSolrCloudWithHadoopAuthPlugin extends SolrCloudAuthTestCase {
public static void setupClass() throws Exception {
HdfsTestUtil.checkAssumptions();
setupMiniKdc();
kerberosTestServices = KerberosUtils.setupMiniKdc(createTempDir());
configureCluster(NUM_SERVERS)// nodes
.withSecurityJson(TEST_PATH().resolve("security").resolve("hadoop_kerberos_config.json"))
@ -54,55 +50,10 @@ public class TestSolrCloudWithHadoopAuthPlugin extends SolrCloudAuthTestCase {
@AfterClass
public static void tearDownClass() throws Exception {
System.clearProperty("java.security.auth.login.config");
System.clearProperty("solr.kerberos.principal");
System.clearProperty("solr.kerberos.keytab");
System.clearProperty("solr.kerberos.name.rules");
System.clearProperty("solr.jaas.debug");
if (kerberosTestServices != null) {
kerberosTestServices.stop();
}
KerberosUtils.cleanupMiniKdc(kerberosTestServices);
kerberosTestServices = null;
}
private static void setupMiniKdc() throws Exception {
System.setProperty("solr.jaas.debug", "true");
String kdcDir = createTempDir()+File.separator+"minikdc";
String solrClientPrincipal = "solr";
File keytabFile = new File(kdcDir, "keytabs");
kerberosTestServices = KerberosTestServices.builder()
.withKdc(new File(kdcDir))
.withJaasConfiguration(solrClientPrincipal, keytabFile, "SolrClient")
.build();
String solrServerPrincipal = "HTTP/127.0.0.1";
kerberosTestServices.start();
kerberosTestServices.getKdc().createPrincipal(keytabFile, solrServerPrincipal, solrClientPrincipal);
String jaas = "SolrClient {\n"
+ " com.sun.security.auth.module.Krb5LoginModule required\n"
+ " useKeyTab=true\n"
+ " keyTab=\"" + keytabFile.getAbsolutePath() + "\"\n"
+ " storeKey=true\n"
+ " useTicketCache=false\n"
+ " doNotPrompt=true\n"
+ " debug=true\n"
+ " principal=\"" + solrClientPrincipal + "\";\n"
+ "};";
String jaasFilePath = kdcDir+File.separator+"jaas-client.conf";
FileUtils.write(new File(jaasFilePath), jaas, StandardCharsets.UTF_8);
System.setProperty("java.security.auth.login.config", jaasFilePath);
System.setProperty("solr.kerberos.jaas.appname", "SolrClient"); // Get this app name from the jaas file
System.setProperty("solr.kerberos.principal", solrServerPrincipal);
System.setProperty("solr.kerberos.keytab", keytabFile.getAbsolutePath());
// Extracts 127.0.0.1 from HTTP/127.0.0.1@EXAMPLE.COM
System.setProperty("solr.kerberos.name.rules", "RULE:[1:$1@$0](.*EXAMPLE.COM)s/@.*//"
+ "\nRULE:[2:$2@$0](.*EXAMPLE.COM)s/@.*//"
+ "\nDEFAULT"
);
}
@Test
public void testBasics() throws Exception {
testCollectionCreateSearchDelete();

View File

@ -74,7 +74,6 @@ The syntax for individual permissions is more involved and is treated in greater
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:
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": {
@ -82,6 +81,11 @@ user-role:: A mapping of individual users to the roles they belong to. The valu
"user2": ["role1", "role2"]
}
----
useShortName:: Determines if user-role mappings will resolve using the full principal or a shortened name provided by the authentication plugin. For example, the `KerberosAuthPlugin` may provide a full principal as `user@EXAMPLE.COM`, while the corresponding short name would be `user`.
+
For some plugins the principal name and short name may be the same.
+
This setting is optional and defaults to `false` if unset.
=== Example for RuleBasedAuthorizationPlugin and BasicAuth