SLING-8258 Provide HTTP API for Sling Clam
authorOliver Lietz <olli@apache.org>
Thu, 31 Jan 2019 10:42:29 +0000 (11:42 +0100)
committerOliver Lietz <olli@apache.org>
Thu, 31 Jan 2019 10:42:29 +0000 (11:42 +0100)
pom.xml
src/main/java/org/apache/sling/clam/http/internal/ClamScanServlet.java [new file with mode: 0644]
src/main/java/org/apache/sling/clam/http/internal/ClamScanServletConfiguration.java [new file with mode: 0644]
src/main/java/org/apache/sling/clam/http/internal/RequestUtil.java [new file with mode: 0644]
src/main/java/org/apache/sling/clam/http/internal/ResponseUtil.java [new file with mode: 0644]

diff --git a/pom.xml b/pom.xml
index 6869803..2467452 100644 (file)
--- a/pom.xml
+++ b/pom.xml
       <artifactId>jcr</artifactId>
       <scope>provided</scope>
     </dependency>
+    <dependency>
+      <groupId>javax.json</groupId>
+      <artifactId>javax.json-api</artifactId>
+      <version>1.1.4</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>javax.servlet-api</artifactId>
+      <scope>provided</scope>
+    </dependency>
     <!-- OSGi -->
     <dependency>
       <groupId>org.osgi</groupId>
