/* (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.decorators; import java.io.IOException; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.print.attribute.standard.Severity; import org.geoserver.ows.Dispatcher; import org.geoserver.ows.Request; import org.geoserver.security.AccessLevel; import org.geoserver.security.VectorAccessLimits; import org.geoserver.security.WrapperPolicy; import org.geotools.data.DataAccess; import org.geotools.data.DataUtilities; import org.geotools.data.FeatureSource; import org.geotools.data.Query; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.store.ReTypingFeatureCollection; import org.geotools.factory.Hints; import org.geotools.feature.FeatureCollection; import org.geotools.feature.FeatureTypes; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.util.logging.Logging; import org.opengis.feature.Feature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.FeatureType; import org.opengis.filter.Filter; import org.opengis.filter.expression.PropertyName; /** * Given a {@link FeatureSource} makes sure only the operations allowed by the WrapperPolicy * can be performed through it or using a object that can be accessed thru it. Depending on the * challenge policy, the object and the related ones will simply hide feature source abilities, * or will throw Spring security exceptions * * @author Andrea Aime - GeoSolutions * * @param <T> * @param <F> */ public class SecuredFeatureSource<T extends FeatureType, F extends Feature> extends DecoratingFeatureSource<T, F> { static final Logger LOGGER = Logging.getLogger(SecuredFeatureSource.class); WrapperPolicy policy; protected SecuredFeatureSource(FeatureSource<T, F> delegate, WrapperPolicy policy) { super(delegate); this.policy = policy; } public DataAccess<T, F> getDataStore() { final DataAccess<T, F> store = delegate.getDataStore(); if (store == null) return null; else return (DataAccess) SecuredObjects.secure(store, policy); } public FeatureCollection<T, F> getFeatures() throws IOException { final FeatureCollection<T, F> fc = delegate.getFeatures(getReadQuery()); if (fc == null) return null; else return (FeatureCollection) SecuredObjects.secure(fc, policy); } public FeatureCollection<T, F> getFeatures(Filter filter) throws IOException { return getFeatures(new Query(null, filter)); } public FeatureCollection<T, F> getFeatures(Query query) throws IOException { // mix the external query with the access limits one final Query readQuery = getReadQuery(); final Query mixed = mixQueries(query, readQuery); int limitedAttributeSize = mixed.getProperties() != null ? mixed.getProperties().size() : 0; final FeatureCollection<T, F> fc = delegate.getFeatures(mixed); if (fc == null) { return null; } else { if (limitedAttributeSize > 0 && fc.getSchema().getDescriptors().size() > limitedAttributeSize) { if(fc instanceof SimpleFeatureCollection) { // the datastore did not honour the query properties?? It's broken, but we can fix it SimpleFeatureCollection sfc = (SimpleFeatureCollection) fc; SimpleFeatureType target = SimpleFeatureTypeBuilder.retype(sfc.getSchema(), mixed.getPropertyNames()); ReTypingFeatureCollection retyped = new ReTypingFeatureCollection(sfc, target); return (FeatureCollection) SecuredObjects.secure(retyped, policy); } else { // complex feature store eh? No way to fix it at least warn the admin LOGGER.log(Level.SEVERE, "Complex store returned more properties than allowed " + "by security (because they are required by the schema). " + "Either the security setup is broken or you have a security breach"); return (FeatureCollection) SecuredObjects.secure(fc, policy); } } else { return (FeatureCollection) SecuredObjects.secure(fc, policy); } } } protected Query getReadQuery() { if(policy.getAccessLevel() == AccessLevel.HIDDEN || policy.getAccessLevel() == AccessLevel.METADATA) { return new Query(null, Filter.EXCLUDE); } else if(policy.getLimits() == null) { return Query.ALL; } else if(policy.getLimits() instanceof VectorAccessLimits) { VectorAccessLimits val = (VectorAccessLimits) policy.getLimits(); // Ugly hack: during WFS transactions the reads we do are used to count the number of features // we are deleting/updating: use the write filter instead of the read filter Request request = Dispatcher.REQUEST.get(); if(request != null && request.getService().equalsIgnoreCase("WFS") && request.getRequest().equalsIgnoreCase("Transaction")) { return val.getWriteQuery(); } else { return val.getReadQuery(); } } else { throw new IllegalArgumentException("SecureFeatureSources has been fed " + "with unexpected AccessLimits class " + policy.getLimits().getClass()); } } /** * Mixes two queries with an eye towards security (limiting attributes instead of adding them) * and preserves all of the other properties in userQuery (hints, crs handling, sorting) * @param userQuery * @param securityQuery * */ protected Query mixQueries(Query userQuery, Query securityQuery) { // first rough mix Query result = DataUtilities.mixQueries(userQuery, securityQuery, userQuery.getHandle()); // check request attributes and use those ones only List<PropertyName> securityProperties = securityQuery.getProperties(); if(securityProperties != null && securityProperties.size() > 0) { List<PropertyName> userProperties = userQuery.getProperties(); if(userProperties == null) { result.setProperties(securityProperties); } else { for (PropertyName pn : userProperties) { if(!securityProperties.contains(pn)) { throw new SecurityException("Attribute " + pn.getPropertyName() + " is not available"); } } result.setProperties(userProperties); } } // mix the hints, keep all the user ones and override with the query ones if(userQuery.getHints() == null) { result.setHints(securityQuery.getHints()); } else if(securityQuery.getHints() == null) { result.setHints(userQuery.getHints()); } else { Hints mix = userQuery.getHints(); mix.putAll(securityQuery.getHints()); result.setHints(mix); } // transfer all other properties from the user query result.setCoordinateSystem(userQuery.getCoordinateSystem()); result.setCoordinateSystemReproject(userQuery.getCoordinateSystemReproject()); result.setStartIndex(userQuery.getStartIndex()); result.setSortBy(userQuery.getSortBy()); return result; } }