SOLR-12131: ExternalRoleRuleBasedAuthorizationPlugin (#341)

This commit is contained in:
Jan Høydahl 2020-05-13 23:29:33 +02:00 committed by GitHub
parent dd4fa8f2f8
commit 1e449e3d04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 815 additions and 441 deletions

View File

@ -84,6 +84,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

View File

@ -41,7 +41,7 @@ import org.apache.solr.request.SolrQueryRequest;
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 class SystemInfoHandler extends RequestHandlerBase
// Mapped roles for this principal
AuthorizationPlugin auth = cc==null? null: cc.getAuthorizationPlugin();
if (auth != null) {
RuleBasedAuthorizationPlugin rbap = (RuleBasedAuthorizationPlugin) auth;
Set<String> roles = rbap.getRoles(username);
RuleBasedAuthorizationPluginBase rbap = (RuleBasedAuthorizationPluginBase) auth;
Set<String> roles = rbap.getUserRoles(req.getUserPrincipal());
info.add("roles", roles);
}
}

View File

@ -0,0 +1,57 @@
/*
* 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.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);
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 which should contain roles
* @return set of roles as strings
*/
@Override
public Set<String> getUserRoles(Principal principal) {
if(principal instanceof VerifiedUserRoles) {
return ((VerifiedUserRoles) principal).getVerifiedRoles();
} else {
return Collections.emptySet();
}
}
}

View File

@ -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<String> 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<String> algWhitelist;
private String principalClaim;
private String rolesClaim;
private HashMap<String, Pattern> claimsMatchCompiled;
private boolean blockUnknown;
private List<String> requiredScopes = new ArrayList<>();
@ -140,6 +142,8 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider,
PARAM_REQUIRE_SUBJECT);
}
principalClaim = (String) pluginConfig.getOrDefault(PARAM_PRINCIPAL_CLAIM, "sub");
rolesClaim = (String) pluginConfig.get(PARAM_ROLES_CLAIM);
algWhitelist = (List<String>) pluginConfig.get(PARAM_ALG_WHITELIST);
realm = (String) pluginConfig.getOrDefault(PARAM_REALM, DEFAULT_AUTH_REALM);
@ -403,6 +407,8 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider,
// 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<String> scopes = Collections.emptySet();
Object scopesObj = jwtClaims.getClaimValue(CLAIM_SCOPE);
if (scopesObj != null) {
@ -417,10 +423,27 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider,
return new JWTAuthenticationResponse(AuthCode.SCOPE_MISSING, "Claim " + CLAIM_SCOPE + " does not contain any of the required scopes: " + requiredScopes);
}
}
final Set<String> 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<String> 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()));
}

View File

