/* (c) 2017 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.rest.security; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang.StringUtils; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.rest.ResourceNotFoundException; import org.geoserver.rest.RestException; import org.geoserver.rest.util.MediaTypeExtensions; import org.geoserver.security.GeoServerSecurityManager; import org.geoserver.security.impl.AbstractAccessRuleDAO; import org.geoserver.security.impl.DataAccessRule; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseBody; public abstract class AbstractAclController<DAO extends AbstractAccessRuleDAO<Comparable<?>>> { public static final String ANY = "*"; DAO ruleDAO; AbstractAclController(DAO ruleDAO) { this.ruleDAO = ruleDAO; } GeoServerSecurityManager getManager() { return GeoServerExtensions.bean(GeoServerSecurityManager.class); } @GetMapping(produces = { MediaType.APPLICATION_JSON_VALUE, MediaTypeExtensions.TEXT_JSON_VALUE, MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE }) @ResponseBody public RuleMap rulesGet() throws IOException { checkUserIsAdmin(); try { return getMap(); } catch (Exception e) { throw createRestException(e); } } @PostMapping(consumes = { MediaType.APPLICATION_JSON_VALUE, MediaTypeExtensions.TEXT_JSON_VALUE, MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE }) public void rulesPost(@RequestBody RuleMap map) throws IOException { checkUserIsAdmin(); try { postMap(map); } catch (Exception e) { throw createRestException(e); } } @PutMapping(consumes = { MediaType.APPLICATION_JSON_VALUE, MediaTypeExtensions.TEXT_JSON_VALUE, MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE }) public void rulesPut(@RequestBody RuleMap map) throws IOException { checkUserIsAdmin(); try { putMap(map); } catch (Exception e) { throw createRestException(e); } } @DeleteMapping(path = "/**") public void rulesDelete(HttpServletRequest request) throws UnsupportedEncodingException { checkUserIsAdmin(); String thePath = request.getPathInfo(); String ruleString = thePath.substring(getBasePath().length() + 1); ruleString = URLDecoder.decode(ruleString, "utf-8"); String msg = validateRuleKey(ruleString); if (msg != null) throw new RestException(msg, HttpStatus.UNPROCESSABLE_ENTITY); Comparable<?> rule = null; for (Comparable<?> ruleCandidate : ruleDAO.getRules()) { if (ruleString.equals(keyFor(ruleCandidate))) { rule = ruleCandidate; break; } } if (rule == null) { throw new ResourceNotFoundException("Rule not found: " + ruleString); } try { ruleDAO.removeRule(rule); ruleDAO.storeRules(); } catch (Exception e) { throw createRestException(e); } } /** * Returns the base path of the ACL resource * * @return */ protected abstract String getBasePath(); protected void checkUserIsAdmin() { if (!getManager().checkAuthenticationForAdminRole()) { throw new RestException("Amdinistrative priveleges required", HttpStatus.FORBIDDEN); } } /** * Adds a rule to a map * * @param rule * @param map */ protected abstract void addRuleToMap(Comparable rule, Map<String, String> map); public RuleMap<String, String> getMap() throws Exception { RuleMap<String, String> result = new RuleMap<>(); for (Comparable<?> rule : ruleDAO.getRules()) { addRuleToMap(rule, result); } return result; } /** * Calculate the the intersection of the keys * * @param map */ protected Set<Object> intersection(Map map) { Set<Object> result = new HashSet<>(); Set<Object> ruleKeys = new HashSet<>(); for (Comparable<?> rule : ruleDAO.getRules()) { ruleKeys.add(keyFor(rule)); } if (ruleKeys.isEmpty() || map.isEmpty()) return result; for (Object key : ruleKeys) { if (map.containsKey(key)) result.add(key); } return result; } /** * Calculate the keys not contained in the rule data access object * * @param map */ protected Set<Object> nonExistingKeys(Map map) { List<Comparable<?>> rules = ruleDAO.getRules(); if (rules.isEmpty()) return map.keySet(); Set<Object> result = new HashSet<>(); Set<Object> ruleKeys = new HashSet<>(); for (Comparable<?> rule : rules) { ruleKeys.add(keyFor(rule)); } for (Object key : map.keySet()) { if (!ruleKeys.contains(key)) result.add(key); } return result; } /** * Returns the key string for a rule * * @param rule * */ protected abstract String keyFor(Comparable<?> rule); /** * Validate a rule, return an error message or <code>null</code> if the rule is ok * * @param ruleKey ,ruleValue */ protected String validateRule(String ruleKey, String ruleValue) { return validateRuleKey(ruleKey); // TODO // roles are not validated at the moment } /** * Validates the string representation of a rule key. Return an error message or <code>null</code> if the rule is ok * * @param ruleKey * */ protected abstract String validateRuleKey(String ruleKey); /** * Convert an {@link Entry} to a rule object * * @param entry * */ protected abstract Comparable convertEntryToRule(Entry<String, String> entry); /** * Validates the string representation of rule keys and values * * @param ruleMap */ protected void validateMap(Map<String, String> ruleMap) { for (Entry<String, String> entry : ruleMap.entrySet()) { String msg = validateRule(entry.getKey(), entry.getValue()); if (msg != null) { throw new RestException(msg, HttpStatus.UNPROCESSABLE_ENTITY); } } } protected void postMap(Map map) throws Exception { validateMap(map); Set<Object> commonKeys = intersection(map); if (!commonKeys.isEmpty()) { String msg = "Already existing rules: " + StringUtils.join(commonKeys.iterator(), ","); throw new RestException(msg, HttpStatus.CONFLICT); } for (Object entry : map.entrySet()) { Comparable rule = convertEntryToRule((Entry<String, String>) entry); ruleDAO.addRule(rule); } ruleDAO.storeRules(); } protected void putMap(Map map) throws Exception { validateMap(map); Set<Object> nonExisting = nonExistingKeys(map); if (!nonExisting.isEmpty()) { String msg = "Unknown rules: " + StringUtils.join(nonExisting.iterator(), ","); throw new RestException(msg, HttpStatus.CONFLICT); } for (Object entry : map.entrySet()) { Comparable rule = convertEntryToRule((Entry<String, String>) entry); // TODO, will not work for REST ruleDAO.removeRule(rule); ruleDAO.addRule(rule); } ruleDAO.storeRules(); } /** * Parses a comma separated list of roles into a set of strings, with special handling for the {@link DataAccessRule#ANY} role * * @param roleCsv Comma separated list of roles. */ protected Set<String> parseRoles(String roleCsv) { // regexp: treat extra spaces as separators, ignore extra commas // "a,,b, ,, c" --> ["a","b","c"] String[] rolesArray = roleCsv.split("[\\s,]+"); Set<String> roles = new HashSet<>(rolesArray.length); roles.addAll(Arrays.asList(rolesArray)); // if any of the roles is * we just remove all of the others for (String role : roles) { if (ANY.equals(role)) return Collections.singleton("*"); } return roles; } protected RestException createRestException(Exception ex) { if (ex instanceof RestException) { return (RestException) ex; // do nothing } else { return new RestException("", HttpStatus.INTERNAL_SERVER_ERROR, ex); } } }