/* * 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.ambari.infra.security; import com.google.common.collect.ImmutableSet; import java.io.IOException; import java.lang.invoke.MethodHandles; import java.security.Principal; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import org.apache.solr.common.SolrException; import org.apache.solr.common.params.CollectionParams; import org.apache.solr.common.util.Utils; import org.apache.solr.security.AuthorizationContext; import org.apache.solr.security.AuthorizationPlugin; import org.apache.solr.security.AuthorizationResponse; import org.apache.solr.security.ConfigEditablePlugin; import org.apache.solr.util.CommandOperation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static java.util.Collections.singleton; import static org.apache.solr.common.params.CommonParams.NAME; import static org.apache.solr.common.util.Utils.getDeepCopy; import static org.apache.solr.handler.admin.SecurityConfHandler.getListValue; import static org.apache.solr.handler.admin.SecurityConfHandler.getMapValue; /** * Modified copy of solr.RuleBasedAuthorizationPlugin to handle role - permission mappings with KereberosPlugin * Added 2 new JSON map: (precedence: user-host-regex > user-host) * 1. "user-host": user host mappings (array) for hostname validation * 2. "user-host-regex": user host regex mapping (string) for hostname validation */ public class InfraRuleBasedAuthorizationPlugin implements AuthorizationPlugin, ConfigEditablePlugin { 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 final Map<String, Set<String>> userVsHosts = new HashMap<>(); private final Map<String, String> userVsHostRegex = new HashMap<>(); private final InfraUserRolesLookupStrategy infraUserRolesLookupStrategy = new InfraUserRolesLookupStrategy(); private final InfraKerberosHostValidator infraKerberosDomainValidator = new InfraKerberosHostValidator(); 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<Permission>() : new ArrayList<Permission>(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 (permission.method != null && !permission.method.contains(context.getHttpMethod())) { //this permissions HTTP method does not match this rule. try other rules continue; } if(permission.predicate != null){ if(!permission.predicate.test(context)) continue ; } if (permission.params != null) { for (Map.Entry<String, Object> e : permission.params.entrySet()) { String paramVal = context.getParams().get(e.getKey()); Object val = e.getValue(); if (val instanceof List) { if (!((List) val).contains(paramVal)) continue loopPermissions; } else if (!Objects.equals(val, 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 = infraUserRolesLookupStrategy.getUserRolesFromPrincipal(usersVsRoles, principal); boolean validHostname = infraKerberosDomainValidator.validate(principal, userVsHosts, userVsHostRegex); if (!validHostname) { log.warn("Hostname is not valid for principal {}", principal); return MatchStatus.FORBIDDEN; } 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, 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); } // adding user-host Map<String, Object> userHostsMap = getMapValue(initInfo, "user-host"); for (Object userHost : userHostsMap.entrySet()) { Map.Entry e = (Map.Entry) userHost; String roleName = (String) e.getKey(); userVsHosts.put(roleName, readValueAsSet(userHostsMap, roleName)); } // adding user-host-regex Map<String, Object> userHostRegexMap = getMapValue(initInfo, "user-host-regex"); for (Map.Entry<String, Object> entry : userHostRegexMap.entrySet()) { userVsHostRegex.put(entry.getKey(), entry.getValue().toString()); } } //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); } } } /** * read a key value as a set. if the value is a single string , * return a singleton set * * @param m the map from which to lookup * @param key the key with which to do lookup */ static Set<String> readValueAsSet(Map m, String key) { Set<String> result = new HashSet<>(); Object val = m.get(key); if (val == null) { if("collection".equals(key)){ //for collection collection: null means a core admin/ collection admin request // otherwise it means a request where collection name is ignored return m.containsKey(key) ? singleton((String) null) : singleton("*"); } return null; } if (val instanceof Collection) { Collection list = (Collection) val; for (Object o : list) result.add(String.valueOf(o)); } else if (val instanceof String) { result.add((String) val); } else { throw new RuntimeException("Bad value for : " + key); } return result.isEmpty() ? null : Collections.unmodifiableSet(result); } @Override public void close() throws IOException { } static class Permission { String name; Set<String> path, role, collections, method; Map<String, Object> params; Predicate<AuthorizationContext> predicate; Map originalConfig; private Permission() { } static Permission load(Map m) { Permission p = new Permission(); p.originalConfig = new LinkedHashMap<>(m); String name = (String) m.get(NAME); if (!m.containsKey("role")) throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "role not specified"); p.role = readValueAsSet(m, "role"); if (well_known_permissions.containsKey(name)) { HashSet<String> disAllowed = new HashSet<>(knownKeys); disAllowed.remove("role");//these are the only disAllowed.remove(NAME);//allowed keys for well-known permissions for (String s : disAllowed) { if (m.containsKey(s)) throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, s + " is not a valid key for the permission : " + name); } p.predicate = (Predicate<AuthorizationContext>) ((Map) well_known_permissions.get(name)).get(Predicate.class.getName()); m = well_known_permissions.get(name); } p.name = name; p.path = readSetSmart(name, m, "path"); p.collections = readSetSmart(name, m, "collection"); p.method = readSetSmart(name, m, "method"); p.params = (Map<String, Object>) m.get("params"); return p; } @Override public String toString() { return Utils.toJSONString(originalConfig); } static final Set<String> knownKeys = ImmutableSet.of("collection", "role", "params", "path", "method", NAME); } 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; } } /** * This checks for the defaults available other rules for the keys */ private static Set<String> readSetSmart(String permissionName, Map m, String key) { Set<String> set = readValueAsSet(m, key); if (set == null && well_known_permissions.containsKey(permissionName)) { set = readValueAsSet((Map) well_known_permissions.get(permissionName), key); } if ("method".equals(key)) { if (set != null) { for (String s : set) if (!HTTP_METHODS.contains(s)) return null; } return set; } return set == null ? singleton((String)null) : set; } @Override public Map<String, Object> edit(Map<String, Object> latestConf, List<CommandOperation> commands) { for (CommandOperation op : commands) { OPERATION operation = null; for (OPERATION o : OPERATION.values()) { if (o.name.equals(op.name)) { operation = o; break; } } if (operation == null) { op.unknownOperation(); return null; } latestConf = operation.edit(latestConf, op); if (latestConf == null) return null; } return latestConf; } enum OPERATION { SET_USER_ROLE("set-user-role") { @Override public Map<String, Object> edit(Map<String, Object> latestConf, CommandOperation op) { Map<String, Object> roleMap = getMapValue(latestConf, "user-role"); Map<String, Object> map = op.getDataMap(); if (op.hasError()) return null; for (Map.Entry<String, Object> e : map.entrySet()) { if (e.getValue() == null) { roleMap.remove(e.getKey()); continue; } if (e.getValue() instanceof String || e.getValue() instanceof List) { roleMap.put(e.getKey(), e.getValue()); } else { op.addError("Unexpected value "); return null; } } return latestConf; } }, SET_PERMISSION("set-permission") { @Override public Map<String, Object> edit(Map<String, Object> latestConf, CommandOperation op) { String name = op.getStr(NAME); Map<String, Object> dataMap = op.getDataMap(); if (op.hasError()) return null; dataMap = getDeepCopy(dataMap, 3); String before = (String) dataMap.remove("before"); for (String key : dataMap.keySet()) { if (!Permission.knownKeys.contains(key)) op.addError("Unknown key, " + key); } try { Permission.load(dataMap); } catch (Exception e) { op.addError(e.getMessage()); return null; } List<Map> permissions = getListValue(latestConf, "permissions"); List<Map> permissionsCopy = new ArrayList<>(); boolean added = false; for (Map e : permissions) { Object n = e.get(NAME); if (n.equals(before) || n.equals(name)) { added = true; permissionsCopy.add(dataMap); } if (!n.equals(name)) permissionsCopy.add(e); } if (!added && before != null) { op.addError("Invalid 'before' :" + before); return null; } if (!added) permissionsCopy.add(dataMap); latestConf.put("permissions", permissionsCopy); return latestConf; } }, UPDATE_PERMISSION("update-permission") { @Override public Map<String, Object> edit(Map<String, Object> latestConf, CommandOperation op) { String name = op.getStr(NAME); if (op.hasError()) return null; for (Map permission : (List<Map>) getListValue(latestConf, "permissions")) { if (name.equals(permission.get(NAME))) { LinkedHashMap copy = new LinkedHashMap<>(permission); copy.putAll(op.getDataMap()); op.setCommandData(copy); return SET_PERMISSION.edit(latestConf, op); } } op.addError("No such permission " + name); return null; } }, DELETE_PERMISSION("delete-permission") { @Override public Map<String, Object> edit(Map<String, Object> latestConf, CommandOperation op) { List<String> names = op.getStrs(""); if (names == null || names.isEmpty()) { op.addError("Invalid command"); return null; } names = new ArrayList<>(names); List<Map> copy = new ArrayList<>(); List<Map> p = getListValue(latestConf, "permissions"); for (Map map : p) { Object n = map.get(NAME); if (names.contains(n)) { names.remove(n); continue; } else { copy.add(map); } } if (!names.isEmpty()) { op.addError("Unknown permission name(s) " + names); return null; } latestConf.put("permissions", copy); return latestConf; } }; public abstract Map<String, Object> edit(Map<String, Object> latestConf, CommandOperation op); public final String name; OPERATION(String s) { this.name = s; } public static OPERATION get(String name) { for (OPERATION o : values()) if (o.name.equals(name)) return o; return null; } } public static final Set<String> HTTP_METHODS = ImmutableSet.of("GET", "POST", "DELETE", "PUT", "HEAD"); private static final Map<String, Map<String,Object>> well_known_permissions = (Map) Utils.fromJSONString( " { " + " security-edit :{" + " path:['/admin/authentication','/admin/authorization']," + " collection:null," + " method:POST }," + " security-read :{" + " path:['/admin/authentication','/admin/authorization']," + " collection:null," + " method:GET}," + " schema-edit :{" + " method:POST," + " path:'/schema/*'}," + " collection-admin-edit :{" + " collection:null," + " path:'/admin/collections'}," + " collection-admin-read :{" + " collection:null," + " path:'/admin/collections'}," + " schema-read :{" + " method:GET," + " path:'/schema/*'}," + " config-read :{" + " method:GET," + " path:'/config/*'}," + " update :{" + " path:'/update/*'}," + " read :{" + " path:['/select', '/get','/browse','/tvrh','/terms','/clustering','/elevate', '/export','/spell','/clustering']}," + " config-edit:{" + " method:POST," + " path:'/config/*'}," + " all:{collection:['*', null]}" + "}"); static { ((Map) well_known_permissions.get("collection-admin-edit")).put(Predicate.class.getName(), getCollectionActionPredicate(true)); ((Map) well_known_permissions.get("collection-admin-read")).put(Predicate.class.getName(), getCollectionActionPredicate(false)); } private static Predicate<AuthorizationContext> getCollectionActionPredicate(final boolean isEdit) { return new Predicate<AuthorizationContext>() { @Override public boolean test(AuthorizationContext context) { String action = context.getParams().get("action"); if (action == null) return false; CollectionParams.CollectionAction collectionAction = CollectionParams.CollectionAction.get(action); if (collectionAction == null) return false; return isEdit ? collectionAction.isWrite : !collectionAction.isWrite; } }; } public static void main(String[] args) { System.out.println(Utils.toJSONString(well_known_permissions)); } public interface Predicate<T> { boolean test(T t); } }