diff --git a/src/main/java/org/apache/sling/clam/http/internal/ClamScanServlet.java b/src/main/java/org/apache/sling/clam/http/internal/ClamScanServlet.java
new file mode 100644 (file)
index 0000000..cb3067e
--- /dev/null
@@ -0,0 +1,162 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.clam.http.internal;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import javax.jcr.Node;
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.SlingHttpServletResponse;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.servlets.SlingAllMethodsServlet;
+import org.apache.sling.clam.jcr.NodeDescendingJcrPropertyDigger;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
+import org.osgi.service.metatype.annotations.Designate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.sling.clam.http.internal.RequestUtil.isAuthorized;
+import static org.apache.sling.clam.http.internal.RequestUtil.maxLength;
+import static org.apache.sling.clam.http.internal.RequestUtil.path;
+import static org.apache.sling.clam.http.internal.RequestUtil.pattern;
+import static org.apache.sling.clam.http.internal.RequestUtil.propertyTypes;
+import static org.apache.sling.clam.http.internal.ResponseUtil.handleError;
+import static org.apache.sling.clam.internal.ClamUtil.propertyTypesFromNames;
+
+@Component(
+    service = Servlet.class,
+    property = {
+        Constants.SERVICE_DESCRIPTION + "=Apache Sling Clam Scan Servlet",
+        Constants.SERVICE_VENDOR + "=The Apache Software Foundation",
+        "sling.servlet.paths=/bin/clam/scan",
+        "sling.auth.requirements=/bin/clam/scan"
+    }
+)
+@Designate(
+    ocd = ClamScanServletConfiguration.class
+)
+public class ClamScanServlet extends SlingAllMethodsServlet {
+
+    @Reference(
+        policy = ReferencePolicy.DYNAMIC,
+        policyOption = ReferencePolicyOption.GREEDY
+    )
+    private volatile NodeDescendingJcrPropertyDigger digger;
+
+    private ClamScanServletConfiguration configuration;
+
+    private Pattern pattern;
+
+    private Set<Integer> propertyTypes;
+
+    private final Logger logger = LoggerFactory.getLogger(ClamScanServlet.class);
+
+    public ClamScanServlet() {
+    }
+
+    @Activate
+    private void activate(final ClamScanServletConfiguration configuration) throws Exception {
+        logger.debug("activating");
+        this.configuration = configuration;
+        configure(configuration);
+    }
+
+    @Modified
+    private void modified(final ClamScanServletConfiguration configuration) throws Exception {
+        logger.debug("modifying");
+        this.configuration = configuration;
+        configure(configuration);
+    }
+
+    @Deactivate
+    private void deactivate() {
+        logger.debug("deactivating");
+        configuration = null;
+        pattern = null;
+        propertyTypes = null;
+    }
+
+    private void configure(final ClamScanServletConfiguration configuration) throws Exception {
+        pattern = Pattern.compile(configuration.digger_default_property_path_pattern());
+        propertyTypes = propertyTypesFromNames(configuration.digger_default_property_types());
+    }
+
+    @Override
+    protected void doPost(@NotNull final SlingHttpServletRequest request, @NotNull final SlingHttpServletResponse response) throws ServletException, IOException {
+        boolean isAuthorized = false;
+        try {
+            isAuthorized = isAuthorized(request, Arrays.asList(configuration.scan_authorized_groups()));
+        } catch (Exception e) {
+            logger.error(e.getMessage(), e);
+        }
+        if (!isAuthorized) {
+            handleError(response, 403, null);
+            return;
+        }
+
+        final String path;
+        final Pattern pattern;
+        final Set<Integer> propertyTypes;
+        final long maxLength;
+        final int maxDepth;
+        try {
+            path = path(request);
+            pattern = pattern(request, this.pattern);
+            propertyTypes = propertyTypes(request, this.propertyTypes);
+            maxLength = maxLength(request, configuration.digger_default_property_length_max());
+            maxDepth = RequestUtil.maxDepth(request, configuration.digger_default_node_depth_max());
+        } catch (Exception e) {
+            response.sendError(400, e.getMessage());
+            return;
+        }
+
+        final Resource resource = request.getResourceResolver().getResource(path);
+        if (resource == null) {
+            response.sendError(400, "No resource at given path found: " + path);
+            return;
+        }
+
+        final Node node = resource.adaptTo(Node.class);
+        if (node == null) {
+            response.sendError(400, "Resource at given path is not a Node: " + path);
+            return;
+        }
+
+        try {
+            digger.dig(node, pattern, propertyTypes, maxLength, maxDepth);
+        } catch (Exception e) {
+            response.sendError(500, e.getMessage());
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/clam/http/internal/ClamScanServletConfiguration.java b/src/main/java/org/apache/sling/clam/http/internal/ClamScanServletConfiguration.java
new file mode 100644 (file)
index 0000000..9fcf4ec
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.clam.http.internal;
+
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.osgi.service.metatype.annotations.Option;
+
+@ObjectClassDefinition(
+    name = "Apache Sling Clam Scan Servlet",
+    description = "..."
+)
+@interface ClamScanServletConfiguration {
+
+    @AttributeDefinition(
+        name = "scan authorized groups",
+        description = "User groups authorized for scanning"
+    )
+    String[] scan_authorized_groups() default {"clam-scan"};
+
+    @AttributeDefinition(
+        name = "default property types",
+        description = "Type of properties",
+        options = {
+            @Option(label = "Binary", value = "Binary"),
+            @Option(label = "String", value = "String")
+        }
+    )
+    String[] digger_default_property_types() default {"Binary"};
+
+    @AttributeDefinition(
+        name = "default property path pattern",
+        description = "Pattern a property path has to match, e.g. '^/content/.*/jcr:content/jcr:data$'"
+    )
+    String digger_default_property_path_pattern() default "^/.*$";
+
+    @AttributeDefinition(
+        name = "default property length max",
+        description = "Max length of property value to scan, -1 for unlimited length. Scanning data greater 4GB may result in errors due to limitations in Clam."
+    )
+    long digger_default_property_length_max() default -1L;
+
+    @AttributeDefinition(
+        name = "default node depth max",
+        description = "Max depth of nodes below given path to scan, -1 for unlimited depth."
+    )
+    int digger_default_node_depth_max() default -1;
+
+}
diff --git a/src/main/java/org/apache/sling/clam/http/internal/RequestUtil.java b/src/main/java/org/apache/sling/clam/http/internal/RequestUtil.java
new file mode 100644 (file)
index 0000000..774a85c
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.clam.http.internal;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.jetbrains.annotations.NotNull;
+
+import static org.apache.sling.clam.internal.ClamUtil.propertyTypesFromNames;
+
+public class RequestUtil {
+
+    private RequestUtil() {
+    }
+
+    static String path(@NotNull final SlingHttpServletRequest request) throws Exception {
+        final String path = request.getParameter("path");
+        if (path == null) {
+            throw new Exception("Mandatory parameter path is missing");
+        } else {
+            return path;
+        }
+    }
+
+    static Pattern pattern(@NotNull final SlingHttpServletRequest request, @NotNull final Pattern pattern) throws Exception {
+        final String parameter = request.getParameter("pattern");
+        if (parameter == null) {
+            return pattern;
+        } else {
+            try {
+                return Pattern.compile(parameter);
+            } catch (Exception e) {
+                throw new Exception("Invalid parameter value for pattern: " + parameter);
+            }
+        }
+    }
+
+    static Set<Integer> propertyTypes(@NotNull final SlingHttpServletRequest request, @NotNull final Set<Integer> propertyTypes) throws Exception {
+        final String[] parameter = request.getParameterValues("propertyType");
+        if (parameter == null || parameter.length == 0) {
+            return propertyTypes;
+        }
+        try {
+            return propertyTypesFromNames(parameter);
+        } catch (Exception e) {
+            throw new Exception("Invalid parameter value for propertyType: " + Arrays.toString(parameter));
+        }
+    }
+
+    static long maxLength(@NotNull final SlingHttpServletRequest request, final long maxLength) throws Exception {
+        final String parameter = request.getParameter("maxLength");
+        if (parameter == null) {
+            return maxLength;
+        } else {
+            try {
+                return Long.parseLong(parameter);
+            } catch (Exception e) {
+                throw new Exception("Invalid parameter value for maxLength: " + parameter);
+            }
+        }
+    }
+
+    static int maxDepth(@NotNull final SlingHttpServletRequest request, final int maxDepth) throws Exception {
+        final String parameter = request.getParameter("maxDepth");
+        if (parameter == null) {
+            return maxDepth;
+        } else {
+            try {
+                return Integer.parseInt(parameter);
+            } catch (Exception e) {
+                throw new Exception("Invalid parameter value for maxDepth: " + parameter);
+            }
+        }
+    }
+
+    static boolean isAuthorized(@NotNull final SlingHttpServletRequest request, @NotNull final Collection<String> authorizedGroups) throws Exception {
+        final Authorizable authorizable = request.getResourceResolver().adaptTo(Authorizable.class);
+        if (authorizable == null) {
+            return false;
+        }
+        final Iterator<Group> groups = authorizable.memberOf();
+        while (groups.hasNext()) {
+            final String id = groups.next().getID();
+            if (authorizedGroups.contains(id)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/clam/http/internal/ResponseUtil.java b/src/main/java/org/apache/sling/clam/http/internal/ResponseUtil.java
new file mode 100644 (file)
index 0000000..606e458
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.clam.http.internal;
+
+import java.io.IOException;
+
+import javax.json.Json;
+import javax.json.JsonException;
+import javax.json.JsonObjectBuilder;
+import javax.servlet.ServletException;
+
+import org.apache.sling.api.SlingHttpServletResponse;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class ResponseUtil {
+
+    private ResponseUtil() {
+    }
+
+    static void handleError(@NotNull final SlingHttpServletResponse response, final int status, @Nullable final String message) throws ServletException, IOException {
+        try {
+            response.setCharacterEncoding("UTF-8");
+            response.setContentType("application/json");
+            response.setStatus(status);
+            if (message != null) {
+                final JsonObjectBuilder error = Json.createObjectBuilder();
+                error.add("message", message);
+                Json.createGenerator(response.getWriter()).write(error.build()).flush();
+            }
+        } catch (final JsonException e) {
+            throw new ServletException("Building response failed.");
+        }
+    }
+
+}