/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.impl;
import static org.geoserver.security.impl.DataAccessRule.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Logger;
import org.geoserver.catalog.Catalog;
import org.geoserver.config.GeoServerDataDirectory;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.resource.Resource;
import org.geoserver.security.AccessMode;
import org.geoserver.security.CatalogMode;
import org.geotools.util.logging.Logging;
/**
* Allows one to manage the rules used by the per layer security subsystem
* TODO: consider splitting the persistence of properties into two strategies,
* and in memory one, and a file system one (this class is so marginal that
* I did not do so right away, in memory access is mostly handy for testing)
*/
public class DataAccessRuleDAO extends AbstractAccessRuleDAO<DataAccessRule> {
private final static Logger LOGGER = Logging.getLogger(DataAccessRuleDAO.class);
/**
* property file name
*/
static final String LAYERS = "layers.properties";
/**
* The catalog
*/
Catalog rawCatalog;
/**
* Default to the highest security mode
*/
CatalogMode catalogMode = CatalogMode.HIDE;
/**
* Returns the instanced contained in the Spring context for the UI to use
*
*/
public static DataAccessRuleDAO get() {
return GeoServerExtensions.bean(DataAccessRuleDAO.class);
}
/**
* Builds a new dao
*
* @param rawCatalog
*/
public DataAccessRuleDAO(GeoServerDataDirectory dd, Catalog rawCatalog) throws IOException {
super(dd, LAYERS);
this.rawCatalog = rawCatalog;
}
/**
* Builds a new dao with a custom security dir. Used mostly for testing purposes
*
* @param rawCatalog
*/
DataAccessRuleDAO(Catalog rawCatalog, Resource securityDir) {
super(securityDir, LAYERS);
this.rawCatalog = rawCatalog;
}
/**
* The way the catalog should react to unauthorized access
*
*
*/
public CatalogMode getMode() {
checkPropertyFile(false);
return catalogMode;
}
/**
* Parses the rules contained in the property file
*
* @param props
*
*/
protected void loadRules(Properties props) {
TreeSet<DataAccessRule> result = new TreeSet<DataAccessRule>();
catalogMode = CatalogMode.HIDE;
for (Map.Entry<Object,Object> entry : props.entrySet()) {
String ruleKey = (String) entry.getKey();
String ruleValue = (String) entry.getValue();
// check for the mode
if ("mode".equalsIgnoreCase(ruleKey)) {
try {
catalogMode = CatalogMode.valueOf(ruleValue.toUpperCase());
} catch (Exception e) {
LOGGER.warning("Invalid security mode " + ruleValue + " acceptable values are "
+ Arrays.asList(CatalogMode.values()));
}
} else {
DataAccessRule rule = parseDataAccessRule(ruleKey, ruleValue);
if (rule != null) {
if (result.contains(rule))
LOGGER.warning("Rule " + ruleKey + "." + ruleValue
+ " overwrites another rule on the same path");
result.add(rule);
}
}
}
// make sure the two basic rules if the set is empty
if(result.size() == 0) {
result.add(new DataAccessRule(DataAccessRule.READ_ALL));
result.add(new DataAccessRule(DataAccessRule.WRITE_ALL));
}
rules = result;
}
/**
* Parses a single layer.properties line into a {@link DataAccessRule}, returns false if the
* rule is not valid
*
*
*/
DataAccessRule parseDataAccessRule(String ruleKey, String ruleValue) {
final String rule = ruleKey + "=" + ruleValue;
// parse
String[] elements = parseElements(ruleKey);
// perform basic checks on the elements
if(elements.length != 3 && elements.length != 2) {
LOGGER.warning("Invalid rule " + rule + ", the expected format is workspace.layer.mode=role1,role2,... or globalGroup.mode=role1,role2,...");
return null;
}
String root = elements[0];
String layerName, modeAlias;
if(elements.length == 3) {
layerName = elements[1];
modeAlias = elements[2];
} else {
layerName = null;
modeAlias = elements[1];
}
Set<String> roles = parseRoles(ruleValue);
// emit warnings for unknown workspaces, layers, but don't skip the rule,
// people might be editing the catalog structure and will edit the access rule
// file afterwards
if(layerName != null) {
if (!ANY.equals(root) && rawCatalog.getWorkspaceByName(root) == null)
LOGGER.warning("Namespace/Workspace " + root + " is unknown in rule " + rule);
if (!ANY.equals(layerName) && rawCatalog.getLayerByName(layerName) == null)
LOGGER.warning("Layer " + root + " is unknown in rule + " + rule);
} else {
if (!ANY.equals(root) && rawCatalog.getLayerGroupByName(root) == null)
LOGGER.warning("Global layer group " + root + " is unknown in rule " + rule);
}
// check the access mode sanity
AccessMode mode = AccessMode.getByAlias(modeAlias);
if (mode == null) {
LOGGER.warning("Unknown access mode " + modeAlias + " in " + ruleKey
+ ", skipping rule " + rule);
return null;
}
// check ANY usage sanity
if (ANY.equals(root)) {
if (!ANY.equals(layerName)) {
LOGGER.warning("Invalid rule " + rule + ", when namespace "
+ "is * then also layer must be *. Skipping rule " + rule);
return null;
}
}
// check admin access only applied globally to workspace
if (mode == AccessMode.ADMIN && !ANY.equals(layerName)) {
//TODO: should this throw an exception instead of ignore rule?
LOGGER.warning("Invalid rule " + rule + ", admin (a) privileges may only be applied " +
"globally to a workspace, layer must be *, skipping rule");
return null;
}
// build the rule
return new DataAccessRule(root, layerName, mode, roles);
}
/**
* Turns the rules list into a property bag
*
*/
protected Properties toProperties() {
Properties props = new Properties();
props.put("mode", catalogMode.toString());
for (DataAccessRule rule : rules) {
StringBuilder sbKey = new StringBuilder(rule.getRoot().replaceAll("\\.", "\\\\."));
if(!rule.isGlobalGroupRule()) {
sbKey.append(".").append(rule.getLayer().replaceAll("\\.", "\\\\."));
}
sbKey.append(".").append(rule.getAccessMode().getAlias());
props.put(sbKey.toString(), rule.getValue());
}
return props;
}
/**
* Parses workspace.layer.mode into an array of strings
*
* @param path
*
*/
static String[] parseElements(String path) {
String[] rawParse = path.trim().split("\\s*\\.\\s*");
List<String> result = new ArrayList<String>();
String prefix = null;
for (String raw : rawParse) {
if(prefix != null)
raw = prefix + "." + raw;
// just assume the escape is invalid char besides \. and check it once only
if (raw.endsWith("\\")) {
prefix = raw.substring(0, raw.length() - 1);
} else {
result.add(raw);
prefix = null;
}
}
return (String[]) result.toArray(new String[result.size()]);
}
public void setCatalogMode(CatalogMode catalogMode) {
this.catalogMode = catalogMode;
}
public static CatalogMode getByAlias(String alias){
for(CatalogMode mode: CatalogMode.values()){
if(mode.name().equals(alias)){
return mode;
}
}
return null;
}
/**
* Returns a sorted set of rules associated to the role
*
* @param role
*
*/
public SortedSet<DataAccessRule> getRulesAssociatedWithRole(String role) {
SortedSet<DataAccessRule> result = new TreeSet<DataAccessRule>();
for (DataAccessRule rule: getRules())
if (rule.getRoles().contains(role))
result.add(rule);
return result;
}
}