[SYNCOPE-1267] New binary Schema validator to check MIME types
authorskylark17 <matteo.alessandroni@tirasa.net>
Tue, 6 Feb 2018 14:51:35 +0000 (15:51 +0100)
committerskylark17 <matteo.alessandroni@tirasa.net>
Tue, 6 Feb 2018 14:51:35 +0000 (15:51 +0100)
14 files changed:
client/console/src/main/java/org/apache/syncope/client/console/panels/PlainSchemaDetails.java
core/persistence-jpa/pom.xml
core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/attrvalue/validation/BinaryValidator.java [new file with mode: 0644]
core/persistence-jpa/src/main/resources/domains/MasterContent.xml
core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/ImplementationTest.java
core/persistence-jpa/src/test/resources/domains/MasterContent.xml
core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java
fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/ITImplementationLookup.java
fit/core-reference/src/test/java/org/apache/syncope/fit/core/PlainSchemaITCase.java
fit/core-reference/src/test/resources/test.html [new file with mode: 0644]
fit/core-reference/src/test/resources/test.json [new file with mode: 0644]
fit/core-reference/src/test/resources/test.pdf [new file with mode: 0644]
fit/core-reference/src/test/resources/test.xml [new file with mode: 0644]
pom.xml

index e79c013..c996bcc 100644 (file)
@@ -67,6 +67,8 @@ public class PlainSchemaDetails extends AbstractSchemaDetailsPanel {
 
     private final MultiFieldPanel<String> enumerationKeys;
 
+    private final AjaxDropDownChoicePanel<String> validator;
+
     public PlainSchemaDetails(
             final String id,
             final PageReference pageReference,
@@ -233,6 +235,7 @@ public class PlainSchemaDetails extends AbstractSchemaDetailsPanel {
                         binaryParams, mimeType);
                 target.add(conversionParams);
                 target.add(typeParams);
+                target.add(validator);
             }
         }
         );
@@ -247,8 +250,9 @@ public class PlainSchemaDetails extends AbstractSchemaDetailsPanel {
                         map(EntityTO::getKey).sorted().collect(Collectors.toList());
             }
         };
