/* * 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.api.ApiBag; import org.apache.solr.api.SpecProvider; 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 { 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 (context.getRequestType() == AuthorizationContext.RequestType.ADMIN) { MatchStatus flag = checkCollPerm(mapping.get(null), context); return flag.rsp; } for (AuthorizationContext.CollectionRequest collreq : collectionRequests) { //check permissions for each collection MatchStatus flag = checkCollPerm(mapping.get(collreq.collectionName), context); if (flag != MatchStatus.NO_PERMISSIONS_FOUND) return flag.rsp; } //check wildcard (all=*) permissions. MatchStatus flag = checkCollPerm(mapping.get("*"), context); return flag.rsp; } private MatchStatus checkCollPerm(Map<String, List<Permission>> pathVsPerms, AuthorizationContext context) { if (pathVsPerms == null) return MatchStatus.NO_PERMISSIONS_FOUND; String path = context.getResource(); MatchStatus flag = checkPathPerm(pathVsPerms.get(path), context); if (flag != MatchStatus.NO_PERMISSIONS_FOUND) return flag; return checkPathPerm(pathVsPerms.get(null), context); } private MatchStatus checkPathPerm(List<Permission> permissions, AuthorizationContext context) { if (permissions == null || permissions.isEmpty()) return MatchStatus.NO_PERMISSIONS_FOUND; Principal principal = context.getUserPrincipal(); loopPermissions: for (int i = 0; i < permissions.size(); i++) { Permission permission = permissions.get(i); if (PermissionNameProvider.values.containsKey(permission.name)) { if (context.getHandler() instanceof PermissionNameProvider) { PermissionNameProvider handler = (PermissionNameProvider) context.getHandler(); PermissionNameProvider.Name permissionName = handler.getPermissionName(context); if (permissionName == null || !permission.name.equals(permissionName.name)) { continue; } } else { //all is special. it can match any if(permission.wellknownName != PermissionNameProvider.Name.ALL) continue; } } else { if (permission.method != null && !permission.method.contains(context.getHttpMethod())) { //this permissions HTTP method does not match this rule. try other rules continue; } if (permission.params != null) { for (Map.Entry<String, Function<String[], Boolean>> e : permission.params.entrySet()) { String[] paramVal = context.getParams().getParams(e.getKey()); if(!e.getValue().apply(paramVal)) continue loopPermissions; } } } if (permission.role == null) { //no role is assigned permission.That means everybody is allowed to access return MatchStatus.PERMITTED; } if (principal == null) { log.info("request has come without principal. failed permission {} ",permission); //this resource needs a principal but the request has come without //any credential. return MatchStatus.USER_REQUIRED; } else if (permission.role.contains("*")) { return MatchStatus.PERMITTED; } for (String role : permission.role) { Set<String> userRoles = usersVsRoles.get(principal.getName()); if (userRoles != null && userRoles.contains(role)) return MatchStatus.PERMITTED; } log.info("This resource is configured to have a permission {}, The principal {} does not have the right role ", permission, principal); return MatchStatus.FORBIDDEN; } log.debug("No permissions configured for the resource {} . So allowed to access", context.getResource()); return MatchStatus.NO_PERMISSIONS_FOUND; } @Override public void init(Map<String, Object> initInfo) { mapping.put(null, new WildCardSupportMap()); 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); } } } @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 ApiBag.getSpec("cluster.security.RuleBasedAuthorization").getSpec(); } }