/* (c) 2014 - 2016 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.ANY; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.CatalogInfo; import org.geoserver.catalog.CoverageInfo; import org.geoserver.catalog.FeatureTypeInfo; import org.geoserver.catalog.LayerGroupInfo; import org.geoserver.catalog.LayerGroupInfo.Mode; import org.geoserver.catalog.LayerInfo; import org.geoserver.catalog.Predicates; import org.geoserver.catalog.PublishedInfo; import org.geoserver.catalog.ResourceInfo; import org.geoserver.catalog.StyleInfo; import org.geoserver.catalog.WMSLayerInfo; import org.geoserver.catalog.WorkspaceInfo; import org.geoserver.ows.Dispatcher; import org.geoserver.ows.Request; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.security.AccessMode; import org.geoserver.security.AdminRequest; import org.geoserver.security.CatalogMode; import org.geoserver.security.CoverageAccessLimits; import org.geoserver.security.DataAccessLimits; import org.geoserver.security.DataAccessManager; import org.geoserver.security.InMemorySecurityFilter; import org.geoserver.security.LayerGroupAccessLimits; import org.geoserver.security.ResourceAccessManager; import org.geoserver.security.StyleAccessLimits; import org.geoserver.security.VectorAccessLimits; import org.geoserver.security.WMSAccessLimits; import org.geoserver.security.WorkspaceAccessLimits; import org.geoserver.security.impl.LayerGroupContainmentCache.LayerGroupSummary; import org.geotools.util.logging.Logging; import org.opengis.filter.Filter; import org.springframework.security.core.Authentication; /** * Default implementation of {@link DataAccessManager}, loads simple access * rules from a properties file or a Properties object. The format of each * property is:<br> * <code>workspace.layer.mode=[role]*</code><br> * where: * <ul> * <li> workspace: either a workspace name or a * to indicate any workspace (in * this case, the layer must also be *) </li> * <li> layer: either a layer name (feature type, coverage, layer group) or * to * indicate any layer </li> * <li> mode: the access mode, at the time or writing, either "r" * (read) or "w" (write) </li> * <li> role: a user role</li> * </ul> * A special line is used to specify the security mode in which GeoServer operates: * <code>mode=HIDE|CHALLENGE|MIDEX</code> * For the meaning of these three constants see {@link CatalogMode}<p> * For more details on how the security rules are applied, see the <a * href="http://geoserver.org/display/GEOS/GSIP+19+-+Per+layer+security"/>per * layer security proposal</a> on the <a * href="www.geoserver.org">GeoServer</a> web site. * <p> * If no {@link Properties} is provided, one will be looked upon in * <code>GEOSERVER_DATA_DIR/security/layers.properties, and the class will * keep up to date vs changes in the file</code> * * @author Andrea Aime - TOPP */ public class DefaultResourceAccessManager implements ResourceAccessManager, DataAccessManager { static final Logger LOGGER = Logging.getLogger(DefaultResourceAccessManager.class); /** * A {@link LayerGroupSummary} extended with the associated secure tree node */ static class SecuredGroupSummary extends LayerGroupSummary { private SecureTreeNode node; SecuredGroupSummary(LayerGroupSummary origin, SecureTreeNode node) { super(origin); this.node = node; } boolean canAccess(Authentication user, AccessMode mode) { return node == null || node.canAccess(user, mode); } public SecureTreeNode getNode() { return node; } } SecureTreeNode root; DataAccessRuleDAO dao; Catalog rawCatalog; long lastLoaded = Long.MIN_VALUE; LayerGroupContainmentCache groupsCache; @Deprecated public DefaultResourceAccessManager(DataAccessRuleDAO dao) { this(dao, (Catalog) GeoServerExtensions.bean("rawCatalog")); } /** * Pass a reference to the raw, unsecured catalog. The reference is used to evaluate the * relationship between layers and the groups containing them * @param dao * @param rawCatalog */ public DefaultResourceAccessManager(DataAccessRuleDAO dao, Catalog rawCatalog) { this.dao = dao; this.rawCatalog = rawCatalog; this.root = buildAuthorizationTree(dao); this.groupsCache = new LayerGroupContainmentCache(rawCatalog); } public CatalogMode getMode() { return dao.getMode(); } public boolean canAccess(Authentication user, WorkspaceInfo workspace, AccessMode mode) { checkPropertyFile(); SecureTreeNode node = root.getDeepestNode(new String[] { workspace.getName() }); if(node.canAccess(user, mode)) { return true; } // perform a drill down search, we still allow access to the workspace // if there is anything inside the workspace that can be read (otherwise // we are denying access to everything below it, which is not the spirit of the // tree override design) if(mode == AccessMode.READ && canAccessChild(node, user, mode)) { return true; } else { return false; } } /** * Returns true if the user can access the specified node, or one of the nodes * below it * * the specified nodes * @param node * @param user * @param mode * @return */ private boolean canAccessChild(SecureTreeNode node, Authentication user, AccessMode mode) { if(node.canAccess(user, mode)) { return true; } for (SecureTreeNode child : node.getChildren().values()) { if(canAccessChild(child, user, mode)) { return true; } } return false; } public boolean canAccess(Authentication user, LayerInfo layer, AccessMode mode, boolean directAccess) { checkPropertyFile(); if (layer.getResource() == null) { LOGGER.log(Level.FINE, "Layer " + layer + " has no attached resource, " + "assuming it's possible to access it"); // it's a layer whose resource we don't know about return true; } else { return canAccess(user, layer.getResource(), mode, directAccess); } } public boolean canAccess(Authentication user, ResourceInfo resource, AccessMode mode, boolean directAccess) { checkPropertyFile(); String workspace; final String resourceName = resource.getName(); try { workspace = resource.getStore().getWorkspace().getName(); } catch (Exception e) { LOGGER.log(Level.FINE, "Errors occurred trying to gather workspace of resource " + resourceName); // it's a layer whose resource we don't know about return true; } // if we have a catalog rule that is at resource level, it's the most specific type, // it wins. Or it could be that we do not need to check layer groups at all SecureTreeNode securityNode = root.getDeepestNode(new String[] { workspace, resourceName }); int catalogNodeDepth = securityNode.getDepth(); boolean rulesAllowAccess = securityNode.canAccess(user, mode); if(catalogNodeDepth == SecureTreeNode.RESOURCE_DEPTH || !layerGroupContainmentCheckRequired()) { return rulesAllowAccess; } // grab the groups containing the resource, if any. If none, there is no group related logic to apply Collection<LayerGroupSummary> containers = groupsCache.getContainerGroupsFor(resource); if(containers.isEmpty()) { return rulesAllowAccess; } // there are groups, so there might be more specific rules overriding the catalog one, search for them List<LayerGroupSummary> groupOverrides = containers.stream().filter(sg -> { LayerGroupInfo gi = rawCatalog.getLayerGroup(sg.getId()); if(gi == null) { return false; } SecureTreeNode node = getNodeForGroup(gi); return (node != null && node.getDepth() > catalogNodeDepth) || (sg.getMode() == Mode.OPAQUE_CONTAINER); }).collect(Collectors.toList()); if(!groupOverrides.isEmpty()) { // if there are overrides, see if at least one of them allows access rulesAllowAccess = groupOverrides.stream().anyMatch(sg -> { if(directAccess && sg.getMode() == Mode.OPAQUE_CONTAINER) { return false; } LayerGroupInfo gi = rawCatalog.getLayerGroup(sg.getId()); return gi != null && canAccess(user, gi, directAccess) && (!directAccess || allowsAccessViaNonOpaqueGroup(gi, resource)); }); } if(rulesAllowAccess) { return true; } // the rules allow no access, but there might still be a non secured layer group allowing access to the resource return containers.stream().anyMatch(sg -> { if(directAccess && sg.getMode() == Mode.OPAQUE_CONTAINER) { return false; } LayerGroupInfo gi = rawCatalog.getLayerGroup(sg.getId()); if(gi == null) { return false; } SecureTreeNode node = getNodeForGroup(gi); return node == null && canAccess(user, gi, directAccess) && (!directAccess || allowsAccessViaNonOpaqueGroup(gi, resource)); }); } /** * Returns true if there is a path from the group to the resource that does not involve crossing * a opaque group */ private boolean allowsAccessViaNonOpaqueGroup(LayerGroupInfo gi, ResourceInfo resource) { for (PublishedInfo pi : gi.getLayers()) { if(pi instanceof LayerInfo) { if(resource.equals(((LayerInfo) pi).getResource())) { return true; } } else { LayerGroupInfo lg = (LayerGroupInfo) pi; if(lg.getMode() != LayerGroupInfo.Mode.OPAQUE_CONTAINER && allowsAccessViaNonOpaqueGroup(lg, resource)) { return true; } } } return false; } private SecureTreeNode getNodeForGroup(LayerGroupInfo lg) { SecureTreeNode node; if(lg.getWorkspace() == null) { node = root.getNode(lg.getName()); } else { String[] path = getLayerGroupPath(lg); node = root.getNode(path); } return node; } private boolean layerGroupContainmentCheckRequired() { // first, is it WMS? Request request = Dispatcher.REQUEST.get(); if(request == null) { return false; } // layer groups are used only in WMS final String service = request.getService(); return "WMS".equalsIgnoreCase(service) || "gwc".equalsIgnoreCase(service); } void checkPropertyFile() { rebuildAuthorizationTree(false); } private void rebuildAuthorizationTree(boolean force) { long daoLastModified = dao.getLastModified(); if(lastLoaded < daoLastModified || force) { root = buildAuthorizationTree(dao); lastLoaded = daoLastModified; } } SecureTreeNode buildAuthorizationTree(DataAccessRuleDAO dao) { SecureTreeNode root = new SecureTreeNode(); for(DataAccessRule rule : dao.getRules()) { String workspace = rule.getRoot(); String layer = rule.getLayer(); AccessMode accessMode = rule.getAccessMode(); // look for the node where the rules will have to be set SecureTreeNode node; // check for the * ws definition if (ANY.equals(workspace)) { node = root; } else { // get or create the workspace SecureTreeNode ws = root.getChild(workspace); if (ws == null) { ws = root.addChild(workspace); } // if layer is "*" the rule applies to the ws, otherwise // get/create the layer if ("*".equals(layer)) { node = ws; } else if(rule.isGlobalGroupRule()) { node = ws; } else { SecureTreeNode layerNode = ws.getChild(layer); if (layerNode == null) { layerNode = ws.addChild(layer); } node = layerNode; } } // actually set the rule, but don't complain for the default root contents if (node.getAuthorizedRoles(accessMode) != null && node.getAuthorizedRoles(accessMode).size() > 0 && node != root) { LOGGER.warning("Rule " + rule + " is overriding another rule targetting the same resource"); } node.setAuthorizedRoles(accessMode, rule.getRoles()); } return root; } public DataAccessLimits getAccessLimits(Authentication user, LayerInfo layer, List<LayerGroupInfo> context) { final boolean directAccess = context == null || context.isEmpty(); boolean read = canAccess(user, layer, AccessMode.READ, directAccess); boolean write = canAccess(user, layer, AccessMode.WRITE, directAccess); Filter readFilter = read ? Filter.INCLUDE : Filter.EXCLUDE; Filter writeFilter = write ? Filter.INCLUDE : Filter.EXCLUDE; return buildLimits(layer.getResource().getClass(), readFilter, writeFilter); } public DataAccessLimits getAccessLimits(Authentication user, ResourceInfo resource) { boolean read = canAccess(user, resource, AccessMode.READ); boolean write = canAccess(user, resource, AccessMode.WRITE); Filter readFilter = read ? Filter.INCLUDE : Filter.EXCLUDE; Filter writeFilter = write ? Filter.INCLUDE : Filter.EXCLUDE; return buildLimits(resource.getClass(), readFilter, writeFilter); } DataAccessLimits buildLimits(Class<? extends ResourceInfo> resourceClass, Filter readFilter, Filter writeFilter) { CatalogMode mode = getMode(); // allow the secure catalog to avoid any kind of wrapping if there are no limits if ((readFilter == null || readFilter == Filter.INCLUDE) && (writeFilter == null || writeFilter == Filter.INCLUDE || WMSLayerInfo.class.isAssignableFrom(resourceClass) || CoverageInfo.class .isAssignableFrom(resourceClass))) { return null; } // build the appropriate limit class if (FeatureTypeInfo.class.isAssignableFrom(resourceClass)) { return new VectorAccessLimits(mode, null, readFilter, null, writeFilter); } else if (CoverageInfo.class.isAssignableFrom(resourceClass)) { return new CoverageAccessLimits(mode, readFilter, null, null); } else if (WMSLayerInfo.class.isAssignableFrom(resourceClass)) { return new WMSAccessLimits(mode, readFilter, null, true); } else { LOGGER.log(Level.INFO, "Warning, adapting to generic access limits for unrecognized resource type " + resourceClass); return new DataAccessLimits(mode, readFilter); } } public WorkspaceAccessLimits getAccessLimits(Authentication user, WorkspaceInfo workspace) { boolean readable = canAccess(user, workspace, AccessMode.READ); boolean writable = canAccess(user, workspace, AccessMode.WRITE); boolean adminable = canAccess(user, workspace, AccessMode.ADMIN); CatalogMode mode = getMode(); if (readable && writable) { if (AdminRequest.get() == null) { // not admin request, read+write means full acesss return null; } } return new WorkspaceAccessLimits(mode, readable, writable, adminable); } @Override public StyleAccessLimits getAccessLimits(Authentication user, StyleInfo style) { return null; } @Override public LayerGroupAccessLimits getAccessLimits(Authentication user, LayerGroupInfo layerGroup, List<LayerGroupInfo> containers) { boolean allowAccess = canAccess(user, layerGroup, containers == null || containers.isEmpty()); return allowAccess ? null : new LayerGroupAccessLimits(getMode()); } private boolean canAccess(Authentication user, LayerGroupInfo layerGroup, boolean directAccess) { String[] path = getLayerGroupPath(layerGroup); SecureTreeNode node = root.getDeepestNode(path); boolean catalogNodeAllowsAccess = node.canAccess(user, AccessMode.READ); boolean allowAccess; if(node != null && !catalogNodeAllowsAccess) { allowAccess = false; } else { // grab the groups containing the group, if any. If none, there is no group related logic to apply Collection<LayerGroupSummary> directContainers = groupsCache.getContainerGroupsFor(layerGroup); if(directContainers.isEmpty()) { allowAccess = true; } else { // do we have at least one path that authorizes access to this group? need to check group by group allowAccess = directContainers.stream().anyMatch(sg -> { if(directAccess && sg.getMode() == Mode.OPAQUE_CONTAINER) { return false; } LayerGroupInfo gi = rawCatalog.getLayerGroup(sg.getId()); return gi != null && canAccess(user, gi, directAccess); }); } } return allowAccess; } /** * Returns the possible location of the group in the secured tree based on name and workspace * @param layerGroup * @return */ private String[] getLayerGroupPath(LayerGroupInfo layerGroup) { if(layerGroup.getWorkspace() == null) { return new String[] {layerGroup.getName()}; } else { return new String[] {layerGroup.getWorkspace().getName(), layerGroup.getName()}; } } @Override public Filter getSecurityFilter(Authentication user, Class<? extends CatalogInfo> clazz) { if(getMode() == CatalogMode.CHALLENGE) { // If we're in CHALLENGE mode, we cannot pre-filter // for the other types we have no clue, use the in memory filtering return InMemorySecurityFilter.buildUserAccessFilter(this, user); } if (WorkspaceInfo.class.isAssignableFrom(clazz)) { // base access boolean rootAccess = canAccess(user, root); List<Filter> exceptions = new ArrayList<>(); // exceptions for (Map.Entry<String, SecureTreeNode> entry : root.getChildren().entrySet()) { String wsName = entry.getKey(); SecureTreeNode node = entry.getValue(); boolean nodeAccess = canAccess(user, node); if (nodeAccess != rootAccess) { if (rootAccess) { exceptions.add(Predicates.notEqual("name", wsName)); } else { exceptions.add(Predicates.equal("name", wsName)); } } } if (exceptions.size() == 0) { return rootAccess ? Filter.INCLUDE : Filter.EXCLUDE; } else { return rootAccess ? Predicates.and(exceptions) : Predicates.or(exceptions); } } else if (PublishedInfo.class.isAssignableFrom(clazz) || ResourceInfo.class.isAssignableFrom(clazz) || CoverageInfo.class.isAssignableFrom(clazz)) { // base access boolean rootAccess = canAccess(user, root); List<Filter> exceptions = new ArrayList<>(); // get the right ws property name String wsNameProperty; if (PublishedInfo.class.isAssignableFrom(clazz)) { wsNameProperty = "resource.store.workspace.name"; } else { wsNameProperty = "store.workspace.name"; } // workspace exceptions for (Map.Entry<String, SecureTreeNode> wsEntry : root.getChildren().entrySet()) { String wsName = wsEntry.getKey(); SecureTreeNode wsNode = wsEntry.getValue(); boolean wsAccess = canAccess(user, wsNode); List<Filter> layerExceptions = new ArrayList<>(); for (Map.Entry<String, SecureTreeNode> layerEntry : wsNode.getChildren().entrySet()) { String layerName = layerEntry.getKey(); SecureTreeNode layerNode = layerEntry.getValue(); boolean layerAccess = canAccess(user, layerNode); if (layerAccess != wsAccess) { if (wsAccess) { layerExceptions.add(Predicates.notEqual("prefixedName", wsName + ":" + layerName)); } else { layerExceptions.add(Predicates.equal("prefixedName", wsName + ":" + layerName)); } } } Filter wsFilter = null; if (rootAccess && !wsAccess) { wsFilter = Predicates.notEqual(wsNameProperty, wsName); } else if (!rootAccess && wsAccess) { wsFilter = Predicates.equal(wsNameProperty, wsName); } if(layerExceptions.isEmpty()) { if (wsFilter != null) { exceptions.add(wsFilter); } } else { if (wsFilter != null) { layerExceptions.add(wsFilter); } Filter combined = wsAccess ? Predicates.and(layerExceptions) : Predicates .or(layerExceptions); exceptions.add(combined); } } if (exceptions.size() == 0) { return rootAccess ? Filter.INCLUDE : Filter.EXCLUDE; } else { Filter filter = rootAccess ? Predicates.and(exceptions) : Predicates.or(exceptions); // in case of published info, we have to filter the layer groups as a separate // entity if (PublishedInfo.class.equals(clazz)) { Filter layerFilter = Predicates.and(Predicates.isInstanceOf(LayerInfo.class), filter); Filter layerGroupFilter = Predicates.isInstanceOf(LayerGroupInfo.class); return Predicates.or(layerFilter, layerGroupFilter); } else { return filter; } } } else if (StyleInfo.class.isAssignableFrom(clazz) || LayerGroupInfo.class.isAssignableFrom(clazz)) { // we just check for workspace containment boolean rootAccess = canAccess(user, root); List<Filter> exceptions = new ArrayList<>(); // exceptions for (Map.Entry<String, SecureTreeNode> entry : root.getChildren().entrySet()) { String wsName = entry.getKey(); SecureTreeNode node = entry.getValue(); boolean nodeAccess = canAccess(user, node); if (nodeAccess != rootAccess) { if (rootAccess) { exceptions.add(Predicates.notEqual("workspace.name", wsName)); } else { exceptions.add(Predicates.equal("workspace.name", wsName)); } } } if (exceptions.size() == 0) { return rootAccess ? Filter.INCLUDE : Filter.EXCLUDE; } else { return rootAccess ? Predicates.and(exceptions) : Predicates.or(exceptions); } } else { // for the other types we have no clue, use the in memory filtering return InMemorySecurityFilter.buildUserAccessFilter(this, user); } } private boolean canAccess(Authentication user, SecureTreeNode node) { boolean access = node.canAccess(user, AccessMode.READ); if (access && AdminRequest.get() != null) { // admin request, we need to check if we can also admin those return node.canAccess(user, AccessMode.ADMIN); } else { return access; } } // backwards compatibility methods @Override public boolean canAccess(Authentication user, ResourceInfo resource, AccessMode mode) { return canAccess(user, resource, mode, true); } @Override public boolean canAccess(Authentication user, LayerInfo layer, AccessMode mode) { return canAccess(user, layer, mode, true); } @Override public DataAccessLimits getAccessLimits(Authentication user, LayerInfo layer) { return getAccessLimits(user, layer, Collections.emptyList()); } @Override public LayerGroupAccessLimits getAccessLimits(Authentication user, LayerGroupInfo layerGroup) { return getAccessLimits(user, layerGroup, Collections.emptyList()); } }