-        final AjaxDropDownChoicePanel<String> validator = new AjaxDropDownChoicePanel<>("validator",
+        validator = new AjaxDropDownChoicePanel<>("validator",
                 getString("validator"), new PropertyModel<>(schemaTO, "validator"));
+        validator.setOutputMarkupId(true);
         ((DropDownChoice) validator.getField()).setNullValid(true);
         validator.setChoices(validators.getObject());
         schemaForm.add(validator);
@@ -330,6 +334,8 @@ public class PlainSchemaDetails extends AbstractSchemaDetailsPanel {
             binaryParams.setVisible(false);
             mimeType.setModelObject(null);
             mimeType.setChoices(null);
+
+            PlainSchemaTO.class.cast(schema).setValidator(null);
         } else if (AttrSchemaType.Enum.ordinal() == typeOrdinal) {
             conversionParams.setVisible(false);
             conversionPattern.setModelObject(null);
@@ -356,6 +362,8 @@ public class PlainSchemaDetails extends AbstractSchemaDetailsPanel {
             binaryParams.setVisible(false);
             mimeType.setModelObject(null);
             mimeType.setChoices(null);
+
+            PlainSchemaTO.class.cast(schema).setValidator(null);
         } else if (AttrSchemaType.Encrypted.ordinal() == typeOrdinal) {
             conversionParams.setVisible(false);
             conversionPattern.setModelObject(null);
@@ -378,6 +386,8 @@ public class PlainSchemaDetails extends AbstractSchemaDetailsPanel {
             binaryParams.setVisible(false);
             mimeType.setModelObject(null);
             mimeType.setChoices(null);
+
+            PlainSchemaTO.class.cast(schema).setValidator(null);
         } else if (AttrSchemaType.Binary.ordinal() == typeOrdinal) {
             conversionParams.setVisible(false);
             conversionPattern.setModelObject(null);
@@ -401,6 +411,8 @@ public class PlainSchemaDetails extends AbstractSchemaDetailsPanel {
 
             binaryParams.setVisible(true);
             mimeType.setChoices(MIME_TYPES_LOADER.getMimeTypes());
+
+            PlainSchemaTO.class.cast(schema).setValidator("BinaryValidator");
         } else {
             conversionParams.setVisible(false);
             conversionPattern.setModelObject(null);
@@ -425,6 +437,8 @@ public class PlainSchemaDetails extends AbstractSchemaDetailsPanel {
             binaryParams.setVisible(false);
             mimeType.setModelObject(null);
             mimeType.setChoices(null);
+
+            PlainSchemaTO.class.cast(schema).setValidator(null);
         }
     }
 }
index da3270d..545e6cb 100644 (file)
@@ -103,6 +103,11 @@ under the License.
       <artifactId>syncope-core-spring</artifactId>
       <version>${project.version}</version>
     </dependency>
+    
+    <dependency>
+      <groupId>org.apache.tika</groupId>
+      <artifactId>tika-core</artifactId>
+    </dependency>
         
     <!-- TEST -->
     <dependency> 
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/attrvalue/validation/BinaryValidator.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/attrvalue/validation/BinaryValidator.java
new file mode 100644 (file)
index 0000000..a7dbb8d
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * 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.syncope.core.persistence.jpa.attrvalue.validation;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import org.apache.syncope.core.persistence.api.attrvalue.validation.InvalidPlainAttrValueException;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
+import org.apache.tika.Tika;
+
+public class BinaryValidator extends AbstractValidator {
+
+    private static final long serialVersionUID = 1344152444666540361L;
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    @Override
+    protected void doValidate(final PlainAttrValue attrValue) {
+        // check Binary schemas MIME Type mismatches
+        if (attrValue.getBinaryValue() != null) {
+            PlainSchema currentSchema = attrValue.getAttr().getSchema();
+            byte[] binaryValue = attrValue.getBinaryValue();
+            String mimeType = detectSchemaMimeType(binaryValue);
+            boolean valid = true;
+            if (!mimeType.equals(currentSchema.getMimeType())) {
+                if (mimeType.equals("text/plain")
+                        && currentSchema.getMimeType().equals("application/json")) {
+                    String decoded = new String(binaryValue).trim();
+                    valid = (decoded.startsWith("{") && decoded.endsWith("}"))
+                            || (decoded.startsWith("[") && decoded.endsWith("]"))
+                            && isValidJSON(decoded);
+                } else {
+                    valid = false;
+                }
+            }
+            if (!valid) {
+                throw new InvalidPlainAttrValueException(
+                        "Found MIME type: '"
+                        + mimeType
+                        + "', expecting: '"
+                        + currentSchema.getMimeType()
+                        + "'");
+            }
+        }
+    }
+
+    private String detectSchemaMimeType(final byte[] value) {
+        Tika tika = new Tika();
+        tika.setMaxStringLength(-1);
+        return tika.detect(value);
+    }
+
+    private boolean isValidJSON(final String value) {
+        try {
+            MAPPER.readTree(value);
+            return true;
+        } catch (IOException e) {
+            return false;
+        }
+    }
+}
index 08e241b..4e33895 100644 (file)
@@ -184,6 +184,9 @@ under the License.
   <PlainSchema id="email" type="String" anyTypeClass_id="BaseUser"
                mandatoryCondition="false" multivalue="0" uniqueConstraint="0" readonly="0"
                validator_id="EmailAddressValidator"/>
+  
+  <Implementation id="BinaryValidator" type="VALIDATOR" engine="JAVA"
+                  body="org.apache.syncope.core.persistence.jpa.attrvalue.validation.BinaryValidator"/>
 
   <Implementation id="PullJobDelegate" type="TASKJOB_DELEGATE" engine="JAVA"
                   body="org.apache.syncope.core.provisioning.java.pushpull.PullJobDelegate"/>
index eea08c9..c0982e6 100644 (file)
@@ -43,7 +43,7 @@ public class ImplementationTest extends AbstractTest {
         List<Implementation> implementations = implementationDAO.findAll();
         assertFalse(implementations.isEmpty());
 
-        assertEquals(16, implementations.size());
+        assertEquals(17, implementations.size());
 
         implementations = implementationDAO.find(ImplementationType.PULL_ACTIONS);
         assertEquals(1, implementations.size());
@@ -64,7 +64,7 @@ public class ImplementationTest extends AbstractTest {
         assertEquals(3, implementations.size());
 
         implementations = implementationDAO.find(ImplementationType.VALIDATOR);
-        assertEquals(1, implementations.size());
+        assertEquals(2, implementations.size());
 
         implementations = implementationDAO.find(ImplementationType.PULL_CORRELATION_RULE);
         assertEquals(1, implementations.size());
index d8eb1be..ec749b7 100644 (file)
@@ -465,6 +465,9 @@ under the License.
   <PlainSchema id="photo" type="Binary" anyTypeClass_id="other"
                mandatoryCondition="false" multivalue="0" uniqueConstraint="0" readonly="0"
                mimeType="image/jpeg"/>
+  
+  <Implementation id="BinaryValidator" type="VALIDATOR" engine="JAVA"
+                  body="org.apache.syncope.core.persistence.jpa.attrvalue.validation.BinaryValidator"/>
 
   <SyncopeSchema id="csvuserid"/>
   <DerSchema id="csvuserid" expression="firstname + ',' + surname" anyTypeClass_id="csv"/>
index 0bc577a..97de7cd 100644 (file)
@@ -188,9 +188,12 @@ abstract class AbstractAnyDataBinder {
                 try {
                     attr.add(value, anyUtils);
                 } catch (InvalidPlainAttrValueException e) {
-                    LOG.warn("Invalid value for attribute " + schema.getKey() + ": " + value, e);
+                    String valueToPrint = value.length() > 40
+                            ? value.substring(0, 20) + "..."
+                            : value;
+                    LOG.warn("Invalid value for attribute " + schema.getKey() + ": " + valueToPrint, e);
 
-                    invalidValues.getElements().add(schema.getKey() + ": " + value + " - " + e.getMessage());
+                    invalidValues.getElements().add(schema.getKey() + ": " + valueToPrint + " - " + e.getMessage());
                 }
             }
         });
index d285ba6..f6ec448 100644 (file)
@@ -59,6 +59,7 @@ import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.Implementation;
 import org.apache.syncope.core.persistence.jpa.attrvalue.validation.AlwaysTrueValidator;
 import org.apache.syncope.core.persistence.jpa.attrvalue.validation.BasicValidator;
+import org.apache.syncope.core.persistence.jpa.attrvalue.validation.BinaryValidator;
 import org.apache.syncope.core.persistence.jpa.attrvalue.validation.EmailAddressValidator;
 import org.apache.syncope.core.persistence.jpa.dao.DefaultAccountRule;
 import org.apache.syncope.core.persistence.jpa.dao.DefaultPasswordRule;
@@ -199,6 +200,7 @@ public class ITImplementationLookup implements ImplementationLookup {
             classNames.add(BasicValidator.class.getName());
             classNames.add(EmailAddressValidator.class.getName());
             classNames.add(AlwaysTrueValidator.class.getName());
+            classNames.add(BinaryValidator.class.getName());
             put(ImplementationType.VALIDATOR, classNames);
 
             classNames = new HashSet<>();
index 79a31e9..395572f 100644 (file)
@@ -25,11 +25,15 @@ import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
+import java.io.IOException;
+import java.util.Base64;
 import java.util.List;
 import javax.ws.rs.core.GenericType;
 import javax.ws.rs.core.Response;
 import org.apache.commons.lang3.SerializationUtils;
 import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.patch.AttrPatch;
+import org.apache.syncope.common.lib.patch.UserPatch;
 import org.apache.syncope.common.lib.to.AnyTypeClassTO;
 import org.apache.syncope.common.lib.to.MembershipTO;
 import org.apache.syncope.common.lib.to.PlainSchemaTO;
@@ -39,10 +43,12 @@ import org.apache.syncope.common.lib.types.AttrSchemaType;
 import org.apache.syncope.common.lib.types.CipherAlgorithm;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.lib.types.EntityViolationType;
+import org.apache.syncope.common.lib.types.PatchOperation;
 import org.apache.syncope.common.lib.types.SchemaType;
 import org.apache.syncope.common.rest.api.beans.SchemaQuery;
 import org.apache.syncope.fit.AbstractITCase;
 import org.junit.jupiter.api.Test;
+import org.apache.cxf.helpers.IOUtils;
 
 public class PlainSchemaITCase extends AbstractITCase {
 
@@ -142,6 +148,95 @@ public class PlainSchemaITCase extends AbstractITCase {
     }
 
     @Test
+    public void testBinaryValidation() throws IOException {
+        // pdf - with validator
+        PlainSchemaTO schemaTOpdf = new PlainSchemaTO();
+        schemaTOpdf.setKey("BinaryPDF");
+        schemaTOpdf.setType(AttrSchemaType.Binary);
+        schemaTOpdf.setMimeType("application/pdf");
+        schemaTOpdf.setValidator("BinaryValidator");
+        schemaTOpdf.setAnyTypeClass("minimal user");
+
+        createSchema(SchemaType.PLAIN, schemaTOpdf);
+
+        // json - with validator
+        PlainSchemaTO schemaTOjson = new PlainSchemaTO();
+        schemaTOjson.setKey("BinaryJSON");
+        schemaTOjson.setType(AttrSchemaType.Binary);
+        schemaTOjson.setMimeType("application/json");
+        schemaTOjson.setValidator("BinaryValidator");
+        schemaTOjson.setAnyTypeClass("minimal user");
+
+        createSchema(SchemaType.PLAIN, schemaTOjson);
+
+        // json - no validator
+        PlainSchemaTO schemaTOjson2 = new PlainSchemaTO();
+        schemaTOjson2.setKey("BinaryJSON2");
+        schemaTOjson2.setType(AttrSchemaType.Binary);
+        schemaTOjson2.setMimeType("application/json");
+        schemaTOjson2.setAnyTypeClass("minimal user");
+
+        createSchema(SchemaType.PLAIN, schemaTOjson2);
+
+        UserTO userTO = UserITCase.getUniqueSampleTO("test@syncope.apache.org");
+
+        userTO = createUser(userTO).getEntity();
+        assertNotNull(userTO);
+
+        UserPatch userPatch = new UserPatch();
+        userPatch.setKey(userTO.getKey());
+        // validation OK - application/pdf -> application/pdf
+        userPatch.getPlainAttrs().add(new AttrPatch.Builder().operation(PatchOperation.ADD_REPLACE).
+                attrTO(attrTO("BinaryPDF",
+                        Base64.getEncoder().encodeToString(
+                                IOUtils.readBytesFromStream(getClass().getResourceAsStream("/test.pdf"))))).
+                build());
+
+        updateUser(userPatch);
+        assertNotNull(userService.read(userTO.getKey()).getPlainAttr("BinaryPDF"));
+
+        userPatch = new UserPatch();
+        userPatch.setKey(userTO.getKey());
+        // validation KO - text/html -> application/pdf
+        try {
+            userPatch.getPlainAttrs().add(new AttrPatch.Builder().operation(PatchOperation.ADD_REPLACE).
+                    attrTO(attrTO("BinaryPDF",
+                            Base64.getEncoder().encodeToString(
+                                    IOUtils.readBytesFromStream(getClass().getResourceAsStream("/test.html"))))).
+                    build());
+
+            updateUser(userPatch);
+            fail("This should not be reacheable");
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.InvalidValues, e.getType());
+        }
+
+        userPatch = new UserPatch();
+        userPatch.setKey(userTO.getKey());
+        // validation ok - application/json -> application/json
+        userPatch.getPlainAttrs().add(new AttrPatch.Builder().operation(PatchOperation.ADD_REPLACE).
+                attrTO(attrTO("BinaryJSON",
+                        Base64.getEncoder().encodeToString(
+                                IOUtils.readBytesFromStream(getClass().getResourceAsStream("/test.json"))))).
+                build());
+
+        updateUser(userPatch);
+        assertNotNull(userService.read(userTO.getKey()).getPlainAttr("BinaryJSON"));
+
+        userPatch = new UserPatch();
+        userPatch.setKey(userTO.getKey());
+        // no validation - application/xml -> application/json
+        userPatch.getPlainAttrs().add(new AttrPatch.Builder().operation(PatchOperation.ADD_REPLACE).
+                attrTO(attrTO("BinaryJSON2",
+                        Base64.getEncoder().encodeToString(
+                                IOUtils.readBytesFromStream(getClass().getResourceAsStream("/test.xml"))))).
+                build());
+
+        updateUser(userPatch);
+        assertNotNull(userService.read(userTO.getKey()).getPlainAttr("BinaryJSON2"));
+    }
+
+    @Test
     public void delete() {
         PlainSchemaTO schemaTO = buildPlainSchemaTO("todelete", AttrSchemaType.String);
         schemaTO.setMandatoryCondition("false");
diff --git a/fit/core-reference/src/test/resources/test.html b/fit/core-reference/src/test/resources/test.html
new file mode 100644 (file)
index 0000000..d55d628
--- /dev/null
@@ -0,0 +1,30 @@
+<!--
+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.
+-->
+<!doctype html>
+
+<head>
+  <meta charset="utf-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="description" content="">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+</head>
+<body>
+  <p> Syncope is Magic </p>
+</body>
+</html>
diff --git a/fit/core-reference/src/test/resources/test.json b/fit/core-reference/src/test/resources/test.json
new file mode 100644 (file)
index 0000000..fd88c8d
--- /dev/null
@@ -0,0 +1,3 @@
+{
+  "message": "Syncope is Magic"
+}
diff --git a/fit/core-reference/src/test/resources/test.pdf b/fit/core-reference/src/test/resources/test.pdf
new file mode 100644 (file)
index 0000000..bed69a7
Binary files /dev/null and b/fit/core-reference/src/test/resources/test.pdf differ
diff --git a/fit/core-reference/src/test/resources/test.xml b/fit/core-reference/src/test/resources/test.xml
new file mode 100644 (file)
index 0000000..66cad36
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<Tests xmlns="http://www.test.com">  
+  <Test>  
+    <Name>Syncope is Magic</Name>  
+  </Test>
+</Tests>  
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 6a476da..47752df 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -402,6 +402,8 @@ under the License.
     <commons-lang.version>3.7</commons-lang.version>
     <commons-text.version>1.2</commons-text.version>
     <commons-logging.version>1.1.3</commons-logging.version>
+    
+    <tika.version>1.17</tika.version>
 
     <joda.version>2.9.9</joda.version>
 
@@ -1487,6 +1489,12 @@ under the License.
         <artifactId>httpmime</artifactId>
         <version>${httpclient.version}</version>
       </dependency>
+      
+      <dependency>
+        <groupId>org.apache.tika</groupId>
+        <artifactId>tika-core</artifactId>
+        <version>${tika.version}</version>
+      </dependency>
 
       <dependency>
         <groupId>org.netbeans.api</groupId>