diff --git a/SECURITY.md b/SECURITY.md index d05c8c3e202..1f313821623 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,7 +12,7 @@ Each GeoServer release is supported with bug fixes for a year, with releases mad This approach provides ample time for upgrading ensuring you are always working with a supported GeoServer release. -If your organisation is making use of a GeoServer version that is no longer in use by the community all is not lost. +If your organization is making use of a GeoServer version that is no longer in use by the community all is not lost. You can volunteer on the developer list to make additional releases, or engage with one of our [Commercial Support](http://geoserver.org/support/) providers. @@ -53,6 +53,6 @@ Disclosure policy: 4. A fix is included for the "stable" and "maintenance" downloads ([released as scheduled](https://github.com/geoserver/geoserver/wiki/Release-Schedule), or issued via emergency update) 6. The CVE vulnerability is published with mitigation and patch instructions -This represents a balance between transparency and particpation that does not overwhelm particpants. +This represents a balance between transparency and participation that does not overwhelm participants. Those seeking greater visibility are encouraged to volunteer with the geoserver-security list; or work with one of the [commercial support providers](https://geoserver.org/support/) who participate on behalf of their customers. diff --git a/doc/en/api/1.0.0/urlchecks.yaml b/doc/en/api/1.0.0/urlchecks.yaml new file mode 100644 index 00000000000..81c45ae5761 --- /dev/null +++ b/doc/en/api/1.0.0/urlchecks.yaml @@ -0,0 +1,281 @@ +--- +swagger: '2.0' +info: + version: 1.0.0 + title: GeoServer UrlCheck + description: An URL External Access Check is the check performed on user provided URLs that GeoServer will use to access remote resources. + contact: + name: GeoServer + email: 'geoserver-users@sourceforge.net' + url: 'http://geoserver.org/comm/' +host: localhost:8080 +basePath: /geoserver/rest + +paths: + /urlchecks: + get: + operationId: getUrlChecks + tags: + - "UrlChecks" + summary: Get a list of URL checks + description: Displays a list of all URL checks on the server. Use the "Accept:" header to specify format or append an extension to the endpoint (example "/urlchecks.xml" for XML) + produces: + - text/html + - application/json + - application/xml + responses: + 200: + description: OK + schema: + $ref: "#/definitions/urlChecks" + examples: + text/html: | + + + GeoServer Configuration + + + + + + + application/xml: | + + + external + + + + icons + + + + safeWFS + + + + application/json: | + {"urlchecks":{"urlcheck":[ + {"name":"external","href":"http:\/\/localhost:8080\/geoserver\/rest\/urlchecks\/external.json"}, + {"name":"icons","href":"http:\/\/localhost:8080\/geoserver\/rest\/urlchecks\/icons.json"}, + {"name":"safeWFS","href":"http:\/\/localhost:8080\/geoserver\/rest\/urlchecks\/safeWFS.json"}]}} + 401: + description: Unauthorized + + post: + operationId: postUrlChecks + tags: + - "UrlChecks" + summary: add a new URL check to GeoServer + description: Adds a new URL check to the server + parameters: + - name: urlcheckBody + description: The url check body to upload. + in: body + required: true + schema: + $ref: "#/definitions/urlCheck" + consumes: + - application/json + - application/xml + produces: + - text/html + - application/json + - application/xml + responses: + 201: + description: Created + schema: + type: string + headers: + Location: + description: URL where the newly created URL check can be found + type: string + 400: + description: Unable to add provided URL check as it misses required fields + 409: + description: Unable to add URL check as it already exists + + put: + operationId: putUrlChecks + description: Not permitted. + tags: + - "UrlChecks" + responses: + 405: + description: Not permitted + + delete: + operationId: deleteUrlChecks + description: Not permitted. + tags: + - "UrlChecks" + responses: + 405: + description: Not permitted + + /urlchecks/{urlcheckname}: + get: + operationId: getUrlCheck + tags: + - "UrlChecks" + summary: Retrieve a URL check + description: Retrieves a single URL check definition. Use the "Accept:" header to specify format or + append an extension to the endpoint (example "/urlchecks/{urlcheck}.xml" for XML). + produces: + - application/xml + - application/json + - text/html + parameters: + - name: urlcheckname + description: the name of the URL check to fetch. + in: path + required: true + type: string + responses: + 200: + description: OK + schema: + $ref: "#/definitions/urlCheck" + examples: + application/xml: | + + + icons + + + + + application/json: | + { + "urlCheck": { + "name": "icons", + "description": "External graphic icons", + "enabled": true, + "regex": "^https://styles.server.net/icons/.*$" + } + } + 404: + description: URL check does not exist + + post: + tags: + - "UrlChecks" + operationId: postUrlCheck + description: Not permitted. + parameters: + - name: urlcheckname + description: the name of the URL check to fetch. + in: path + required: true + type: string + responses: + 405: + description: Not permitted + + put: + summary: Update a URL check + tags: + - "UrlChecks" + description: Changes the URL check with the provided data. + operationId: putUrlCheck + consumes: + - application/json + - application/xml + responses: + 200: + description: Modified + 400: + description: Cannot perform the change to the URL check as required fields are missing + 404: + description: Url check not found + parameters: + - name: urlcheckname + in: path + description: name of URL check. + required: true + type: string + - name: urlcheckBody + description: The url check body to perform the change against. + in: body + required: true + schema: + $ref: "#/definitions/urlCheck" + + delete: + operationId: deleteUrlCheck + tags: + - "UrlChecks" + parameters: + - name: urlcheckname + in: path + description: name of URL check to delete. + required: true + type: string + responses: + 200: + description: Successfully deleted URL check + 404: + description: Url check doesn't exist + +definitions: + urlCheck: + type: object + description: > + + The body for a URL check request. + + For a PUT, only values which should be changed need to be included. + + + JSON or XML style requests should follow the schemas below: + + - application/xml: + + ``` + + string + string + false + string + + ``` + + - application/json: + + ``` + { + "regexUrlCheck": { + "name": "string", + "description": "string", + "enabled": false, + "regex": "string" + } + } + ``` + + required: + - name + - regex + properties: + name: + type: string + description: name of the URL check + description: + type: string + description: description for the URL check + enabled: + type: boolean + description: enabled status of the URL check + default: false + regex: + type: string + description: regex to perform the check with + urlChecks: + type: array + items: + $ref: "#/definitions/urlCheck" diff --git a/doc/en/pom.xml b/doc/en/pom.xml index a06de9c1ea5..818e90dc4b8 100644 --- a/doc/en/pom.xml +++ b/doc/en/pom.xml @@ -598,6 +598,18 @@ + + urlchecks + process-resources + + generate + + + ${project.basedir}/api/1.0.0/urlchecks.yaml + ${project.build.directory}/api/urlchecks + + + diff --git a/doc/en/user/source/extensions/rat/index.rst b/doc/en/user/source/extensions/rat/index.rst index 4d5166b58b1..875e299b8af 100644 --- a/doc/en/user/source/extensions/rat/index.rst +++ b/doc/en/user/source/extensions/rat/index.rst @@ -14,7 +14,7 @@ and may include information like land cover types, elevation values, or any othe may also contain color information that can be used to render the raster in a more visually appealing way. The RAT is stored in a separate file from the raster data itself, and is typically stored in a ".aux.xml" file, -as part of a a PAMDataset. Each of the bands in the PAM can contain a separate RAT, allowing +as part of a PAMDataset. Each of the bands in the PAM can contain a separate RAT, allowing for different attributes to be associated with each band. One example of RAT usage is the NOAA Bluetopo dataset, which contains 3 floating points bands: @@ -29,4 +29,4 @@ In this section: :maxdepth: 1 installing - using \ No newline at end of file + using diff --git a/doc/en/user/source/extensions/rat/installing.rst b/doc/en/user/source/extensions/rat/installing.rst index ac30f183861..5738ece39c1 100644 --- a/doc/en/user/source/extensions/rat/installing.rst +++ b/doc/en/user/source/extensions/rat/installing.rst @@ -9,6 +9,6 @@ To install the Raster Attribute Table support: .. warning:: Make sure to match the version of the extension to the version of GeoServer. -#. Extract this these files and place the JARs in ``WEB-INF/lib``. +#. Extract these files and place the JARs in ``WEB-INF/lib``. #. Perform any configuration required by your servlet container, and then restart. diff --git a/doc/en/user/source/extensions/rat/using.rst b/doc/en/user/source/extensions/rat/using.rst index 1a8a7158a27..d3b8a8f3ecf 100644 --- a/doc/en/user/source/extensions/rat/using.rst +++ b/doc/en/user/source/extensions/rat/using.rst @@ -17,7 +17,7 @@ allows to generate styles based on the table contents: * The :guilabel:`Classification` dropdown allows to select a column to use for classification. * The :guilabel:`Style name` controls the name of the style to be generated. It's automatically filled with a naming convention of ``_b_``, but can be customized. -* The :guilabel:`Create style` button generates the style based on the chosen classification, eventually using colors if available in the table, otherwise generating random colors. The geneated style will also be included among the "alternate styles" of the layer. +* The :guilabel:`Create style` button generates the style based on the chosen classification, eventually using colors if available in the table, otherwise generating random colors. The generated style will also be included among the "alternate styles" of the layer. The generated style will match all the values in the raster attribute table, and ensure the chosen classification column is used for both styling, legend generation, and ``GetFeatureInfo`` output. @@ -60,6 +60,6 @@ REST API -------- A REST API is available, to fetch the full PAM dataset attached to a raster, and to create -styles out of RAT classfication fields: +styles out of RAT classification fields: -* :api:`/rat ` \ No newline at end of file +* :api:`/rat ` diff --git a/doc/en/user/source/rest/api/index.rst b/doc/en/user/source/rest/api/index.rst index 706b0652ea9..54976d7926f 100644 --- a/doc/en/user/source/rest/api/index.rst +++ b/doc/en/user/source/rest/api/index.rst @@ -34,3 +34,4 @@ This section describes the GeoServer REST configuration API. accesscontrol userrole resources + urlchecks diff --git a/doc/en/user/source/rest/api/urlchecks.rst b/doc/en/user/source/rest/api/urlchecks.rst new file mode 100644 index 00000000000..6a099c99bea --- /dev/null +++ b/doc/en/user/source/rest/api/urlchecks.rst @@ -0,0 +1,94 @@ +.. _rest_api_urlchecks: + +URL Checks +========== + +An ``URL External Access Check`` is the check performed on user provided URLs that GeoServer will use to access remote resources. + +``/urlchecks[.]`` +-------------------------- + +Returns all url checks. + +.. list-table:: + :header-rows: 1 + + * - Method + - Action + - Status code + - Formats + - Default Format + * - GET + - List all url checks + - 200 + - HTML, XML, JSON + - JSON + * - POST + - Create a new url check + - 201 with ``Location`` header + - XML, JSON + - + * - PUT + - + - 405 + - + - + * - DELETE + - + - 405 + - + - + +``/urlchecks/[.]`` +------------------------------- + +Returns a specific url check. + +.. list-table:: + :header-rows: 1 + + * - Method + - Action + - Status code + - Formats + - Default Format + - Parameters + * - GET + - Return url check ``uc`` + - 200 + - HTML, XML, JSON + - JSON + - + * - POST + - + - 405 + - + - + - + * - PUT + - 200 + - Modify url check ``uc`` + - XML, JSON + - + - + * - DELETE + - 200 + - Delete url check ``uc`` + - XML, JSON + - + - + +Exceptions +~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + + * - Exception + - Status code + * - POST or PUT for a url check missing required fields + - 401 + * - GET or DELETE for a url check that does not exist + - 404 + * - POST for a url check that already exists + - 409 diff --git a/doc/en/user/source/rest/index.rst b/doc/en/user/source/rest/index.rst index 64aa022307b..e0944256b8c 100644 --- a/doc/en/user/source/rest/index.rst +++ b/doc/en/user/source/rest/index.rst @@ -43,6 +43,7 @@ The following links provide direct access to the GeoServer REST API documentatio * :api:`/wmtslayers ` * :api:`/wmtsstores ` * :api:`/workspaces ` +* :api:`/urlchecks ` * :api:`/usergroup ` * :api:`/roles ` @@ -97,7 +98,8 @@ This section contains a number of examples which illustrate some of the most com stores imagemosaic appschema - + urlchecks + .. toctree:: diff --git a/doc/en/user/source/rest/urlchecks.rst b/doc/en/user/source/rest/urlchecks.rst new file mode 100644 index 00000000000..7610b87741b --- /dev/null +++ b/doc/en/user/source/rest/urlchecks.rst @@ -0,0 +1,156 @@ +.. _rest_urlchecks: + +URL Checks +========== + +This REST API allows you to create and manage URL External Access Checks in GeoServer. + +.. note:: Read the :api:`API reference for /urlchecks `. + +Listing all URL Checks +---------------------- + +**List all URL Checks on the server, in JSON format:** + +*Request* + +.. admonition:: curl + + :: + + curl -u admin:geoserver -XGET http://localhost:8080/geoserver/rest/urlchecks.json + +*Response* + +.. code-block:: json + + {"urlchecks":{"urlcheck":[ + {"name":"external","href":"http:\/\/localhost:8080\/geoserver\/rest\/urlchecks\/external.json"}, + {"name":"icons","href":"http:\/\/localhost:8080\/geoserver\/rest\/urlchecks\/icons.json"}, + {"name":"safeWFS","href":"http:\/\/localhost:8080\/geoserver\/rest\/urlchecks\/safeWFS.json"}]}} + + +**List all URL Checks, in XML format:** + +*Request* + +.. admonition:: curl + + :: + + curl -u admin:geoserver -XGET http://localhost:8080/geoserver/rest/urlchecks.xml + +*Response* + +.. code-block:: xml + + + + external + + + + icons + + + + safeWFS + + + + + +Listing URL Check details +------------------------- + +**Retrieve information about a specific URL Check:** + +*Request* + +.. admonition:: curl + + :: + + curl -u admin:geoserver -XGET http://localhost:8080/geoserver/rest/urlchecks/icons.xml + +*Response* + +.. code-block:: xml + + + icons + External graphic icons + true + ^https://styles.server.net/icons/.*$ + + + +Creating a URL Check +-------------------- + +**Create a new URL Check:** + +*Request* + +.. admonition:: curl + + :: + + curl -u admin:geoserver -XPOST -H "Content-type: text/xml" \ + -d " \ + icons \ + External graphic icons \ + true \ + ^https://styles\.server\.net/icons/.*$ \ + " \ + http://localhost:8080/geoserver/rest/urlchecks + +*Response* + +:: + + 201 Created + +Changing an existing URL Check +------------------------------ + +**Edit the configuration of an existing URL Check:** + +*Request* + +.. admonition:: curl + + :: + + curl -u admin:geoserver -XPUT -H "Content-type: text/xml" \ + -d " \ + External graphic icons (disabled) \ + false \ + ^https://styles\.server\.com/icons/.*$ \ + " \ + http://localhost:8080/geoserver/rest/urlchecks/icons + +*Response* + +:: + + 200 OK + +Deleting a URL Check +-------------------- + +**Remove a URL Check:** + +*Request* + +.. admonition:: curl + + :: + + curl -u admin:geoserver -XDELETE http://localhost:8080/geoserver/rest/urlchecks/icons + +*Response* + +:: + + 200 OK diff --git a/src/community/features-templating/features-templating-core/src/main/java/org/geoserver/featurestemplating/writers/GMLDialectManager.java b/src/community/features-templating/features-templating-core/src/main/java/org/geoserver/featurestemplating/writers/GMLDialectManager.java index 5a4661e1fc1..d7955a767ba 100644 --- a/src/community/features-templating/features-templating-core/src/main/java/org/geoserver/featurestemplating/writers/GMLDialectManager.java +++ b/src/community/features-templating/features-templating-core/src/main/java/org/geoserver/featurestemplating/writers/GMLDialectManager.java @@ -12,8 +12,8 @@ import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import org.geotools.api.referencing.crs.CoordinateReferenceSystem; +import org.geotools.geometry.jts.MultiSurface; import org.geotools.geometry.jts.ReferencedEnvelope; -import org.geotools.gml3.MultiSurface; import org.geotools.referencing.CRS; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; diff --git a/src/community/jms-cluster/jms-geoserver/pom.xml b/src/community/jms-cluster/jms-geoserver/pom.xml index 68ac73c06be..6aad35818fc 100644 --- a/src/community/jms-cluster/jms-geoserver/pom.xml +++ b/src/community/jms-cluster/jms-geoserver/pom.xml @@ -9,7 +9,6 @@ 2.26-SNAPSHOT - org.geoserver.community gs-jms-geoserver jar GeoServer JMS clustering module @@ -35,7 +34,6 @@ org.geoserver gs-main - ${project.version} @@ -53,10 +51,6 @@ org.geoserver gs-rest - - org.jdom - jdom2 - org.restlet @@ -177,7 +171,6 @@ org.jasypt jasypt - 1.9.2 @@ -243,7 +236,6 @@ org.geoserver gs-main - ${project.version} tests test @@ -257,7 +249,6 @@ org.geoserver gs-platform - ${project.version} tests test diff --git a/src/community/jms-cluster/jms-geoserver/src/main/java/org/geoserver/cluster/impl/handlers/DocumentFile.java b/src/community/jms-cluster/jms-geoserver/src/main/java/org/geoserver/cluster/impl/handlers/DocumentFile.java index 6af6a9eefbc..a9f25639f2d 100644 --- a/src/community/jms-cluster/jms-geoserver/src/main/java/org/geoserver/cluster/impl/handlers/DocumentFile.java +++ b/src/community/jms-cluster/jms-geoserver/src/main/java/org/geoserver/cluster/impl/handlers/DocumentFile.java @@ -11,7 +11,6 @@ import org.apache.commons.io.IOUtils; import org.geoserver.platform.resource.Resource; import org.geoserver.platform.resource.Resources; -import org.jdom2.JDOMException; /** * Class used to handle a text file @@ -34,9 +33,9 @@ public final String getBody() { * Constructor * * @param path the path referring to this file - * @param document the string containing the body of the file (should be a valid JDOM document) + * @param document the string containing the body of the file */ - public DocumentFile(Resource path, final String document) throws JDOMException, IOException { + public DocumentFile(Resource path, final String document) throws IOException { if (!Resources.exists(path)) { throw new IllegalArgumentException("Unable to locate the file path: \'" + path + "\'"); } @@ -45,7 +44,7 @@ public DocumentFile(Resource path, final String document) throws JDOMException, this.body = document; } - public DocumentFile(Resource path) throws JDOMException, IOException { + public DocumentFile(Resource path) throws IOException { if (!Resources.exists(path)) { throw new IllegalArgumentException("Unable to locate the file path: \'" + path + "\'"); } @@ -65,7 +64,7 @@ public String getResourcePath() { } /** write the body to the passed file argument */ - public void writeTo(Resource file) throws JDOMException, IOException { + public void writeTo(Resource file) throws IOException { try (OutputStream out = file.out()) { IOUtils.write(body, out); out.flush(); diff --git a/src/community/oseo/oseo-core/src/main/java/org/geoserver/opensearch/eo/store/AbstractMappingStore.java b/src/community/oseo/oseo-core/src/main/java/org/geoserver/opensearch/eo/store/AbstractMappingStore.java index 2031cbedacb..567e4b0372a 100644 --- a/src/community/oseo/oseo-core/src/main/java/org/geoserver/opensearch/eo/store/AbstractMappingStore.java +++ b/src/community/oseo/oseo-core/src/main/java/org/geoserver/opensearch/eo/store/AbstractMappingStore.java @@ -402,7 +402,10 @@ public int getCount(Query query) throws IOException { return getDelegateSource().getCount(mappedQuery); } - /** Maps query back the main underlying feature source */ + /** + * Maps query back the main underlying feature source. When updating this method for joins, + * update also #needsJoins + */ protected Query mapToSimpleCollectionQuery(Query query, boolean addJoins) throws IOException { Query result = new Query(getDelegateSource().getSchema().getTypeName()); final Filter originalFilter = query.getFilter(); @@ -447,7 +450,7 @@ protected Query mapToSimpleCollectionQuery(Query query, boolean addJoins) throws result.setSortBy(defaultSort); } - if (addJoins) { + if (addJoins && needsJoins(query)) { // join output layer, if necessary if (hasOutputProperty(query, openSearchAccess.getName(LAYERS), true)) { Filter filter = FF.equal(FF.property("id"), FF.property("layer.cid"), true); @@ -478,6 +481,16 @@ protected Query mapToSimpleCollectionQuery(Query query, boolean addJoins) throws return result; } + /** + * Checks if the query at hand needs to join with other tables, or not. Subclasses should + * override if they override {@link #mapToSimpleCollectionQuery(Query, boolean)} and add extra + * joins + */ + protected boolean needsJoins(Query query) { + return hasOutputProperty(query, openSearchAccess.getName(LAYERS), true) + || hasOutputProperty(query, OGC_LINKS_PROPERTY_NAME, true); + } + private Filter mapFilterToDelegateSchema(final Filter filter) { MappingFilterVisitor visitor = new MappingFilterVisitor(propertyMapper); Filter mappedFilter = (Filter) filter.accept(visitor, null); @@ -530,10 +543,24 @@ protected boolean hasOutputProperty(Query query, Name property, boolean included @Override public FeatureCollection getFeatures(Query query) throws IOException { + // fast path for query with no paging or with no joins + if (!needsJoins(query) + || (query.getStartIndex() == null && query.getMaxFeatures() == Integer.MAX_VALUE)) { + Query mappedQuery = mapToSimpleCollectionQuery(query, true); + SimpleFeatureCollection fc = getDelegateSource().getFeatures(mappedQuery); + HashMap mapperState = new HashMap<>(); + return new MappingFeatureCollection( + schema, fc, it -> mapToComplexFeature(it, mapperState)); + } + + // Paging is active, and joins cause extra records to be returned, so we need to + // first collect the collection/product ids for the current page, and then use a + // paging-less query + // first get the ids of the features we are going to return, no joins to support paging Query idsQuery = mapToSimpleCollectionQuery(query, false); // uncommenting causes a ClassCastException, need to figure out why - // idsQuery.setPropertyNames("eoIdentifier"); // (no can do, there are mandatory fields) + idsQuery.setPropertyNames("eoIdentifier"); SimpleFeatureCollection idFeatureCollection = getDelegateSource().getFeatures(idsQuery); Set ids = new LinkedHashSet<>(); diff --git a/src/community/oseo/oseo-core/src/main/java/org/geoserver/opensearch/eo/store/JDBCProductFeatureStore.java b/src/community/oseo/oseo-core/src/main/java/org/geoserver/opensearch/eo/store/JDBCProductFeatureStore.java index e046e8b568c..3eda9dbf8e7 100644 --- a/src/community/oseo/oseo-core/src/main/java/org/geoserver/opensearch/eo/store/JDBCProductFeatureStore.java +++ b/src/community/oseo/oseo-core/src/main/java/org/geoserver/opensearch/eo/store/JDBCProductFeatureStore.java @@ -107,6 +107,12 @@ protected Query mapToSimpleCollectionQuery(Query query, boolean addJoins) throws return result; } + @Override + protected boolean needsJoins(Query query) { + return super.needsJoins(query) + || hasOutputProperty(query, OpenSearchAccess.QUICKLOOK_PROPERTY_NAME, false); + } + @Override protected void mapPropertiesToComplex( ComplexFeatureBuilder builder, SimpleFeature fi, Map mapperState) { diff --git a/src/community/oseo/oseo-core/src/main/java/org/geoserver/opensearch/eo/store/WorkspaceFeatureSource.java b/src/community/oseo/oseo-core/src/main/java/org/geoserver/opensearch/eo/store/WorkspaceFeatureSource.java index 89e7f632acb..7c444bdf8c8 100644 --- a/src/community/oseo/oseo-core/src/main/java/org/geoserver/opensearch/eo/store/WorkspaceFeatureSource.java +++ b/src/community/oseo/oseo-core/src/main/java/org/geoserver/opensearch/eo/store/WorkspaceFeatureSource.java @@ -6,7 +6,6 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import org.geoserver.catalog.WorkspaceInfo; @@ -15,10 +14,13 @@ import org.geotools.api.data.SimpleFeatureSource; import org.geotools.api.filter.Filter; import org.geotools.api.filter.FilterFactory; -import org.geotools.api.filter.identity.FeatureId; +import org.geotools.api.filter.expression.PropertyName; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.factory.CommonFactoryFinder; +import org.geotools.feature.visitor.UniqueVisitor; import org.geotools.geometry.jts.ReferencedEnvelope; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; /** Adds a workspace filter to the queries/filters, if a workspace is set */ public class WorkspaceFeatureSource extends DecoratingSimpleFeatureSource { @@ -27,6 +29,8 @@ public class WorkspaceFeatureSource extends DecoratingSimpleFeatureSource { static final FilterFactory FF = CommonFactoryFinder.getFilterFactory(); public static String WORKSPACES_FIELD = "workspaces"; + /** Key used to cache the list of current collections in the request context holder */ + public static String WS_COLLECTION_CACHE_KEY = "org.geoserver.os.workspaceCollections"; /** * Constructor @@ -68,20 +72,40 @@ public SimpleFeatureCollection getFeatures(Query query) throws IOException { return delegate.getFeatures(query); } - private Set getCollectionIdsForWorkspace() throws IOException { - Set ids = new LinkedHashSet<>(); - SimpleFeatureCollection idFeatureCollection = getWorkspaceCollection(); - idFeatureCollection.accepts(f -> ids.add(f.getIdentifier()), null); - return ids; + /** + * The collection names can be queried over and over during a STAC/OS interaction, cache it at + * the request level, since it depends only on the eventual workspace context + * + * @return + * @throws IOException + */ + private Set getCollectionNamesForWorkspace() throws IOException { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + // no caching in this case + return queryCollectionNamesForWorkspace(); + } + // there's a request, check if the collection names have been computed already + Object attribute = + attributes.getAttribute(WS_COLLECTION_CACHE_KEY, RequestAttributes.SCOPE_REQUEST); + Set result; + if (attribute instanceof Set) { + result = (Set) attribute; + } else { + result = queryCollectionNamesForWorkspace(); + attributes.setAttribute( + WS_COLLECTION_CACHE_KEY, result, RequestAttributes.SCOPE_REQUEST); + } + return result; } - private Set getCollectionNamesForWorkspace() throws IOException { - String workspace = null; - Set names = new LinkedHashSet<>(); + private Set queryCollectionNamesForWorkspace() throws IOException { SimpleFeatureCollection idFeatureCollection = getWorkspaceCollection(); - idFeatureCollection.accepts( - f -> names.add(f.getProperty("eoIdentifier").getValue().toString()), null); - return names; + UniqueVisitor unique = new UniqueVisitor("eoIdentifier"); + unique.setPreserveOrder(true); + idFeatureCollection.accepts(unique, null); + Set result = unique.getUnique(); + return result; } private SimpleFeatureCollection getWorkspaceCollection() throws IOException { @@ -107,38 +131,31 @@ private SimpleFeatureCollection getWorkspaceCollection() throws IOException { } private Filter appendWorkspaceToFilter(Filter filterIn) throws IOException { - if (delegate.getSchema().getTypeName().equals(JDBCOpenSearchAccess.COLLECTION)) { - Set collectionIds = getCollectionIdsForWorkspace(); - if (collectionIds != null && !collectionIds.isEmpty()) { - return appendWorkspaceIdsToFilter(filterIn, collectionIds); - } - } else if (delegate.getSchema().getTypeName().equals(JDBCOpenSearchAccess.PRODUCT)) { - Set collectionNames = getCollectionNamesForWorkspace(); - if (collectionNames != null && !collectionNames.isEmpty()) { - return appendWorkspaceNamesToFilter(filterIn, collectionNames); + Set collectionNames = getCollectionNamesForWorkspace(); + if (collectionNames != null && !collectionNames.isEmpty()) { + if (delegate.getSchema().getTypeName().equals(JDBCOpenSearchAccess.COLLECTION)) { + return appendWorkspaceNamesToFilter(filterIn, collectionNames, "eoIdentifier"); + } else if (delegate.getSchema().getTypeName().equals(JDBCOpenSearchAccess.PRODUCT)) { + return appendWorkspaceNamesToFilter( + filterIn, collectionNames, "eoParentIdentifier"); } } return filterIn; } - private Filter appendWorkspaceIdsToFilter(Filter filterIn, Set collectionIds) { - if (collectionIds == null || collectionIds.isEmpty()) { - return Filter.EXCLUDE; - } - return FF.and(filterIn, FF.id(collectionIds)); - } - - private Filter appendWorkspaceNamesToFilter(Filter filterIn, Set collectionNames) { + private Filter appendWorkspaceNamesToFilter( + Filter filterIn, Set collectionNames, String identifier) { if (collectionNames == null || collectionNames.isEmpty()) { return filterIn; } - return FF.and(filterIn, collectionNamesToOrFilter(collectionNames)); + return FF.and(filterIn, collectionNamesToOrFilter(identifier, collectionNames)); } - private Filter collectionNamesToOrFilter(Set collectionNames) { + private Filter collectionNamesToOrFilter(String identifier, Set collectionNames) { List orClauses = new ArrayList<>(); + PropertyName identifierProperty = FF.property(identifier); for (String collectionName : collectionNames) { - orClauses.add(FF.equals(FF.property("eoParentIdentifier"), FF.literal(collectionName))); + orClauses.add(FF.equals(identifierProperty, FF.literal(collectionName))); } return FF.or(orClauses); } diff --git a/src/community/oseo/oseo-core/src/test/java/org/geoserver/opensearch/eo/store/JDBCOpenSearchAccessTest.java b/src/community/oseo/oseo-core/src/test/java/org/geoserver/opensearch/eo/store/JDBCOpenSearchAccessTest.java index 272dff6aab4..bd1ffe66dd5 100644 --- a/src/community/oseo/oseo-core/src/test/java/org/geoserver/opensearch/eo/store/JDBCOpenSearchAccessTest.java +++ b/src/community/oseo/oseo-core/src/test/java/org/geoserver/opensearch/eo/store/JDBCOpenSearchAccessTest.java @@ -11,6 +11,8 @@ import static org.geoserver.opensearch.eo.ProductClass.RADAR; import static org.geoserver.opensearch.eo.store.OpenSearchAccess.EO_NAMESPACE; import static org.geoserver.opensearch.eo.store.OpenSearchAccess.LAYERS; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.hasItems; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; @@ -41,15 +43,19 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.TimeZone; import java.util.stream.Collectors; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.time.FastDateFormat; import org.easymock.EasyMock; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.CoverageInfo; @@ -60,12 +66,16 @@ import org.geoserver.opensearch.eo.OSEOInfoImpl; import org.geoserver.opensearch.eo.ProductClass; import org.geoserver.opensearch.eo.store.Indexable.FieldType; +import org.geoserver.ows.Dispatcher; +import org.geoserver.ows.Request; import org.geoserver.platform.GeoServerExtensionsHelper; +import org.geoserver.security.decorators.DecoratingSimpleFeatureSource; import org.geotools.api.data.DataAccessFinder; import org.geotools.api.data.DataStoreFinder; import org.geotools.api.data.FeatureSource; import org.geotools.api.data.FeatureStore; import org.geotools.api.data.Query; +import org.geotools.api.data.SimpleFeatureSource; import org.geotools.api.data.Transaction; import org.geotools.api.feature.Attribute; import org.geotools.api.feature.Feature; @@ -76,19 +86,24 @@ import org.geotools.api.feature.type.FeatureType; import org.geotools.api.feature.type.Name; import org.geotools.api.feature.type.PropertyDescriptor; +import org.geotools.api.filter.Filter; import org.geotools.api.filter.FilterFactory; import org.geotools.api.filter.PropertyIsEqualTo; import org.geotools.api.style.Style; import org.geotools.data.DataUtilities; import org.geotools.data.collection.ListFeatureCollection; +import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.factory.CommonFactoryFinder; import org.geotools.feature.AttributeImpl; import org.geotools.feature.FeatureCollection; import org.geotools.feature.NameImpl; import org.geotools.feature.simple.SimpleFeatureBuilder; +import org.geotools.feature.visitor.MaxVisitor; import org.geotools.filter.text.cql2.CQL; import org.geotools.jdbc.JDBCDataStore; import org.geotools.styling.StyleBuilder; +import org.geotools.util.factory.GeoTools; +import org.geotools.util.logging.DefaultLoggerFactory; import org.hamcrest.Matchers; import org.junit.After; import org.junit.AfterClass; @@ -97,6 +112,10 @@ import org.junit.BeforeClass; import org.junit.Test; import org.locationtech.jts.geom.Polygon; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; public class JDBCOpenSearchAccessTest { @@ -111,6 +130,12 @@ public class JDBCOpenSearchAccessTest { public static final ProductClass GS_PRODUCT = new ProductClass("geoServer", "gs", "http://www.geoserver.org/eo/test"); + // make sure Java logging is used + @BeforeClass + public static void setupLogging() throws IOException, SQLException { + GeoTools.setLoggerFactory(DefaultLoggerFactory.getInstance()); + } + /** * Returns the test fixture to run tests against a PostGIS database * @@ -935,4 +960,85 @@ public void testJDBCProducteFeatureStoreSortJSONB() throws Exception { "{\"a\":{\"archive\":7,\"hello\":6,\"meh\":{\"aver\":9,\"working\":8}},\"c\":5,\"f\":3,\"g\":1,\"h\":4,\"m\":2,\"opt:cloudCover\":34}", f.getAttribute("string").toString()); } + + @Test + public void testJDBCVisitQuery() throws Exception { + JDBCProductFeatureStore testSource = + new JDBCProductFeatureStore( + (JDBCOpenSearchAccess) osAccess, osAccess.getProductSource().getSchema()) { + @Override + public SimpleFeatureSource getDelegateSource() throws IOException { + SimpleFeatureSource delegate = super.getDelegateSource(); + return new DecoratingSimpleFeatureSource(delegate) { + @Override + public SimpleFeatureCollection getFeatures(Query query) + throws IOException { + // query filter did not turn into a list of id matches + assertEquals(Filter.INCLUDE, query.getFilter()); + return super.getFeatures(query); + } + }; + } + }; + + // max visitor, no paging, will use joining, should avoid loading all ids in advance + MaxVisitor visitor = new MaxVisitor("timeStart"); + testSource.getFeatures().accepts(visitor, null); + Date maxDate = (Date) visitor.getMax(); + FastDateFormat format = + FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ssZZ", TimeZone.getTimeZone("UTC")); + assertEquals("2018-02-27 09:20:21Z", format.format(maxDate)); + } + + @Test + public void testCollectionCaching() throws Exception { + // setup environment to cache stuff in + MockHttpServletRequest request = new MockHttpServletRequest(); + ServletRequestAttributes attrs = new ServletRequestAttributes(request); + RequestContextHolder.setRequestAttributes(attrs); + // simulate the presence of a OGC request + Dispatcher.REQUEST.set(new Request()); + + try { + + // grab the collections, check two well known entries + FeatureSource collections = osAccess.getCollectionSource(); + Set collectionIdentifiers = getCollectionIdentifiers(collections); + assertThat(collectionIdentifiers, hasItems("SENTINEL2", "LANDSAT8")); + + // check it has been cached too + Set cachedCollections = + (Set) + attrs.getAttribute( + WorkspaceFeatureSource.WS_COLLECTION_CACHE_KEY, + RequestAttributes.SCOPE_REQUEST); + assertThat(cachedCollections, hasItems("SENTINEL2", "LANDSAT8")); + + // now corrupt the cache, remove one of the two entries + cachedCollections.remove("SENTINEL2"); + + // the query should now miss SENTINEL2 too, since it's using the cached results + collectionIdentifiers = getCollectionIdentifiers(collections); + assertThat( + collectionIdentifiers, allOf(hasItems("LANDSAT8"), not(hasItems("SENTINEL2")))); + } finally { + // clean up + Dispatcher.REQUEST.remove(); + RequestContextHolder.resetRequestAttributes(); + } + } + + private static Set getCollectionIdentifiers( + FeatureSource collections) throws IOException { + Set collectionIdentifiers = new LinkedHashSet<>(); + collections + .getFeatures(Query.ALL) + .accepts( + f -> { + String id = (String) f.getProperty("identifier").getValue(); + collectionIdentifiers.add(id); + }, + null); + return collectionIdentifiers; + } } diff --git a/src/community/oseo/oseo-rest/src/test/java/org/geoserver/opensearch/rest/CollectionLayerTest.java b/src/community/oseo/oseo-rest/src/test/java/org/geoserver/opensearch/rest/CollectionLayerTest.java index aa9a51381de..6b1b1658008 100644 --- a/src/community/oseo/oseo-rest/src/test/java/org/geoserver/opensearch/rest/CollectionLayerTest.java +++ b/src/community/oseo/oseo-rest/src/test/java/org/geoserver/opensearch/rest/CollectionLayerTest.java @@ -768,7 +768,7 @@ public void testCreateTimeRangesMultiband() throws Exception { .collect(Collectors.toList()); assertEquals(1, styles.size()); assertEquals("test123", styles.get(0).getAttribute("name")); - assertEquals("test123", styles.get(0).getAttribute("title")); + assertEquals("gs:test123", styles.get(0).getAttribute("title")); } @Nullable diff --git a/src/community/rest-openapi/generated/feign-client/pom.xml b/src/community/rest-openapi/generated/feign-client/pom.xml index c312ed6cff1..018076466e9 100644 --- a/src/community/rest-openapi/generated/feign-client/pom.xml +++ b/src/community/rest-openapi/generated/feign-client/pom.xml @@ -262,6 +262,22 @@ false + + generate-urlchecks-api + + generate + + + java + ${openapi-base-folder}/urlchecks.yaml + ${project.basedir}/client-api-config.json + false + false + false + false + false + + diff --git a/src/community/rest-openapi/openapi/src/main/resources/org/geoserver/rest/openapi/1.0.0/urlchecks.yaml b/src/community/rest-openapi/openapi/src/main/resources/org/geoserver/rest/openapi/1.0.0/urlchecks.yaml new file mode 100644 index 00000000000..2be18cd8d5c --- /dev/null +++ b/src/community/rest-openapi/openapi/src/main/resources/org/geoserver/rest/openapi/1.0.0/urlchecks.yaml @@ -0,0 +1,249 @@ +--- +swagger: '2.0' +info: + version: 1.0.0 + title: GeoServer UrlCheck + description: An URL External Access Check is the check performed on user provided URLs that GeoServer will use to access remote resources. + contact: + name: GeoServer + email: 'geoserver-users@sourceforge.net' + url: 'http://geoserver.org/comm/' +host: localhost:8080 +basePath: /geoserver/rest + +paths: + /urlchecks: + get: + operationId: getUrlChecks + tags: + - "UrlChecks" + summary: Get a list of URL checks + description: Displays a list of all URL checks on the server. Use the "Accept:" header to specify format or append an extension to the endpoint (example "/urlchecks.xml" for XML) + produces: + - text/html + - application/json + - application/xml + responses: + 200: + description: OK + schema: + $ref: "#/definitions/urlChecks" + examples: + text/html: | + + + GeoServer Configuration + + + + + + + application/xml: | + + + external + + + + icons + + + + safeWFS + + + + application/json: | + {"urlchecks":{"urlcheck":[ + {"name":"external","href":"http:\/\/localhost:8080\/geoserver\/rest\/urlchecks\/external.json"}, + {"name":"icons","href":"http:\/\/localhost:8080\/geoserver\/rest\/urlchecks\/icons.json"}, + {"name":"safeWFS","href":"http:\/\/localhost:8080\/geoserver\/rest\/urlchecks\/safeWFS.json"}]}} + 401: + description: Unauthorized + + post: + operationId: postUrlChecks + tags: + - "UrlChecks" + summary: add a new URL check to GeoServer + description: Adds a new URL check to the server + parameters: + - name: urlcheckBody + description: The url check body to upload. + in: body + required: true + schema: + $ref: "#/definitions/urlCheck" + consumes: + - application/json + - application/xml + produces: + - text/html + - application/json + - application/xml + responses: + 201: + description: Created + schema: + type: string + headers: + Location: + description: URL where the newly created URL check can be found + type: string + 400: + description: Unable to add provided URL check as it misses required fields + 409: + description: Unable to add URL check as it already exists + + /urlchecks/{urlcheckname}: + get: + operationId: getUrlCheck + tags: + - "UrlChecks" + summary: Retrieve a URL check + description: Retrieves a single URL check definition. Use the "Accept:" header to specify format or + append an extension to the endpoint (example "/urlchecks/{urlcheck}.xml" for XML). + produces: + - application/xml + - application/json + - text/html + parameters: + - name: urlcheckname + description: the name of the URL check to fetch. + in: path + required: true + type: string + responses: + 200: + description: OK + schema: + $ref: "#/definitions/urlCheck" + examples: + application/xml: | + + + icons + + + + + application/json: | + { + "urlCheck": { + "name": "icons", + "description": "External graphic icons", + "enabled": true, + "regex": "^https://styles.server.net/icons/.*$" + } + } + 404: + description: URL check does not exist + + put: + summary: Update a URL check + tags: + - "UrlChecks" + description: Changes the URL check with the provided data. + operationId: putUrlCheck + consumes: + - application/json + - application/xml + responses: + 200: + description: Modified + 400: + description: Cannot perform the change to the URL check as required fields are missing + 404: + description: Url check not found + parameters: + - name: urlcheckname + in: path + description: name of URL check. + required: true + type: string + - name: urlcheckBody + description: The url check body to perform the change against. + in: body + required: true + schema: + $ref: "#/definitions/urlCheck" + + delete: + operationId: deleteUrlCheck + tags: + - "UrlChecks" + parameters: + - name: urlcheckname + in: path + description: name of URL check to delete. + required: true + type: string + responses: + 200: + description: Successfully deleted URL check + 404: + description: Url check doesn't exist + +definitions: + urlCheck: + type: object + description: > + + The body for a URL check request. + + For a PUT, only values which should be changed need to be included. + + + JSON or XML style requests should follow the schemas below: + + - application/xml: + + ``` + + string + string + false + string + + ``` + + - application/json: + + ``` + { + "regexUrlCheck": { + "name": "string", + "description": "string", + "enabled": false, + "regex": "string" + } + } + ``` + + required: + - name + - regex + properties: + name: + type: string + description: name of the URL check + description: + type: string + description: description for the URL check + enabled: + type: boolean + description: enabled status of the URL check + default: false + regex: + type: string + description: regex to perform the check with + urlChecks: + type: array + items: + $ref: "#/definitions/urlCheck" + diff --git a/src/extension/wps/wps-core/src/main/java/org/geoserver/wps/xml/v1_0_0/WpsXmlReader.java b/src/extension/wps/wps-core/src/main/java/org/geoserver/wps/xml/v1_0_0/WpsXmlReader.java index ca619b3a03a..d34e5e2c541 100644 --- a/src/extension/wps/wps-core/src/main/java/org/geoserver/wps/xml/v1_0_0/WpsXmlReader.java +++ b/src/extension/wps/wps-core/src/main/java/org/geoserver/wps/xml/v1_0_0/WpsXmlReader.java @@ -50,7 +50,7 @@ public Object read(Object request, Reader reader, Map kvp) throws Exception { try { parsed = parser.parse(reader); } catch (Exception e) { - throw new WPSException("Could not parse XML request.", e); + throw new WPSException("Could not parse XML request.", cleanException(e)); } if (!parser.getValidationErrors().isEmpty()) { diff --git a/src/main/src/main/java/org/geoserver/config/GeoServerInfo.java b/src/main/src/main/java/org/geoserver/config/GeoServerInfo.java index 64a7079b79a..05b3901bee8 100644 --- a/src/main/src/main/java/org/geoserver/config/GeoServerInfo.java +++ b/src/main/src/main/java/org/geoserver/config/GeoServerInfo.java @@ -109,14 +109,16 @@ public interface GeoServerInfo extends Info { Integer getXmlPostRequestLogBufferSize(); /** - * If true it enables evaluation of XML entities contained in XML files received in a service - * (WMS, WFS, ...) request. Default is FALSE. Enabling this feature is a security risk. + * If true it enables unrestricted evaluation of XML entities contained in XML files received in + * a service (WMS, WFS, ...) request. Default is FALSE. Enabling this feature is a security + * risk. */ void setXmlExternalEntitiesEnabled(Boolean xmlExternalEntitiesEnabled); /** - * If true it enables evaluation of XML entities contained in XML files received in a service - * (WMS, WFS, ...) request. Default is FALSE. Enabling this feature is a security risk. + * If true it enables unrestricted evaluation of XML entities contained in XML files received in + * a service (WMS, WFS, ...) request. Default is FALSE. Enabling this feature is a security + * risk. */ Boolean isXmlExternalEntitiesEnabled(); diff --git a/src/main/src/main/java/org/geoserver/config/impl/GeoServerInfoImpl.java b/src/main/src/main/java/org/geoserver/config/impl/GeoServerInfoImpl.java index d89f639213d..c0f1a9d3606 100644 --- a/src/main/src/main/java/org/geoserver/config/impl/GeoServerInfoImpl.java +++ b/src/main/src/main/java/org/geoserver/config/impl/GeoServerInfoImpl.java @@ -268,8 +268,9 @@ public Integer getXmlPostRequestLogBufferSize() { } /** - * If true it enables evaluation of XML entities contained in XML files received in a service - * (WMS, WFS, ...) request. Default is FALSE. Enabling this feature is a security risk. + * If true it enables unrestricted evaluation of XML entities contained in XML files received in + * a service (WMS, WFS, ...) request. Default is FALSE. Enabling this feature is a security + * risk. */ @Override public void setXmlExternalEntitiesEnabled(Boolean xmlExternalEntitiesEnabled) { @@ -277,8 +278,9 @@ public void setXmlExternalEntitiesEnabled(Boolean xmlExternalEntitiesEnabled) { } /** - * If true it enables evaluation of XML entities contained in XML files received in a service - * (WMS, WFS, ...) request. Default is FALSE. Enabling this feature is a security risk. + * If true it enables unrestricted evaluation of XML entities contained in XML files received in + * a service (WMS, WFS, ...) request. Default is FALSE. Enabling this feature is a security + * risk. */ @Override public Boolean isXmlExternalEntitiesEnabled() { diff --git a/src/main/src/main/java/org/geoserver/config/util/XStreamPersister.java b/src/main/src/main/java/org/geoserver/config/util/XStreamPersister.java index 3afbf20ec55..e067e29044b 100644 --- a/src/main/src/main/java/org/geoserver/config/util/XStreamPersister.java +++ b/src/main/src/main/java/org/geoserver/config/util/XStreamPersister.java @@ -149,6 +149,8 @@ import org.geoserver.platform.GeoServerExtensions; import org.geoserver.security.GeoServerSecurityManager; import org.geoserver.security.SecureCatalogImpl; +import org.geoserver.security.urlchecks.AbstractURLCheck; +import org.geoserver.security.urlchecks.RegexURLCheck; import org.geotools.api.coverage.grid.GridGeometry; import org.geotools.api.referencing.FactoryException; import org.geotools.api.referencing.crs.CoordinateReferenceSystem; @@ -356,6 +358,8 @@ protected void init(XStream xs) { xs.aliasField("abstract", ResourceInfoImpl.class, "_abstract"); xs.alias("AuthorityURL", AuthorityURLInfo.class); xs.alias("Identifier", LayerIdentifierInfo.class); + xs.alias("urlCheck", AbstractURLCheck.class); + xs.alias("regexUrlCheck", RegexURLCheck.class); // GeoServerInfo xs.omitField(impl(GeoServerInfo.class), "clientProperties"); diff --git a/src/main/src/main/java/org/geoserver/security/urlchecks/AbstractURLCheck.java b/src/main/src/main/java/org/geoserver/security/urlchecks/AbstractURLCheck.java index 42a4d7a4c3f..a9003c6ddcc 100644 --- a/src/main/src/main/java/org/geoserver/security/urlchecks/AbstractURLCheck.java +++ b/src/main/src/main/java/org/geoserver/security/urlchecks/AbstractURLCheck.java @@ -43,13 +43,13 @@ public void setDescription(String description) { this.description = description; } - /** @return the enable */ + /** @return the enabled status */ @Override public boolean isEnabled() { return enabled; } - /** @param enabled the enable to set */ + /** @param enabled the enabled status to set */ public void setEnabled(boolean enabled) { this.enabled = enabled; } diff --git a/src/main/src/main/java/org/geoserver/security/urlchecks/RegexURLCheck.java b/src/main/src/main/java/org/geoserver/security/urlchecks/RegexURLCheck.java index e0a9ef2ac3b..42fd97da79e 100644 --- a/src/main/src/main/java/org/geoserver/security/urlchecks/RegexURLCheck.java +++ b/src/main/src/main/java/org/geoserver/security/urlchecks/RegexURLCheck.java @@ -49,7 +49,7 @@ public int hashCode() { @Override public String toString() { - return "URLCheckInfo{" + return "RegexUrlCheck{" + "name='" + name + '\'' @@ -71,6 +71,6 @@ public boolean confirm(String location) { @Override public String getConfiguration() { - return regex; + return getRegex(); } } diff --git a/src/main/src/main/java/org/geoserver/util/AllowListEntityResolver.java b/src/main/src/main/java/org/geoserver/util/AllowListEntityResolver.java index e025d510b53..37a7e66134f 100644 --- a/src/main/src/main/java/org/geoserver/util/AllowListEntityResolver.java +++ b/src/main/src/main/java/org/geoserver/util/AllowListEntityResolver.java @@ -28,7 +28,10 @@ public class AllowListEntityResolver implements EntityResolver2, Serializable { public static String UNRESTRICTED = "*"; /** Location of Open Geospatical Consortium schemas for OGC OpenGIS standards */ - public static String OGC = "schemas.opengis.net|www.opengis.net"; + public static String OGC1 = "schemas.opengis.net"; + + public static String OGC2 = "www.opengis.net"; + public static String OGC = OGC1 + "|" + OGC2; /** * Location of {@code http://inspire.ec.europa.eu/schemas/ } XSD documents for INSPIRE program @@ -43,7 +46,16 @@ public class AllowListEntityResolver implements EntityResolver2, Serializable { protected static final Logger LOGGER = Logging.getLogger(AllowListEntityResolver.class); - /** Internal uri references */ + /** + * Internal uri references. + * + *
    + *
  • allow schema parsing for validation. + *
  • http(s) - external schema reference + *
  • jar - internal schema reference + *
  • vfs - internal schema reference (JBoss/WildFly) + *
+ */ private static final Pattern INTERNAL_URIS = Pattern.compile("(?i)(jar:file|vfs)[^?#;]*\\.xsd"); /** Allowed http(s) locations */ @@ -82,11 +94,13 @@ public AllowListEntityResolver(GeoServer geoServer, String baseURL) { ALLOWED_URIS = Pattern.compile( "(?i)(http|https)://(" - + W3C + + Pattern.quote(W3C) + + "|" + + Pattern.quote(OGC1) + "|" - + OGC + + Pattern.quote(OGC2) + "|" - + INSPIRE + + Pattern.quote(INSPIRE) + ")/[^?#;]*\\.xsd"); } else { StringBuilder pattern = new StringBuilder("(?i)(http|https)://("); @@ -97,7 +111,7 @@ public AllowListEntityResolver(GeoServer geoServer, String baseURL) { } else { pattern.append('|'); } - pattern.append(allow); + pattern.append(Pattern.quote(allow)); } pattern.append(")/[^?#;]*\\.xsd"); String regex = pattern.toString(); @@ -131,6 +145,14 @@ public InputSource resolveEntity(String name, String publicId, String baseURI, S try { String uri; + if (systemId == null) { + if (name != null) { + LOGGER.finest("resolveEntity name: " + name); + return null; + } + throw new SAXException("External entity systemId not provided"); + } + if (URI.create(systemId).isAbsolute()) { uri = systemId; } else { @@ -175,4 +197,14 @@ public InputSource resolveEntity(String name, String publicId, String baseURI, S // do not allow external entities throw new SAXException(ERROR_MESSAGE_BASE + systemId); } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("AllowListEntityResolver:( "); + builder.append(this.baseURL); + builder.append(" "); + builder.append(this.ALLOWED_URIS); + builder.append(")"); + return builder.toString(); + } } diff --git a/src/main/src/main/java/org/geoserver/util/EntityResolverProvider.java b/src/main/src/main/java/org/geoserver/util/EntityResolverProvider.java index f7e9b9f8c0c..cc73b4e36f3 100644 --- a/src/main/src/main/java/org/geoserver/util/EntityResolverProvider.java +++ b/src/main/src/main/java/org/geoserver/util/EntityResolverProvider.java @@ -73,7 +73,7 @@ public static void setEntityResolver(EntityResolver resolver) { * preventing local file access. This implementation allows access to XSD files included in the * geoserver application. * - * @return EntityResolver, or null if + * @return EntityResolver, or {@code null} if unrestricted */ public EntityResolver getEntityResolver() { if (geoServer != null) { @@ -102,11 +102,10 @@ public EntityResolver getEntityResolver() { * "ENTITY_RESOLUTION_ALLOWLIST". * *
    - *
  • "*": Allow all http(s) schema locations - *
  • "" or undefined: Restrict to schemas provided by w3c, ogc and inspire - *
  • - * "location1,location2": Restrict to the provided locations, and those list by w4c, ogc and inspire - * + *
  • {@code "*"}: Allow all http(s) schema locations + *
  • {@code ""} or undefined: Restrict to schemas provided by w3c, ogc and inspire + *
  • {@code "location1,location2"}: Restrict to the provided locations, and those list by + * w4c, ogc and inspire *
* *

The built-in list appended by {@link AllowListEntityResolver} is equivalent to: @@ -130,17 +129,16 @@ public static Set entityResolutionAllowlist() { * @return set of allowed http(s) entity expansion external locations. */ static Set entityResolutionAllowlist(String allowed) { - final String[] DEFAULT_LIST = - String.join( - "|", - AllowListEntityResolver.W3C, - AllowListEntityResolver.OGC, - AllowListEntityResolver.INSPIRE) - .split("\\|"); + final String[] DEFAULT_LIST = { + AllowListEntityResolver.W3C, + AllowListEntityResolver.OGC1, + AllowListEntityResolver.OGC2, + AllowListEntityResolver.INSPIRE + }; if (allowed == null || allowed.trim().isEmpty()) { return new HashSet<>(Arrays.asList(DEFAULT_LIST)); - } else if (allowed.contains(AllowListEntityResolver.UNRESTRICTED)) { + } else if (allowed.equals(AllowListEntityResolver.UNRESTRICTED)) { return null; } else { Set allowedList = new HashSet<>(Arrays.asList(DEFAULT_LIST)); diff --git a/src/main/src/test/java/org/geoserver/test/GeoServerSystemTestSupport.java b/src/main/src/test/java/org/geoserver/test/GeoServerSystemTestSupport.java index 0675ff976fe..8ebbfc832fe 100644 --- a/src/main/src/test/java/org/geoserver/test/GeoServerSystemTestSupport.java +++ b/src/main/src/test/java/org/geoserver/test/GeoServerSystemTestSupport.java @@ -259,6 +259,11 @@ private boolean isLocalGeotoolsSchema(String path) { return path.matches(".*modules[\\\\/]extension[\\\\/]xsd[\\\\/].*\\.xsd") || path.matches(".*modules[\\\\/]ogc[\\\\/].*\\.xsd"); } + + @Override + public String toString() { + return "PreventLocalEntityResolver"; + } }; @Override diff --git a/src/main/src/test/java/org/geoserver/util/EntityResolverProviderTest.java b/src/main/src/test/java/org/geoserver/util/EntityResolverProviderTest.java index f11f734bc6c..52aa19bbfb4 100644 --- a/src/main/src/test/java/org/geoserver/util/EntityResolverProviderTest.java +++ b/src/main/src/test/java/org/geoserver/util/EntityResolverProviderTest.java @@ -5,48 +5,214 @@ package org.geoserver.util; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; -import java.util.Arrays; import java.util.Set; +import org.geotools.util.PreventLocalEntityResolver; +import org.junit.After; import org.junit.Test; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; public class EntityResolverProviderTest { + @After + public void after() { + EntityResolverProvider.setEntityResolver(null); + } @Test public void testAllowListDefaults() throws Exception { + // include defaults if allow list is empty Set allowed = EntityResolverProvider.entityResolutionAllowlist(""); - assertNotNull(allowed); + assertNotNull("defaults for empty", allowed); assertEquals(4, allowed.size()); assertTrue(allowed.contains(AllowListEntityResolver.W3C)); assertTrue(allowed.contains(AllowListEntityResolver.INSPIRE)); - assertTrue(allowed.containsAll(Arrays.asList(AllowListEntityResolver.OGC.split("\\|")))); + assertTrue(allowed.contains(AllowListEntityResolver.OGC1)); + assertTrue(allowed.contains(AllowListEntityResolver.OGC2)); + // include defaults if allow list is null allowed = EntityResolverProvider.entityResolutionAllowlist(null); - assertNotNull(allowed); + assertNotNull("defaults for null", allowed); assertEquals(4, allowed.size()); assertTrue(allowed.contains(AllowListEntityResolver.W3C)); assertTrue(allowed.contains(AllowListEntityResolver.INSPIRE)); - assertTrue(allowed.containsAll(Arrays.asList(AllowListEntityResolver.OGC.split("\\|")))); + assertTrue(allowed.contains(AllowListEntityResolver.OGC1)); + assertTrue(allowed.contains(AllowListEntityResolver.OGC2)); } @Test - public void testAllowListWildCard() throws Exception { + public void testAllowListUnrestriced() throws Exception { Set allowed = EntityResolverProvider.entityResolutionAllowlist("*"); - assertNull(allowed); + assertNull("null for Unrestricted", allowed); } @Test public void testAllowListDomains() throws Exception { Set allowed = EntityResolverProvider.entityResolutionAllowlist("how2map.com"); - + // confirm allowed includes the defaults assertNotNull(allowed); assertEquals(5, allowed.size()); assertTrue(allowed.contains(AllowListEntityResolver.W3C)); assertTrue(allowed.contains(AllowListEntityResolver.INSPIRE)); - assertTrue(allowed.containsAll(Arrays.asList(AllowListEntityResolver.OGC.split("\\|")))); + assertTrue(allowed.contains(AllowListEntityResolver.OGC1)); + assertTrue(allowed.contains(AllowListEntityResolver.OGC2)); + // in addition to the provided domain assertTrue(allowed.contains("how2map.com")); + // and not allow other matches + assertFalse(allowed.contains("geocat.net")); + } + + @Test + public void testNoWildcard() throws Exception { + // AllowListEntityResolver uses '*' to allow all http content (null returned) + Set everything = + EntityResolverProvider.entityResolutionAllowlist( + AllowListEntityResolver.UNRESTRICTED); + assertNull("* allows everything", everything); + + // but wild cards such as `foo*bar` are not supported, strings are quoted and not intended + // to be a RegEx + Set allowed = EntityResolverProvider.entityResolutionAllowlist("foo*bar"); + assertNotNull(allowed); + assertTrue(allowed.contains("foo*bar")); + } + + /** + * Test behaviour of EntityResolveProvider in response to default configuration (if + * ENTITY_RESOLUTION_ALLOWLIST is unset or empty). + * + *

EntityResolver returns {@code null} when the provided URI is *allowed*, Returns an + * Inputstream if the content is provided, or throws an Exception if the URI is not allowed. + */ + @Test + public void testEntityResolverDefaultBehaviour() throws Exception { + EntityResolverProvider provider = new EntityResolverProvider(null); + provider.setEntityResolver(new AllowListEntityResolver(null)); + EntityResolver resolver = provider.getEntityResolver(); + + // Confirm schema is available from public location + // (this is a default from AllowListEntiryResolver) + InputSource filter = + resolver.resolveEntity(null, "http://schemas.opengis.net/filter/1.1.0/filter.xsd"); + assertNull("Public Filter 1.1.0 connection allowed", filter); + + // Confirm schema is available from jars, as is the case for those included in GeoTools + InputSource filterJar = + resolver.resolveEntity( + null, "jar:file:/some/path/gs-main.jar!schemas/filter/1.1.0/filter.xsd"); + assertNull("JAR Filter 1.1.0 connection allowed", filterJar); + + // Confirm schema is available when war is unpacked into JBoss virtual filesystem + InputSource filterJBoss = + resolver.resolveEntity( + null, + "vfsfile:/home/userone/jboss-eap-5.1/jboss-as/server/default_WAR/deploy/geoserver.war/WEB-INF/lib/gs-main.jar!/filter/1.1.0/filter.xsd"); + assertNull("JBoss Virtual File System Filter 1.1.0 connection allowed", filterJBoss); + + // confirm schema CANNOT be accessed from a random website http address + // (such as an external geoserver location mentioned below) + try { + InputSource external = + resolver.resolveEntity( + null, + "https://how2map.geocat.live/geoserver/schemas/wfs/1.0.0/WFS-basic.xsd"); + assertNotNull("Website Filter 1.1.0 not allowed", external); + fail("Filter 1.1.0 is should not be provided built-in"); + } catch (SAXException e) { + // Confirm the exception is clear, and contains the URI for folks to troubleshoot their + // xml document + assertTrue( + "External XSD not allowed", + e.getMessage().startsWith("Entity resolution disallowed for")); + assertTrue( + "External XSD not allowed", + e.getMessage() + .contains( + "https://how2map.geocat.live/geoserver/schemas/wfs/1.0.0/WFS-basic.xsd")); + } + + // not allowed to access local file system + try { + InputSource filesystem = + resolver.resolveEntity( + null, "file:/var/opt/geoserver/data/www/schemas/WFS-basic.xsd"); + assertNotNull("Filesystem Filter 1.1.0 not allowed", filesystem); + fail("Filter 1.1.0 is should not avalable as a file reference"); + } catch (SAXException e) { + // Confirm the exception is clear, and contains the URI for folks to troubleshoot their + // xml document + assertTrue( + "Filesystem XSD not allowed", + e.getMessage().startsWith("Entity resolution disallowed for")); + assertTrue( + "Filesystem XSD not allowed", + e.getMessage() + .contains("file:/var/opt/geoserver/data/www/schemas/WFS-basic.xsd")); + } + } + + /** + * Test behaviour of EntityResolveProvider in response to configuration to prevent local + * filesystem access (as done with ENTITY_RESOLUTION_ALLOWLIST=*) + * + *

EntityResolver returns {@code null} when the provided URI is *allowed*, Returns an + * Inputstream if the content is provided, or throws an Exception if the URI is not allowed. + */ + @Test + public void testEntityResolverPreventLocal() throws Exception { + EntityResolverProvider provider = new EntityResolverProvider(null); + provider.setEntityResolver(PreventLocalEntityResolver.INSTANCE); + EntityResolver resolver = provider.getEntityResolver(); + + // Confirm schema is available from public location + // (this is a default from AllowListEntiryResolver) + InputSource filter = + resolver.resolveEntity(null, "http://schemas.opengis.net/filter/1.1.0/filter.xsd"); + assertNull("Public Filter 1.1.0 connection allowed", filter); + + // Confirm schema is available from jars, as is the case for those included in GeoTools + InputSource filterJar = + resolver.resolveEntity( + null, "jar:file:/some/path/gs-main.jar!schemas/filter/1.1.0/filter.xsd"); + assertNull("JAR Filter 1.1.0 connection allowed", filterJar); + + // Confirm schema is available when war is unpacked into JBoss virtual filesystem + InputSource filterJBoss = + resolver.resolveEntity( + null, + "vfsfile:/home/userone/jboss-eap-5.1/jboss-as/server/default_WAR/deploy/geoserver.war/WEB-INF/lib/gs-main.jar!/filter/1.1.0/filter.xsd"); + assertNull("JBoss Virtual File System Filter 1.1.0 connection allowed", filterJBoss); + + // confirm that by default can access any random website http address + InputSource external = + resolver.resolveEntity( + null, + "https://how2map.geocat.live/geoserver/schemas/wfs/1.0.0/WFS-basic.xsd"); + assertNull("Website Filter 1.1.0 allowed", external); + + // not allowed to access local file system + try { + InputSource filesystem = + resolver.resolveEntity( + null, "file:/var/opt/geoserver/data/www/schemas/WFS-basic.xsd"); + assertNotNull("Filesystem Filter 1.1.0 not allowed", filesystem); + fail("Filter 1.1.0 is should not avalable as a file reference"); + } catch (SAXException e) { + // Confirm the exception is clear, and contains the URI for folks to troubleshoot their + // xml document + assertTrue( + "Filesystem XSD not allowed", + e.getMessage().startsWith("Entity resolution disallowed for")); + assertTrue( + "Filesystem XSD not allowed", + e.getMessage() + .contains("file:/var/opt/geoserver/data/www/schemas/WFS-basic.xsd")); + } } } diff --git a/src/ows/src/main/java/org/geoserver/ows/XmlRequestReader.java b/src/ows/src/main/java/org/geoserver/ows/XmlRequestReader.java index 1d51ca5de09..be14fcc082f 100644 --- a/src/ows/src/main/java/org/geoserver/ows/XmlRequestReader.java +++ b/src/ows/src/main/java/org/geoserver/ows/XmlRequestReader.java @@ -5,12 +5,16 @@ */ package org.geoserver.ows; +import java.io.IOException; import java.io.Reader; import java.util.Map; +import java.util.logging.Logger; import javax.xml.namespace.QName; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.geotools.util.Version; +import org.geotools.util.logging.Logging; +import org.xml.sax.SAXException; /** * Creates a request bean from xml. @@ -24,6 +28,11 @@ * @author Justin Deoliveira, The Open Planning Project, jdeolive@openplans.org */ public abstract class XmlRequestReader { + /** + * Logger for XmlRequestReader subclass established in constructor for logging of parse errors. + */ + final Logger LOGGER; + /** the qualified name of the element this reader can read. */ final QName element; @@ -64,6 +73,8 @@ public XmlRequestReader(QName element, Version version, String serviceId) { this.version = version; this.serviceId = serviceId; + LOGGER = Logging.getLogger(this.getClass()); + if (element == null) { throw new NullPointerException("element"); } @@ -126,4 +137,76 @@ public int hashCode() { public String getServiceId() { return serviceId; } + + /** + * Wrap a request parsing failure in a SAXException to prevent generic FileNotFound and + * ConnectionRefused messages. + * + *

This communicates that there is a problem with the XML Document while avoiding providing + * internal details that are not useful to web service users. + * + *

The full stack trace is preserved to support development and debugging; so the line number + * can be used by developers. + * + * @param t Failure during parsing (such as SAXException or IOException) + * @return Clean SAXException for common parsing failures, or initial throwable + */ + protected Exception cleanException(Exception t) { + if (t instanceof IOException) { + return createParseException(t); + } + if (t instanceof SAXException) { + // Double check SAXException does not echo caused by message + return cleanSaxException((SAXException) t); + } + return t; + } + + /** + * Clean the localized message, in the case where it is reproduced by SAXException default + * constructor. + * + * @param saxException + * @return saxException with nested localized message removed. + */ + protected SAXException cleanSaxException(SAXException saxException) { + Throwable cause = saxException.getCause(); + // We only wish to check SAXException which echos internal caused by message + // Subclasses such as SAXParserException provide a useful message + if (saxException != null + && saxException.getCause() != null + && saxException.getClass() == SAXException.class) { + String saxMessage = saxException.getMessage(); + String causeMessage = cause.getLocalizedMessage(); + if (causeMessage != null && saxMessage.contains(causeMessage)) { + return createParseException(saxException); + } + } + return saxException; + } + + /** + * Log the cause, and return a SAXException indicaitng a parse failure. + * + *

This is a replacement for the provided cause, and includes the same stack trace to assist + * with troubleshooting and debugging. + * + * @param cause + * @return SAXException indicating parse failure + */ + protected SAXException createParseException(Throwable cause) { + // Log actual failure for debugging and troubleshooting + String requestFailure = "XML " + getElement().getLocalPart() + " Parsing Failure: "; + LOGGER.info(requestFailure + cause.toString()); + + // Provide clean SAXException message, keep stacktrace history (for verbose service + // exception document) + String cleanMessage = + "Parsing failed, the xml request is most probably not compliant to " + + getElement().getLocalPart() + + " element"; + SAXException saxException = new SAXException(cleanMessage); + saxException.setStackTrace(cause.getStackTrace()); + return saxException; + } } diff --git a/src/ows/src/test/java/org/geoserver/ows/DispatcherTest.java b/src/ows/src/test/java/org/geoserver/ows/DispatcherTest.java index 349946314fb..1441973a6cd 100644 --- a/src/ows/src/test/java/org/geoserver/ows/DispatcherTest.java +++ b/src/ows/src/test/java/org/geoserver/ows/DispatcherTest.java @@ -10,13 +10,17 @@ import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; @@ -44,6 +48,8 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.web.servlet.ModelAndView; import org.w3c.dom.Document; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; public class DispatcherTest { @Test @@ -130,6 +136,56 @@ public void testReadOpPost() throws Exception { } } + @Test + public void testReadOpPostServiceExceptions() throws Exception { + // Test XmlRequestReader cleanException handling + MessageXmlParser parser = new MessageXmlParser(); + + IOException ioException = new FileNotFoundException("notFound.txt"); + SAXException saxException = new SAXException(ioException); + SAXException saxParseException = + new SAXParseException("glitch", "test.xsd", "test.xsd", 30, 12); + + Exception clean = parser.cleanException(ioException); + String message = clean.getLocalizedMessage(); + assertFalse(message.contains("notFound.txt")); + + Exception cleanSAX = parser.cleanSaxException(saxException); + String message2 = cleanSAX.getLocalizedMessage(); + assertFalse(message2.contains("notFound.txt")); + + Exception cleanSAXParse = parser.cleanSaxException(saxParseException); + assertSame(saxParseException, cleanSAXParse); + } + + // // Test ServiceException via dispatcher + // MockHttpServletRequest request = new MockHttpServletRequest(); + // request.setContextPath("/geoserver"); + // request.setRequestURI("/geoserver/hello"); + // request.setMethod("post"); + // + // String dtdExternal = "\n" + + // " %external;\n" + + // " ]>"; + // String body = dtdExternal+"\n"; + // DelegatingServletInputStream input = + // new DelegatingServletInputStream(new ByteArrayInputStream(body.getBytes())); + // + // try (BufferedReader buffered = new BufferedReader(new InputStreamReader(input))) { + // buffered.mark(2048); + // Request requst = new Request(); + // requst.setInput(buffered); + // + // Request response = Dispatcher.readOpPost(requst); + // assertSame(requst, response); + // } + // catch (ServiceException serviceException) { + // assertTrue(serviceException.getMessage().contains("xml request is most probably + // not compliant to hello element")); + // } + // } + @Test public void testParseKVP() throws Exception { URL url = getClass().getResource("applicationContext.xml"); @@ -194,6 +250,55 @@ public void testParseXML() throws Exception { } } + @Test + public void testParseXMLServiceException() throws Exception { + URL url = getClass().getResource("applicationContext.xml"); + File file = File.createTempFile("geoserver", "req"); + try (FileSystemXmlApplicationContext context = + new FileSystemXmlApplicationContext(url.toString())) { + + Dispatcher dispatcher = (Dispatcher) context.getBean("dispatcher"); + + String dtdExternal = + "\n" + + " %external;\n" + + " ]>"; + String body = dtdExternal + "\n"; + + try (FileOutputStream output = new FileOutputStream(file)) { + output.write(body.getBytes()); + output.flush(); + } + + try (BufferedReader input = + new BufferedReader(new InputStreamReader(new FileInputStream(file)))) { + + input.mark(8192); + + Request req = new Request(); + req.setInput(input); + req.setRequest("Hello"); + req.setPostRequestElementName("Hello"); + req.setService("hello"); + + try { + Object request = dispatcher.parseRequestXML(null, input, req); + fail("ServiceException expected due to invalid DTD reference:" + request); + } catch (SAXException e) { + assertSame(e.getClass(), SAXException.class); + String messsage = e.getMessage(); + assertFalse(messsage.contains("invalid.xsd")); + } catch (Throwable t) { + fail( + "ServiceException expected, to test use use of XmlRequestReader.cleanException(t)"); + } + } + } finally { + file.delete(); + } + } + @Test public void testHelloOperationGet() throws Exception { URL url = getClass().getResource("applicationContext.xml"); @@ -247,8 +352,8 @@ public Object operationExecuted( Request request, Operation operation, Object result) { Operation op = Dispatcher.REQUEST.get().getOperation(); Assert.assertNotNull(op); - Assert.assertTrue(op.getService().getService() instanceof HelloWorld); - Assert.assertTrue(op.getParameters()[0] instanceof Message); + assertTrue(op.getService().getService() instanceof HelloWorld); + assertTrue(op.getParameters()[0] instanceof Message); return result; } }); @@ -542,7 +647,7 @@ public String concat(String param1, String param2) { Object result = dispatcher.execute(new Request(), opDescriptor); Assert.assertEquals("p1p2", result); - Assert.assertTrue(invokeDirectCalled.get()); + assertTrue(invokeDirectCalled.get()); } } @@ -690,7 +795,7 @@ public Request init(Request request) { dispatcher.handleRequest(request, response); - Assert.assertTrue(response.getContentAsString().contains("ows:ExceptionReport")); + assertTrue(response.getContentAsString().contains("ows:ExceptionReport")); Assert.assertEquals( TestDispatcherCallback.Status.FINISHED, callback1.dispatcherStatus.get()); Assert.assertEquals( @@ -727,7 +832,7 @@ public Service serviceDispatched(Request request, Service service) { dispatcher.handleRequest(request, response); - Assert.assertTrue(response.getContentAsString().contains("ows:ExceptionReport")); + assertTrue(response.getContentAsString().contains("ows:ExceptionReport")); Assert.assertEquals( TestDispatcherCallback.Status.FINISHED, callback1.dispatcherStatus.get()); Assert.assertEquals( @@ -764,7 +869,7 @@ public Operation operationDispatched(Request request, Operation operation) { dispatcher.handleRequest(request, response); - Assert.assertTrue(response.getContentAsString().contains("ows:ExceptionReport")); + assertTrue(response.getContentAsString().contains("ows:ExceptionReport")); Assert.assertEquals( TestDispatcherCallback.Status.FINISHED, callback1.dispatcherStatus.get()); Assert.assertEquals( @@ -802,7 +907,7 @@ public Object operationExecuted( dispatcher.handleRequest(request, response); - Assert.assertTrue(response.getContentAsString().contains("ows:ExceptionReport")); + assertTrue(response.getContentAsString().contains("ows:ExceptionReport")); Assert.assertEquals( TestDispatcherCallback.Status.FINISHED, callback1.dispatcherStatus.get()); Assert.assertEquals( @@ -843,7 +948,7 @@ public Response responseDispatched( dispatcher.handleRequest(request, response); - Assert.assertTrue(response.getContentAsString().contains("ows:ExceptionReport")); + assertTrue(response.getContentAsString().contains("ows:ExceptionReport")); Assert.assertEquals( TestDispatcherCallback.Status.FINISHED, callback1.dispatcherStatus.get()); Assert.assertEquals( @@ -887,7 +992,7 @@ public void finished(Request request) { dispatcher.handleRequest(request, response); Assert.assertEquals("Hello world!", response.getContentAsString()); - Assert.assertTrue(firedCallback.get()); + assertTrue(firedCallback.get()); Assert.assertEquals( TestDispatcherCallback.Status.FINISHED, callback1.dispatcherStatus.get()); Assert.assertEquals( @@ -1008,7 +1113,7 @@ public void setCharacterEncoding(String encoding) { // Service exception, null is returned. Assert.assertNull(mov); // Check the response - Assert.assertTrue(response.getContentAsString().contains("Could not parse the XML")); + assertTrue(response.getContentAsString().contains("Could not parse the XML")); } } @@ -1063,7 +1168,7 @@ public void setCharacterEncoding(String encoding) { // Service exception, null is returned. Assert.assertNull(mov); // Check the response - Assert.assertTrue(response.getContentAsString().contains("Could not parse the KVP")); + assertTrue(response.getContentAsString().contains("Could not parse the KVP")); } } diff --git a/src/ows/src/test/java/org/geoserver/ows/MessageXmlParser.java b/src/ows/src/test/java/org/geoserver/ows/MessageXmlParser.java index 381ae064537..360d20eb3a1 100644 --- a/src/ows/src/test/java/org/geoserver/ows/MessageXmlParser.java +++ b/src/ows/src/test/java/org/geoserver/ows/MessageXmlParser.java @@ -25,11 +25,22 @@ public MessageXmlParser(String namespace, Version ver) { @Override public Object read(Object request, Reader reader, Map kvp) throws Exception { - DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - Document doc = builder.parse(new InputSource(reader)); - String message = doc.getDocumentElement().getAttribute("message"); + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - return new Message(message); + dbf.setExpandEntityReferences(false); + dbf.setValidating(false); + dbf.setNamespaceAware(true); + + DocumentBuilder builder = dbf.newDocumentBuilder(); + // builder.setEntityResolver(PreventLocalEntityResolver.INSTANCE); + try { + Document doc = builder.parse(new InputSource(reader)); + String message = doc.getDocumentElement().getAttribute("message"); + + return new Message(message); + } catch (Exception e) { + throw cleanException(e); + } } } diff --git a/src/pom.xml b/src/pom.xml index b9f8b3a3969..eb73cf81846 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -103,7 +103,7 @@ 7.18.0 1.10.11 4.5.14 - 1.4.10 + 1.4.11 1.1.25 true true diff --git a/src/release/war.xml b/src/release/war.xml index 94b7208dd39..4bee2706244 100644 --- a/src/release/war.xml +++ b/src/release/war.xml @@ -42,7 +42,7 @@ ../licenses/NOTICE.md - license + licenses NOTICE.txt diff --git a/src/restconfig/src/main/java/org/geoserver/rest/security/UrlCheckController.java b/src/restconfig/src/main/java/org/geoserver/rest/security/UrlCheckController.java new file mode 100644 index 00000000000..eb2bf5c6fe6 --- /dev/null +++ b/src/restconfig/src/main/java/org/geoserver/rest/security/UrlCheckController.java @@ -0,0 +1,211 @@ +/* (c) 2024 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.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.geoserver.ows.util.OwsUtils; +import org.geoserver.rest.ResourceNotFoundException; +import org.geoserver.rest.RestBaseController; +import org.geoserver.rest.RestException; +import org.geoserver.rest.util.MediaTypeExtensions; +import org.geoserver.rest.wrapper.RestWrapper; +import org.geoserver.security.urlchecks.AbstractURLCheck; +import org.geoserver.security.urlchecks.URLCheckDAO; +import org.geotools.util.logging.Logging; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +@RestController +@RequestMapping( + path = RestBaseController.ROOT_PATH + "/urlchecks", + produces = { + MediaType.APPLICATION_JSON_VALUE, + MediaType.APPLICATION_XML_VALUE, + MediaType.TEXT_HTML_VALUE + }) +public class UrlCheckController extends RestBaseController { + + private static final Logger LOGGER = Logging.getLogger(UrlCheckController.class); + + private final URLCheckDAO urlCheckDao; + + @Autowired + public UrlCheckController(URLCheckDAO urlCheckDAO) { + this.urlCheckDao = urlCheckDAO; + } + + @GetMapping + public RestWrapper urlChecksGet() throws IOException { + List checks = urlCheckDao.getChecks(); + return wrapList(checks, AbstractURLCheck.class); + } + + @GetMapping("/{urlCheckName}") + public RestWrapper urlCheckGet(@PathVariable String urlCheckName) + throws IOException { + + AbstractURLCheck check = urlCheckDao.getCheckByName(urlCheckName); + if (check == null) { + throw new ResourceNotFoundException("No such URL check found: '" + urlCheckName + "'"); + } + + return wrapObject(check, AbstractURLCheck.class); + } + + @PostMapping( + consumes = { + MediaType.TEXT_XML_VALUE, + MediaType.APPLICATION_XML_VALUE, + MediaTypeExtensions.TEXT_JSON_VALUE, + MediaType.APPLICATION_JSON_VALUE + }) + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity urlCheckPost( + @RequestBody AbstractURLCheck urlCheck, UriComponentsBuilder builder) { + try { + + if (urlCheckDao.getCheckByName(urlCheck.getName()) != null) { + throw new RestException( + "URL check '" + urlCheck.getName() + "' already exists", + HttpStatus.CONFLICT); + } + + verifyCheckName(urlCheck); + verifyCheckConfiguration(urlCheck); + + urlCheckDao.save(urlCheck); + + String name = urlCheck.getName(); + + LOGGER.log(Level.FINEST, "Added urlCheck {0}", name); + + return new ResponseEntity<>( + name, composeCreatedResponseHeaders(builder, name), HttpStatus.CREATED); + + } catch (IOException ex) { + throw new RestException( + "Error occurred in creating a new URL check", + HttpStatus.INTERNAL_SERVER_ERROR, + ex); + } + } + + private static void verifyCheckName(AbstractURLCheck urlCheck) { + String checkName = urlCheck.getName(); + if (checkName == null || checkName.isEmpty()) { + throw new RestException("The URL check name is required", HttpStatus.BAD_REQUEST); + } + } + + private HttpHeaders composeCreatedResponseHeaders( + UriComponentsBuilder builder, String checkIdentifier) { + HttpHeaders headers = new HttpHeaders(); + UriComponents uriComponents = + builder.path("/urlchecks/{id}").buildAndExpand(checkIdentifier); + headers.setLocation(uriComponents.toUri()); + headers.setContentType(MediaType.TEXT_PLAIN); + return headers; + } + + @PutMapping( + value = "/{urlCheckName}", + consumes = { + MediaType.TEXT_XML_VALUE, + MediaType.APPLICATION_XML_VALUE, + MediaTypeExtensions.TEXT_JSON_VALUE, + MediaType.APPLICATION_JSON_VALUE + }) + @ResponseStatus(HttpStatus.OK) + public void urlCheckPut( + @RequestBody AbstractURLCheck providedCheck, @PathVariable String urlCheckName) { + try { + + AbstractURLCheck urlCheck = urlCheckDao.getCheckByName(urlCheckName); + if (urlCheck == null) { + throw new ResourceNotFoundException( + "Can't change a non existent URL check (" + urlCheckName + ")"); + } + + verifyCheckType(providedCheck, urlCheck); + if (providedCheck.getConfiguration() != null) { + verifyCheckConfiguration(providedCheck); + } + + OwsUtils.copy(providedCheck, urlCheck, urlCheck.getClass()); + urlCheckDao.save(urlCheck); + + LOGGER.log(Level.FINEST, "Edited urlCheck {0}", urlCheckName); + + } catch (IOException ex) { + throw new RestException( + "Error occurred in changing the URL check", + HttpStatus.INTERNAL_SERVER_ERROR, + ex); + } + } + + private static void verifyCheckType(AbstractURLCheck providedCheck, AbstractURLCheck urlCheck) { + if (!providedCheck.getClass().equals(urlCheck.getClass())) { + throw new RestException( + "The existent URL check type differs from the provided one", + HttpStatus.BAD_REQUEST); + } + } + + private static void verifyCheckConfiguration(AbstractURLCheck urlCheck) { + String checkConfiguration = urlCheck.getConfiguration(); + if (checkConfiguration == null || checkConfiguration.isEmpty()) { + throw new RestException( + "The URL check configuration is required", HttpStatus.BAD_REQUEST); + } + } + + @DeleteMapping(path = "/{urlCheckName}") + protected void urlCheckDelete(@PathVariable String urlCheckName) { + try { + + AbstractURLCheck check = urlCheckDao.getCheckByName(urlCheckName); + if (check == null) { + throw new ResourceNotFoundException( + "No such URL check found: '" + urlCheckName + "'"); + } + + urlCheckDao.removeByName(urlCheckName); + + LOGGER.log(Level.FINEST, "Deleted urlCheck {0}", urlCheckName); + + } catch (IOException ex) { + throw new RestException( + "Error occurred in deleting the URL check", + HttpStatus.INTERNAL_SERVER_ERROR, + ex); + } + } + + @Override + protected String getTemplateName(Object object) { + if (object instanceof AbstractURLCheck) { + return "AbstractUrlCheck"; + } + return null; + } +} diff --git a/src/restconfig/src/main/java/org/geoserver/rest/security/ftl-templates/AbstractUrlCheck.ftl b/src/restconfig/src/main/java/org/geoserver/rest/security/ftl-templates/AbstractUrlCheck.ftl new file mode 100644 index 00000000000..c5787a8045f --- /dev/null +++ b/src/restconfig/src/main/java/org/geoserver/rest/security/ftl-templates/AbstractUrlCheck.ftl @@ -0,0 +1,11 @@ +<#include "head.ftl"> +Url check "${properties.name}" + +

    +
  • Name: ${properties.name}
  • +
  • Description: ${properties.description}
  • +
  • Configuration: ${properties.configuration}
  • +
  • Enabled: ${properties.enabled}
  • +
+ +<#include "tail.ftl"> \ No newline at end of file diff --git a/src/restconfig/src/main/java/org/geoserver/rest/security/ftl-templates/head.ftl b/src/restconfig/src/main/java/org/geoserver/rest/security/ftl-templates/head.ftl new file mode 100644 index 00000000000..37eeabaa41a --- /dev/null +++ b/src/restconfig/src/main/java/org/geoserver/rest/security/ftl-templates/head.ftl @@ -0,0 +1,10 @@ + + + + GeoServer Configuration + + + + +<#setting number_format="#0.0#"> diff --git a/src/restconfig/src/main/java/org/geoserver/rest/security/ftl-templates/tail.ftl b/src/restconfig/src/main/java/org/geoserver/rest/security/ftl-templates/tail.ftl new file mode 100644 index 00000000000..308b1d01b6c --- /dev/null +++ b/src/restconfig/src/main/java/org/geoserver/rest/security/ftl-templates/tail.ftl @@ -0,0 +1,2 @@ + + diff --git a/src/restconfig/src/main/java/org/geoserver/rest/security/ftl-templates/urlchecks.ftl b/src/restconfig/src/main/java/org/geoserver/rest/security/ftl-templates/urlchecks.ftl new file mode 100644 index 00000000000..6ef78d3a8c7 --- /dev/null +++ b/src/restconfig/src/main/java/org/geoserver/rest/security/ftl-templates/urlchecks.ftl @@ -0,0 +1,7 @@ +<#include "head.ftl"> + +<#include "tail.ftl"> diff --git a/src/restconfig/src/test/java/org/geoserver/rest/security/UrlCheckControllerTest.java b/src/restconfig/src/test/java/org/geoserver/rest/security/UrlCheckControllerTest.java new file mode 100644 index 00000000000..6d39b267365 --- /dev/null +++ b/src/restconfig/src/test/java/org/geoserver/rest/security/UrlCheckControllerTest.java @@ -0,0 +1,385 @@ +/* (c) 2024 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 static org.custommonkey.xmlunit.XMLAssert.assertXpathEvaluatesTo; +import static org.geoserver.rest.RestBaseController.ROOT_PATH; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +import java.util.List; +import net.sf.json.JSON; +import net.sf.json.JSONArray; +import net.sf.json.JSONObject; +import org.geoserver.platform.GeoServerExtensions; +import org.geoserver.security.urlchecks.AbstractURLCheck; +import org.geoserver.security.urlchecks.RegexURLCheck; +import org.geoserver.security.urlchecks.URLCheckDAO; +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletResponse; +import org.w3c.dom.Document; + +public class UrlCheckControllerTest extends SecurityRESTTestSupport { + + private URLCheckDAO urlCheckDao; + + @Before + public void setUp() throws Exception { + + urlCheckDao = GeoServerExtensions.bean(URLCheckDAO.class, applicationContext); + + RegexURLCheck check1 = new RegexURLCheck("check1", "regex check 1", "check1://.*"); + RegexURLCheck check2 = new RegexURLCheck("check2", "regex check 2", "check2://.*"); + RegexURLCheck check3 = new RegexURLCheck("check3", "regex check 3", "check3://.*"); + + check2.setEnabled(false); + + urlCheckDao.saveChecks(List.of(check1, check2, check3)); + } + + @Test + public void testGetAllAsXml() throws Exception { + + Document dom = getAsDOM(ROOT_PATH + "/urlchecks.xml"); + + assertXpathEvaluatesTo("3", "count(//urlCheck)", dom); + + assertXpathEvaluatesTo("check1", "//urlCheck[1]/name", dom); + assertXpathEvaluatesTo( + "http://localhost:8080/geoserver/rest/urlchecks/check1.xml", + "//urlCheck[1]/atom:link/@href", + dom); + + assertXpathEvaluatesTo("check2", "//urlCheck[2]/name", dom); + assertXpathEvaluatesTo( + "http://localhost:8080/geoserver/rest/urlchecks/check2.xml", + "//urlCheck[2]/atom:link/@href", + dom); + + assertXpathEvaluatesTo("check3", "//urlCheck[3]/name", dom); + assertXpathEvaluatesTo( + "http://localhost:8080/geoserver/rest/urlchecks/check3.xml", + "//urlCheck[3]/atom:link/@href", + dom); + } + + @Test + public void testGetAllAsJson() throws Exception { + + JSON json = getAsJSON(ROOT_PATH + "/urlchecks.json"); + + JSONArray urlChecks = + ((JSONObject) json).getJSONObject("urlChecks").getJSONArray("urlCheck"); + + assertEquals(3, urlChecks.size()); + + JSONObject check1 = urlChecks.getJSONObject(0); + JSONObject check2 = urlChecks.getJSONObject(1); + JSONObject check3 = urlChecks.getJSONObject(2); + + assertEquals("check1", check1.getString("name")); + assertEquals( + "http://localhost:8080/geoserver/rest/urlchecks/check1.json", + check1.getString("href")); + assertEquals("check2", check2.getString("name")); + assertEquals( + "http://localhost:8080/geoserver/rest/urlchecks/check2.json", + check2.getString("href")); + assertEquals("check3", check3.getString("name")); + assertEquals( + "http://localhost:8080/geoserver/rest/urlchecks/check3.json", + check3.getString("href")); + } + + @Test + public void testGetAllAsHtml() throws Exception { + + Document dom = getAsDOM(ROOT_PATH + "/urlchecks.html"); + + assertXpathEvaluatesTo("3", "count(//html:li)", dom); + + assertXpathEvaluatesTo("check1", "//html:li[1]", dom); + assertXpathEvaluatesTo( + "http://localhost:8080/geoserver/rest/urlchecks/check1.html", + "//html:li[1]/html:a/@href", + dom); + + assertXpathEvaluatesTo("check2", "//html:li[2]", dom); + assertXpathEvaluatesTo( + "http://localhost:8080/geoserver/rest/urlchecks/check2.html", + "//html:li[2]/html:a/@href", + dom); + + assertXpathEvaluatesTo("check3", "//html:li[3]", dom); + assertXpathEvaluatesTo( + "http://localhost:8080/geoserver/rest/urlchecks/check3.html", + "//html:li[3]/html:a/@href", + dom); + } + + @Test + public void testGetAsXml() throws Exception { + + Document dom = getAsDOM(ROOT_PATH + "/urlchecks/check1.xml"); + + assertXpathEvaluatesTo("1", "count(/regexUrlCheck)", dom); + + assertXpathEvaluatesTo("check1", "/regexUrlCheck/name", dom); + assertXpathEvaluatesTo("regex check 1", "/regexUrlCheck/description", dom); + assertXpathEvaluatesTo("true", "/regexUrlCheck/enabled", dom); + assertXpathEvaluatesTo("check1://.*", "/regexUrlCheck/regex", dom); + } + + @Test + public void testGetAsJson() throws Exception { + + JSON json = getAsJSON(ROOT_PATH + "/urlchecks/check2.json"); + + JSONObject urlCheck = ((JSONObject) json).getJSONObject("regexUrlCheck"); + + assertFalse(urlCheck.isNullObject()); + + assertEquals("check2", urlCheck.getString("name")); + assertEquals("regex check 2", urlCheck.getString("description")); + assertEquals("false", urlCheck.getString("enabled")); + assertEquals("check2://.*", urlCheck.getString("regex")); + } + + @Test + public void testGetAsHtml() throws Exception { + + Document dom = getAsDOM(ROOT_PATH + "/urlchecks/check3.html"); + + assertXpathEvaluatesTo("1", "count(//html:ul)", dom); + + assertXpathEvaluatesTo("Name: check3", "//html:li[1]", dom); + assertXpathEvaluatesTo("Description: regex check 3", "//html:li[2]", dom); + assertXpathEvaluatesTo("Configuration: check3://.*", "//html:li[3]", dom); + assertXpathEvaluatesTo("Enabled: true", "//html:li[4]", dom); + } + + @Test + public void testGetUnknown() throws Exception { + + String checkName = "unknown"; + + String requestPath = ROOT_PATH + "/urlchecks/" + checkName + ".html"; + MockHttpServletResponse response = getAsServletResponse(requestPath); + + assertEquals(404, response.getStatus()); + assertEquals("No such URL check found: '" + checkName + "'", response.getContentAsString()); + } + + @Test + public void testPost() throws Exception { + + String checkJson = + "{" + + " \"regexUrlCheck\": {" + + " \"name\": \"check\"," + + " \"description\": \"this is another check\"," + + " \"enabled\": \"true\"," + + " \"regex\": \"http://example.com/.*\"" + + " }" + + "}"; + + String requestPath = ROOT_PATH + "/urlchecks"; + MockHttpServletResponse response = + postAsServletResponse(requestPath, checkJson, APPLICATION_JSON_VALUE); + + assertEquals(201, response.getStatus()); + assertEquals("check", response.getContentAsString()); + assertEquals( + "http://localhost:8080/geoserver/rest/urlchecks/check", + response.getHeader("location")); + + AbstractURLCheck check = urlCheckDao.getCheckByName("check"); + assertEquals("check", check.getName()); + assertEquals("this is another check", check.getDescription()); + assertTrue(check.isEnabled()); + assertEquals("http://example.com/.*", check.getConfiguration()); + } + + @Test + public void testPostWhenAlreadyExists() throws Exception { + + String checkJson = + "{" + + " \"regexUrlCheck\": {" + + " \"name\": \"check\"," + + " \"description\": \"this is another check\"," + + " \"enabled\": \"true\"," + + " \"regex\": \"http://example.com/.*\"" + + " }" + + "}"; + + String requestPath = ROOT_PATH + "/urlchecks"; + postAsServletResponse(requestPath, checkJson, APPLICATION_JSON_VALUE); + + MockHttpServletResponse response = + postAsServletResponse(requestPath, checkJson, APPLICATION_JSON_VALUE); + + assertEquals(409, response.getStatus()); + assertEquals("URL check 'check' already exists", response.getContentAsString()); + } + + @Test + public void testPostWithoutName() throws Exception { + + String checkJson = + "{" + + " \"regexUrlCheck\": {" + + " \"description\": \"this is another check\"," + + " \"enabled\": \"true\"," + + " \"regex\": \"http://example.com/.*\"" + + " }" + + "}"; + + String requestPath = ROOT_PATH + "/urlchecks"; + MockHttpServletResponse response = + postAsServletResponse(requestPath, checkJson, APPLICATION_JSON_VALUE); + + assertEquals(400, response.getStatus()); + assertEquals("The URL check name is required", response.getContentAsString()); + } + + @Test + public void testPostWithoutConfiguration() throws Exception { + + String checkJson = + "{" + + " \"regexUrlCheck\": {" + + " \"name\": \"check\"," + + " \"description\": \"this is another check\"," + + " \"enabled\": \"true\"" + + " }" + + "}"; + + String requestPath = ROOT_PATH + "/urlchecks"; + MockHttpServletResponse response = + postAsServletResponse(requestPath, checkJson, APPLICATION_JSON_VALUE); + + assertEquals(400, response.getStatus()); + assertEquals("The URL check configuration is required", response.getContentAsString()); + } + + @Test + public void testPostWithoutEnabled() throws Exception { + + String checkJson = + "{" + + " \"regexUrlCheck\": {" + + " \"name\": \"check\"," + + " \"description\": \"this is another check\"," + + " \"regex\": \"http://example.com/.*\"" + + " }" + + "}"; + + String requestPath = ROOT_PATH + "/urlchecks"; + MockHttpServletResponse response = + postAsServletResponse(requestPath, checkJson, APPLICATION_JSON_VALUE); + + assertEquals(201, response.getStatus()); + assertEquals("check", response.getContentAsString()); + assertEquals( + "http://localhost:8080/geoserver/rest/urlchecks/check", + response.getHeader("location")); + + AbstractURLCheck check = urlCheckDao.getCheckByName("check"); + assertFalse(check.isEnabled()); + } + + @Test + public void testPut() throws Exception { + + String editCheckJson = + "{" + + " \"regexUrlCheck\": {" + + " \"name\": \"new-check\"," + + " \"description\": \"new description\"," + + " \"enabled\": \"false\"," + + " \"regex\": \"new regex\"" + + " }" + + "}"; + + String requestPath = ROOT_PATH + "/urlchecks/check1"; + MockHttpServletResponse response = + putAsServletResponse(requestPath, editCheckJson, APPLICATION_JSON_VALUE); + + assertEquals(200, response.getStatus()); + + AbstractURLCheck check = urlCheckDao.getCheckByName("new-check"); + assertEquals("new-check", check.getName()); + assertEquals("new description", check.getDescription()); + assertFalse(check.isEnabled()); + assertEquals("new regex", check.getConfiguration()); + } + + @Test + public void testPutUnknown() throws Exception { + + String checkName = "unknown"; + String editCheckJson = "{\"regexUrlCheck\": {}}"; + + String requestPath = ROOT_PATH + "/urlchecks/" + checkName; + MockHttpServletResponse response = + putAsServletResponse(requestPath, editCheckJson, APPLICATION_JSON_VALUE); + + assertEquals(404, response.getStatus()); + assertEquals( + "Can't change a non existent URL check (" + checkName + ")", + response.getContentAsString()); + } + + @Test + public void testPutWithEmptyConfiguration() throws Exception { + + String editCheckJson = "{\"regexUrlCheck\": {\"regex\": \"\"}}"; + + String requestPath = ROOT_PATH + "/urlchecks/check1"; + MockHttpServletResponse response = + putAsServletResponse(requestPath, editCheckJson, APPLICATION_JSON_VALUE); + + assertEquals(400, response.getStatus()); + assertEquals("The URL check configuration is required", response.getContentAsString()); + } + + @Test + public void testPutNoChanges() throws Exception { + + String editCheckJson = "{\"regexUrlCheck\": {}}"; + + String requestPath = ROOT_PATH + "/urlchecks/check1"; + MockHttpServletResponse response = + putAsServletResponse(requestPath, editCheckJson, APPLICATION_JSON_VALUE); + + assertEquals(200, response.getStatus()); + + AbstractURLCheck check = urlCheckDao.getCheckByName("check1"); + assertEquals("check1", check.getName()); + assertEquals("regex check 1", check.getDescription()); + assertFalse(check.isEnabled()); + assertEquals("check1://.*", check.getConfiguration()); + } + + @Test + public void testDelete() throws Exception { + + /* needed to avoid UnsupportedOperationException */ + Thread.sleep(1000); + + String requestPath = ROOT_PATH + "/urlchecks/check1"; + MockHttpServletResponse response = deleteAsServletResponse(requestPath); + + assertEquals(200, response.getStatus()); + + AbstractURLCheck check = urlCheckDao.getCheckByName("check1"); + assertNull(check); + } +} diff --git a/src/wcs1_0/src/main/java/org/geoserver/wcs/xml/v1_0_0/WcsXmlReader.java b/src/wcs1_0/src/main/java/org/geoserver/wcs/xml/v1_0_0/WcsXmlReader.java index 425e9aabf1c..ad9b8353d57 100644 --- a/src/wcs1_0/src/main/java/org/geoserver/wcs/xml/v1_0_0/WcsXmlReader.java +++ b/src/wcs1_0/src/main/java/org/geoserver/wcs/xml/v1_0_0/WcsXmlReader.java @@ -54,9 +54,8 @@ public Object read(Object request, Reader reader, Map kvp) throws Exception { } catch (Exception e) { throw new WcsException( "Parsing failed, the xml request is most probably not compliant to the wcs schema", - e); + cleanException(e)); } - return parsed; } } diff --git a/src/wcs1_1/src/main/java/org/geoserver/wcs/xml/v1_1_1/WcsXmlReader.java b/src/wcs1_1/src/main/java/org/geoserver/wcs/xml/v1_1_1/WcsXmlReader.java index d278e56968a..cb8572f09cc 100644 --- a/src/wcs1_1/src/main/java/org/geoserver/wcs/xml/v1_1_1/WcsXmlReader.java +++ b/src/wcs1_1/src/main/java/org/geoserver/wcs/xml/v1_1_1/WcsXmlReader.java @@ -55,7 +55,7 @@ public Object read(Object request, Reader reader, Map kvp) throws Exception { } catch (Exception e) { throw new WcsException( "Parsing failed, the xml request is most probably not compliant to the wcs schema", - e); + cleanException(e)); } return parsed; diff --git a/src/wcs2_0/src/main/java/org/geoserver/wcs2_0/xml/WcsXmlReader.java b/src/wcs2_0/src/main/java/org/geoserver/wcs2_0/xml/WcsXmlReader.java index 9ca4be863e3..7c55b366301 100644 --- a/src/wcs2_0/src/main/java/org/geoserver/wcs2_0/xml/WcsXmlReader.java +++ b/src/wcs2_0/src/main/java/org/geoserver/wcs2_0/xml/WcsXmlReader.java @@ -55,7 +55,7 @@ public Object read(Object request, Reader reader, Map kvp) throws Exception { } catch (Exception e) { throw new WcsException( "Parsing failed, the xml request is most probably not compliant to the wcs 2.0.1 schema", - e); + cleanException(e)); } return parsed; diff --git a/src/wfs/src/main/java/org/geoserver/wfs/xml/v1_0_0/WfsXmlReader.java b/src/wfs/src/main/java/org/geoserver/wfs/xml/v1_0_0/WfsXmlReader.java index c1f9c5d519f..849fb190d9c 100644 --- a/src/wfs/src/main/java/org/geoserver/wfs/xml/v1_0_0/WfsXmlReader.java +++ b/src/wfs/src/main/java/org/geoserver/wfs/xml/v1_0_0/WfsXmlReader.java @@ -72,8 +72,12 @@ public Object read(Object request, Reader reader, Map kvp) throws Exception { parser.setEntityExpansionLimit(WFSXmlUtils.getEntityExpansionLimitConfiguration()); // parse - Object parsed = parser.parse(reader); - + Object parsed = null; + try { + parsed = parser.parse(reader); + } catch (Exception e) { + throw cleanException(e); + } // if strict was set, check for validation errors and throw an exception if (strict.booleanValue() && !parser.getValidationErrors().isEmpty()) { WFSException exception = new WFSException("Invalid request", "InvalidParameterValue"); diff --git a/src/wfs/src/main/java/org/geoserver/wfs/xml/v1_1_0/WfsXmlReader.java b/src/wfs/src/main/java/org/geoserver/wfs/xml/v1_1_0/WfsXmlReader.java index 0d06c67735e..0c985f15a02 100644 --- a/src/wfs/src/main/java/org/geoserver/wfs/xml/v1_1_0/WfsXmlReader.java +++ b/src/wfs/src/main/java/org/geoserver/wfs/xml/v1_1_0/WfsXmlReader.java @@ -5,6 +5,7 @@ */ package org.geoserver.wfs.xml.v1_1_0; +import java.io.IOException; import java.io.Reader; import java.util.Map; import javax.xml.namespace.QName; @@ -16,6 +17,7 @@ import org.geotools.util.Version; import org.geotools.xsd.Configuration; import org.geotools.xsd.Parser; +import org.xml.sax.SAXException; /** * Xml reader for wfs 1.1.0 xml requests. @@ -59,11 +61,17 @@ public Object read(Object request, Reader reader, Map kvp) throws Exception { // set entity expansion limit parser.setEntityExpansionLimit(WFSXmlUtils.getEntityExpansionLimitConfiguration()); - WFSXmlUtils.initRequestParser(parser, wfs, geoServer, kvp); - Object parsed = WFSXmlUtils.parseRequest(parser, reader, wfs); + try { + WFSXmlUtils.initRequestParser(parser, wfs, geoServer, kvp); + Object parsed = WFSXmlUtils.parseRequest(parser, reader, wfs); - WFSXmlUtils.checkValidationErrors(parser, this); + WFSXmlUtils.checkValidationErrors(parser, this); - return parsed; + return parsed; + } catch (IOException e) { + throw cleanException(e); + } catch (SAXException e) { + throw cleanSaxException(e); + } } } diff --git a/src/wfs/src/main/java/org/geoserver/wfs/xml/v2_0/WfsXmlReader.java b/src/wfs/src/main/java/org/geoserver/wfs/xml/v2_0/WfsXmlReader.java index 2c3b6aee20d..b18bed0513c 100644 --- a/src/wfs/src/main/java/org/geoserver/wfs/xml/v2_0/WfsXmlReader.java +++ b/src/wfs/src/main/java/org/geoserver/wfs/xml/v2_0/WfsXmlReader.java @@ -48,13 +48,17 @@ public Object read(Object request, Reader reader, Map kvp) throws Exception { WFSInfo wfs = wfs(); WFSXmlUtils.initRequestParser(parser, wfs, gs, kvp); - Object parsed = null; + Object parsed; try { parsed = WFSXmlUtils.parseRequest(parser, reader, wfs); } catch (Exception e) { // check the exception, and set code to OperationParsingFailed if code not set if (!(e instanceof ServiceException) || ((ServiceException) e).getCode() == null) { - e = new WFSException("Request parsing failed", e, "OperationParsingFailed"); + e = + new WFSException( + "Request parsing failed", + cleanException(e), + "OperationParsingFailed"); } throw e; } diff --git a/src/wfs/src/test/java/org/geoserver/wfs/ExternalEntitiesTest.java b/src/wfs/src/test/java/org/geoserver/wfs/ExternalEntitiesTest.java index 444a3c67e0d..1fba0f9153a 100644 --- a/src/wfs/src/test/java/org/geoserver/wfs/ExternalEntitiesTest.java +++ b/src/wfs/src/test/java/org/geoserver/wfs/ExternalEntitiesTest.java @@ -208,21 +208,25 @@ public void testWfs1_0() throws Exception { String output = string(post("wfs", WFS_1_0_0_REQUEST)); // the server tried to read a file on local file system - Assert.assertTrue(output.indexOf("java.io.FileNotFoundException") > -1); + Assert.assertTrue( + "FileNotFoundException", + output.indexOf( + "xml request is most probably not compliant to GetFeature element") + > -1); // disable entity parsing cfg.setXmlExternalEntitiesEnabled(false); getGeoServer().save(cfg); output = string(post("wfs", WFS_1_0_0_REQUEST)); - Assert.assertTrue(output.indexOf("Entity resolution disallowed") > -1); + Assert.assertTrue("disallowed", output.indexOf("Entity resolution disallowed") > -1); // set default (entity parsing disabled); cfg.setXmlExternalEntitiesEnabled(null); getGeoServer().save(cfg); output = string(post("wfs", WFS_1_0_0_REQUEST)); - Assert.assertTrue(output.indexOf("Entity resolution disallowed") > -1); + Assert.assertTrue("disallowed", output.indexOf("Entity resolution disallowed") > -1); } finally { cfg.setXmlExternalEntitiesEnabled(null); getGeoServer().save(cfg); @@ -239,21 +243,25 @@ public void testWfs1_1() throws Exception { String output = string(post("wfs", WFS_1_1_0_REQUEST)); // the server tried to read a file on local file system - Assert.assertTrue(output.indexOf("java.io.FileNotFoundException") > -1); + Assert.assertTrue( + "FileNotFoundException", + output.indexOf( + "xml request is most probably not compliant to GetFeature element") + > -1); // disable entity parsing cfg.setXmlExternalEntitiesEnabled(false); getGeoServer().save(cfg); output = string(post("wfs", WFS_1_1_0_REQUEST)); - Assert.assertTrue(output.indexOf("Entity resolution disallowed") > -1); + Assert.assertTrue("disallowed", output.indexOf("Entity resolution disallowed") > -1); // set default (entity parsing disabled); cfg.setXmlExternalEntitiesEnabled(null); getGeoServer().save(cfg); output = string(post("wfs", WFS_1_1_0_REQUEST)); - Assert.assertTrue(output.indexOf("Entity resolution disallowed") > -1); + Assert.assertTrue("disallowed", output.indexOf("Entity resolution disallowed") > -1); } finally { cfg.setXmlExternalEntitiesEnabled(null); getGeoServer().save(cfg); @@ -270,7 +278,10 @@ public void testWfs2_0() throws Exception { String output = string(post("wfs", WFS_2_0_0_REQUEST)); // the server tried to read a file on local file system - Assert.assertTrue(output.indexOf("thisfiledoesnotexist") > -1); + Assert.assertTrue( + output.indexOf( + "xml request is most probably not compliant to GetFeature element") + > -1); // disable entity parsing cfg.setXmlExternalEntitiesEnabled(false); diff --git a/src/wms/src/main/java/org/geoserver/sld/SLDXmlRequestReader.java b/src/wms/src/main/java/org/geoserver/sld/SLDXmlRequestReader.java index 6827d5a35af..b07346e643b 100644 --- a/src/wms/src/main/java/org/geoserver/sld/SLDXmlRequestReader.java +++ b/src/wms/src/main/java/org/geoserver/sld/SLDXmlRequestReader.java @@ -5,10 +5,12 @@ */ package org.geoserver.sld; +import java.io.IOException; import java.io.Reader; import java.util.Map; import org.geoserver.catalog.Styles; import org.geoserver.ows.XmlRequestReader; +import org.geoserver.platform.ServiceException; import org.geoserver.wms.GetMapRequest; import org.geoserver.wms.WMS; import org.geoserver.wms.map.ProcessStandaloneSLDVisitor; @@ -33,16 +35,19 @@ public Object read(Object request, Reader reader, Map kvp) throws Exception { if (request == null) { throw new IllegalArgumentException("request must be not null"); } - - GetMapRequest getMap = (GetMapRequest) request; - StyledLayerDescriptor sld = - Styles.handler(getMap.getStyleFormat()) - .parse(reader, getMap.styleVersion(), null, null); - - // process the sld - sld.accept(new ProcessStandaloneSLDVisitor(wms, getMap)); - // GetMapKvpRequestReader.processStandaloneSld(wms, getMap, sld); - - return getMap; + try { + GetMapRequest getMap = (GetMapRequest) request; + StyledLayerDescriptor sld = + Styles.handler(getMap.getStyleFormat()) + .parse(reader, getMap.styleVersion(), null, null); + + // process the sld + sld.accept(new ProcessStandaloneSLDVisitor(wms, getMap)); + // GetMapKvpRequestReader.processStandaloneSld(wms, getMap, sld); + + return getMap; + } catch (IOException e) { + throw new ServiceException(cleanException(e)); + } } } diff --git a/src/wms/src/main/java/org/geoserver/wms/capabilities/CapabilitiesXmlReader.java b/src/wms/src/main/java/org/geoserver/wms/capabilities/CapabilitiesXmlReader.java index f80336d0f5d..79621777a27 100644 --- a/src/wms/src/main/java/org/geoserver/wms/capabilities/CapabilitiesXmlReader.java +++ b/src/wms/src/main/java/org/geoserver/wms/capabilities/CapabilitiesXmlReader.java @@ -61,13 +61,17 @@ public Object read(Object request, Reader reader, Map kvp) throws Exception { adapter.parse(new InputSource(reader)); } catch (SAXException e) { throw new ServiceException( - e, "XML capabilities request parsing error", getClass().getName()); + cleanSaxException((SAXException) e), + "XML capabilities request parsing error", + getClass().getName()); } catch (IOException e) { throw new ServiceException( - e, "XML capabilities request input error", getClass().getName()); + cleanException(e), + "XML capabilities request input error", + getClass().getName()); } catch (ParserConfigurationException e) { throw new ServiceException( - e, "Some sort of issue creating parser", getClass().getName()); + cleanException(e), "Some sort of issue creating parser", getClass().getName()); } return req; diff --git a/src/wms/src/main/java/org/geoserver/wms/map/GetMapXmlReader.java b/src/wms/src/main/java/org/geoserver/wms/map/GetMapXmlReader.java index e4882bbb9f6..5ca8a3f18ab 100644 --- a/src/wms/src/main/java/org/geoserver/wms/map/GetMapXmlReader.java +++ b/src/wms/src/main/java/org/geoserver/wms/map/GetMapXmlReader.java @@ -137,8 +137,10 @@ public Object read(Object request, Reader reader, Map kvp) throws Exception { + se.getColumnNumber() + " -- " + se.getLocalizedMessage()); + } catch (SAXException sax) { + throw new ServiceException(cleanSaxException(sax)); } catch (Exception e) { - throw new ServiceException(e); + throw new ServiceException(cleanException(e)); } return getMapRequest; diff --git a/src/wms/src/test/java/org/geoserver/wms/map/GetMapXmlReaderTest.java b/src/wms/src/test/java/org/geoserver/wms/map/GetMapXmlReaderTest.java index aff5f7149cc..ac22c59bf83 100644 --- a/src/wms/src/test/java/org/geoserver/wms/map/GetMapXmlReaderTest.java +++ b/src/wms/src/test/java/org/geoserver/wms/map/GetMapXmlReaderTest.java @@ -7,6 +7,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.io.BufferedReader; import java.io.IOException; @@ -16,11 +17,14 @@ import org.geoserver.catalog.CatalogBuilder; import org.geoserver.catalog.CatalogFactory; import org.geoserver.catalog.LayerGroupInfo; +import org.geoserver.config.GeoServerInfo; import org.geoserver.config.GeoServerLoader; import org.geoserver.data.test.MockData; import org.geoserver.ows.Dispatcher; import org.geoserver.platform.ServiceException; +import org.geoserver.test.GeoServerSystemTestSupport; import org.geoserver.test.ows.KvpRequestReaderTestSupport; +import org.geoserver.util.EntityResolverProvider; import org.geoserver.wms.GetMapRequest; import org.geoserver.wms.WMS; import org.geoserver.wms.WMSInfo; @@ -28,6 +32,7 @@ import org.geotools.api.filter.PropertyIsEqualTo; import org.geotools.api.style.Style; import org.junit.Test; +import org.xml.sax.SAXException; public class GetMapXmlReaderTest extends KvpRequestReaderTestSupport { GetMapXmlReader reader; @@ -132,6 +137,32 @@ public void testAllowDynamicStyles() throws Exception { } } + @Test + public void testCleanServiceException() throws Exception { + GeoServerInfo cfg = getGeoServer().getGlobal(); + GetMapRequest request = reader.createRequest(); + // this request forces an IOException + try (BufferedReader input = getResourceInputStream("WMSPostServiceException.xml")) { + + cfg.setXmlExternalEntitiesEnabled(true); + getGeoServer().save(cfg); + + request = (GetMapRequest) reader.read(request, input, new HashMap()); + fail("ServiceException with IOException Expected"); + } catch (ServiceException e) { + assertTrue( + e.getMessage() + .contains( + "xml request is most probably not compliant to GetMap element")); + assertTrue(e.getCause() instanceof SAXException); + } finally { + cfg.setXmlExternalEntitiesEnabled(null); + getGeoServer().save(cfg); + EntityResolverProvider.setEntityResolver( + GeoServerSystemTestSupport.RESOLVE_DISABLED_PROVIDER_DEVMODE); + } + } + @SuppressWarnings("PMD.CloseResource") // wrapped and returned private BufferedReader getResourceInputStream(String classRelativePath) throws IOException { InputStream resourceStream = getClass().getResource(classRelativePath).openStream(); diff --git a/src/wms/src/test/resources/org/geoserver/wms/map/WMSPostServiceException.xml b/src/wms/src/test/resources/org/geoserver/wms/map/WMSPostServiceException.xml new file mode 100644 index 00000000000..0b36d5ef950 --- /dev/null +++ b/src/wms/src/test/resources/org/geoserver/wms/map/WMSPostServiceException.xml @@ -0,0 +1,23 @@ + + + %external; + ]> + + + + topp:states + population + + + + -13024 + -5550 + + + image/png + 550250 + +