@ -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;
@ -27,8 +26,7 @@ import org.apache.http.util.Args;
/**
* 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<String,Object> claims;

View File

@ -91,7 +91,7 @@ public class KerberosFilter extends AuthenticationFilter {
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() {

View File

@ -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;
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<String, Set<String>> usersVsRoles = new HashMap<>();
private final Map<String, WildCardSupportMap> mapping = new HashMap<>();
private final List<Permission> permissions = new ArrayList<>();
private static class WildCardSupportMap extends HashMap<String, List<Permission>> {
final Set<String> wildcardPrefixes = new HashSet<>();
@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);
}
@Override
public List<Permission> get(Object key) {
List<Permission> result = super.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);
}
}
}
}
return result;
}
}
@Override
public AuthorizationResponse authorize(AuthorizationContext context) {
List<AuthorizationContext.CollectionRequest> 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<String, List<Permission>> 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<Permission> 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<Permission> 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<Permission> 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<String, Function<String[], Boolean>> 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<String> 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<String> getRoles(String user) {
Set<String> roles = usersVsRoles.get(user);
return roles;
}
public boolean doesUserHavePermission(String user, PermissionNameProvider.Name permission) {
Set<String> roles = usersVsRoles.get(user);
if (roles != null) {
for (String role: roles) {
if (mapping.get(null) == null) continue;
List<Permission> 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<String, Object> initInfo) {
mapping.put(null, new WildCardSupportMap());
super.init(initInfo);
Map<String, Object> 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<Map> 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<Permission> perms = m.get(path);
if (perms == null) m.put(path, perms = new ArrayList<>());
perms.add(permission);
}
}
}
/**
* 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
*/
@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<String, Object> edit(Map<String, Object> latestConf, List<CommandOperation> 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<String, AutorizationEditOperation> ops = unmodifiableMap(asList(AutorizationEditOperation.values()).stream().collect(toMap(AutorizationEditOperation::getOperationName, identity())));
@Override
public ValidatingJsonMap getSpec() {
return Utils.getSpec("cluster.security.RuleBasedAuthorization").getSpec();
public Set<String> getUserRoles(Principal principal) {
return usersVsRoles.get(principal.getName());
}
}

View File

@ -0,0 +1,339 @@
/*
* 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.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;
/**
* 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());
private final Map<String, WildCardSupportMap> mapping = new HashMap<>();
private final List<Permission> permissions = new ArrayList<>();
private static class WildCardSupportMap extends HashMap<String, List<Permission>> {
final Set<String> wildcardPrefixes = new HashSet<>();
@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);
}
@Override
public List<Permission> get(Object key) {
List<Permission> result = super.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);
}
}
}
}
return result;
}
}
@Override
public AuthorizationResponse authorize(AuthorizationContext context) {
List<AuthorizationContext.CollectionRequest> 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<String, List<Permission>> 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<Permission> 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<Permission> 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<Permission> 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<String, Function<String[], Boolean>> 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<String> userRoles = getUserRoles(principal);
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(Principal principal, PermissionNameProvider.Name permission) {
Set<String> roles = getUserRoles(principal);
if (roles != null) {
for (String role: roles) {
if (mapping.get(null) == null) continue;
List<Permission> 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<String, Object> initInfo) {
mapping.put(null, new WildCardSupportMap());
List<Map> 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<Permission> perms = m.get(path);
if (perms == null) m.put(path, perms = new ArrayList<>());
perms.add(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<String> getUserRoles(Principal principal);
@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<String, Object> edit(Map<String, Object> latestConf, List<CommandOperation> 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<String, AutorizationEditOperation> ops = unmodifiableMap(asList(AutorizationEditOperation.values()).stream().collect(toMap(AutorizationEditOperation::getOperationName, identity())));
@Override
public ValidatingJsonMap getSpec() {
return Utils.getSpec("cluster.security.RuleBasedAuthorization").getSpec();
}
}

View File

@ -26,11 +26,13 @@ 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;
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;
@ -43,7 +45,6 @@ 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.common.util.CommandOperation;
import org.hamcrest.core.IsInstanceOf;
import org.hamcrest.core.IsNot;
import org.junit.Test;
@ -51,39 +52,54 @@ import org.junit.Test;
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;
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;
/**
* 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;
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:'*'}]}";
final int STATUS_OK = 200;
final int FORBIDDEN = 403;
final int PROMPT_FOR_CREDENTIALS = 401;
@Override
public void setUp() throws Exception {
super.setUp();
resetPermissionsAndRoles();
}
protected void resetPermissionsAndRoles() {
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:'*'}]}";
rules = (Map) Utils.fromJSONString(permissions);
}
@Test
public void testBasicPermissions() {
checkRules(makeMap("resource", "/update/json/docs",
"httpMethod", "POST",
@ -99,7 +115,6 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
"handler", new UpdateRequestHandler())
, STATUS_OK);
checkRules(makeMap("resource", "/update/json/docs",
"httpMethod", "POST",
"collectionRequests", "mycoll",
@ -117,8 +132,7 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
"userPrincipal", "somebody",
"collectionRequests", "mycoll",
"httpMethod", "GET",
"handler", new SchemaHandler()
)
"handler", new SchemaHandler())
, STATUS_OK);
checkRules(makeMap("resource", "/schema/fields",
@ -169,7 +183,6 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
"params", new MapSolrParams(singletonMap("action", "RELOAD")))
, PROMPT_FOR_CREDENTIALS);
checkRules(makeMap("resource", "/admin/collections",
"userPrincipal", "somebody",
"requestType", RequestType.ADMIN,
@ -193,24 +206,22 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
"userPrincipal", "joe")
, 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 +229,13 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
"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);
resetPermissionsAndRoles();
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 +253,7 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
"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 +267,10 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
"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);
resetPermissionsAndRoles();
addPermission("test-params", "admin", "/x", makeMap("key", Arrays.asList("REGEX:(?i)val1", "VAL2")));
checkRules(makeMap("resource", "/x",
"userPrincipal", null,
@ -289,6 +295,7 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
"handler", new DumpRequestHandler(),
"params", new MapSolrParams(singletonMap("key", "Val1")))
, PROMPT_FOR_CREDENTIALS);
checkRules(makeMap("resource", "/x",
"userPrincipal", "joe",
"requestType", RequestType.UNKNOWN,
@ -304,6 +311,7 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
"handler", new DumpRequestHandler(),
"params", new MapSolrParams(singletonMap("key", "Val2")))
, STATUS_OK);
checkRules(makeMap("resource", "/x",
"userPrincipal", "joe",
"requestType", RequestType.UNKNOWN,
@ -312,20 +320,24 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
"params", new MapSolrParams(singletonMap("key", "VAL2")))
, FORBIDDEN);
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",
"userPrincipal", "solr",
"requestType", RequestType.UNKNOWN,
"collectionRequests", "go",
"handler", new UpdateRequestHandler(),
"params", new MapSolrParams(singletonMap("key", "VAL2")))
, FORBIDDEN, (Map<String, Object>) 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);
}
/*
@ -337,19 +349,16 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
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<String, Object>) 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)));
@ -359,13 +368,7 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
"collectionRequests", "go",
"handler", handler,
"params", new MapSolrParams(emptyMap()))
, STATUS_OK, (Map<String, Object>) Utils.fromJSONString( "{" +
" user-role:{" +
" dev:[dev_role]," +
" admin:[admin_role]}," +
" permissions:[" +
" {name:all, role:[dev_role, admin_role]}" +
"]}"));
, STATUS_OK);
}
@ -378,19 +381,16 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
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<String, Object>) 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)));
@ -400,13 +400,7 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
"collectionRequests", "go",
"handler", handler,
"params", new MapSolrParams(emptyMap()))
, STATUS_OK, (Map<String, Object>) Utils.fromJSONString( "{" +
" user-role:{" +
" dev:[dev_role]," +
" admin:[admin_role]}," +
" permissions:[" +
" {name:all, role:'*'}" +
"]}"));
, STATUS_OK);
}
/*
@ -418,19 +412,16 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
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<String, Object>) 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)));
@ -440,13 +431,29 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
"collectionRequests", "go",
"handler", handler,
"params", new MapSolrParams(emptyMap()))
, FORBIDDEN, (Map<String, Object>) 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<String, Object> params) {
((List)rules.get("permissions")).add( makeMap("name", permissionName, "role", role, "path", path, "params", params));
}
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);
}
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>());
}
protected void setUserRole(String user, String role) {
((Map)rules.get("user-role")).put(user, role);
}
public void testEditRules() throws IOException {
@ -498,13 +505,13 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
}
}
private void checkRules(Map<String, Object> values, int expected) {
checkRules(values,expected,(Map) Utils.fromJSONString(permissions));
void checkRules(Map<String, Object> values, int expected) {
checkRules(values, expected, rules);
}
private void checkRules(Map<String, Object> values, int expected, Map<String ,Object> permissions) {
AuthorizationContext context = new MockAuthorizationContext(values);
try (RuleBasedAuthorizationPlugin plugin = new RuleBasedAuthorizationPlugin()) {
void checkRules(Map<String, Object> values, int expected, Map<String, Object> permissions) {
AuthorizationContext context = getMockContext(values);
try (RuleBasedAuthorizationPluginBase plugin = createPlugin()) {
plugin.init(permissions);
AuthorizationResponse authResp = plugin.authorize(context);
assertEquals(expected, authResp.statusCode);
@ -513,23 +520,31 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
}
}
private static class MockAuthorizationContext extends AuthorizationContext {
protected RuleBasedAuthorizationPluginBase createPlugin() {
return new RuleBasedAuthorizationPlugin();
}
AuthorizationContext getMockContext(Map<String, Object> 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<String,Object> values;
private MockAuthorizationContext(Map<String, Object> values) {
public MockAuthorizationContext(Map<String, Object> values) {
this.values = values;
}
@Override
public SolrParams getParams() {
SolrParams params = (SolrParams) values.get("params");
return params == null ? new MapSolrParams(new HashMap<String, String>()) : 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

View File

@ -113,8 +113,8 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
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<String> 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<String> 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 class JWTAuthPluginTest extends SolrTestCaseJ4 {
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<String> roles = ((VerifiedUserRoles)principal).getVerifiedRoles();
@ -332,6 +333,23 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
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<String> 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");

View File

@ -0,0 +1,91 @@
/*
* 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.Objects;
import java.util.Set;
/**
* 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 {
private final String username;
private final Set<String> 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<String> roles) {
super();
Objects.requireNonNull(username, "User name was null");
Objects.requireNonNull(roles, "User roles was null");
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<String> 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 +
'}';
}
}

View File

@ -0,0 +1,78 @@
/*
* 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;
/**
* Tests {@link ExternalRoleRuleBasedAuthorizationPlugin} through simulating principals with roles attached
*/
public class TestExternalRoleRuleBasedAuthorizationPlugin extends BaseTestRuleBasedAuthorizationPlugin {
private HashMap<String, Principal> principals;
@Override
public void setUp() throws Exception {
super.setUp();
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<String, Object> 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();
}
@Override
protected void resetPermissionsAndRoles() {
super.resetPermissionsAndRoles();
rules.remove("user-role");
}
}

View File

@ -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.

View File

@ -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 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 AuthenticationPlugin to provide a Principal that has the roles information as well, implementing the `VerifiedUserRoles` interface.
=== 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 <<authentication-and-authorization-plugins.adoc#enable-plugins-with-security-json,here>> for more information on how to setup `security.json` in your cluster.
@ -54,15 +57,6 @@ Solr offers an <<Authorization API>> 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 <<Permissions,below>>.
=== 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 <<basic-authentication-plugin.adoc#basic-authentication-plugin,Basic authentication plugin>> 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 <<jwt-authentication-plugin.adoc#jwt-authentication-plugin,JWT authentication plugin>>, 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.

View File

@ -53,6 +53,7 @@ Authorization makes sure that only users with the necessary roles/permissions ca
// tag::list-of-authorization-plugins[]
* <<rule-based-authorization-plugin.adoc#rule-based-authorization-plugin,Rule-Based Authorization Plugin>>
* <<rule-based-authorization-plugin.adoc#rule-based-authorization-plugin,External Role Rule-Based Authorization Plugin>>
// end::list-of-authorization-plugins[]
=== Audit Logging Plugins