mirror of https://github.com/apache/lucene.git
SOLR-12131: ExternalRoleRuleBasedAuthorizationPlugin (#341)
This commit is contained in:
parent
dd4fa8f2f8
commit
1e449e3d04
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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");
|
||||
|
|
|
@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue