keycloak-aplcache

Merge pull request #3642 from mposolda/master KEYCLOAK-3921

12/13/2016 6:11:41 PM

Changes

Details

diff --git a/examples/ldap/ldap-example-users.ldif b/examples/ldap/ldap-example-users.ldif
index d503255..082ee53 100644
--- a/examples/ldap/ldap-example-users.ldif
+++ b/examples/ldap/ldap-example-users.ldif
@@ -45,6 +45,7 @@ postalCode: 77332
 postalCode: 66221
 street: Elm 5
 userPassword: password
+jpegPhoto:: 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
 
 dn: cn=ldap-user,ou=RealmRoles,dc=keycloak,dc=org
 objectclass: top
diff --git a/examples/ldap/ldaprealm.json b/examples/ldap/ldaprealm.json
index ef1e2ae..8a04b3e 100644
--- a/examples/ldap/ldaprealm.json
+++ b/examples/ldap/ldaprealm.json
@@ -119,6 +119,21 @@
             "id.token.claim" : "true",
             "access.token.claim" : "true"
           }
+        },
+        {
+          "protocolMapper" : "oidc-usermodel-attribute-mapper",
+          "protocol" : "openid-connect",
+          "name" : "picture",
+          "consentText" : "Picture",
+          "consentRequired" : true,
+          "config" : {
+            "Claim JSON Type" : "String",
+            "user.attribute" : "picture",
+            "claim.name" : "profile_picture",
+            "multivalued": "false",
+            "id.token.claim" : "true",
+            "access.token.claim" : "true"
+          }
         }
       ]
     }
@@ -248,6 +263,19 @@
       }
     },
     {
+      "name" : "picture",
+      "federationMapperType" : "user-attribute-ldap-mapper",
+      "federationProviderDisplayName" : "ldap-apacheds",
+      "config" : {
+        "ldap.attribute" : "jpegPhoto",
+        "user.model.attribute" : "picture",
+        "is.mandatory.in.ldap" : "false",
+        "read.only" : "false",
+        "always.read.value.from.ldap" : "true",
+        "is.binary.attribute" : "true"
+      }
+    },
+    {
       "name" : "realm roles",
       "federationMapperType" : "role-ldap-mapper",
       "federationProviderDisplayName" : "ldap-apacheds",
diff --git a/examples/ldap/src/main/java/org/keycloak/example/ldap/LDAPPictureServlet.java b/examples/ldap/src/main/java/org/keycloak/example/ldap/LDAPPictureServlet.java
new file mode 100644
index 0000000..3ca7856
--- /dev/null
+++ b/examples/ldap/src/main/java/org/keycloak/example/ldap/LDAPPictureServlet.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed 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.keycloak.example.ldap;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.common.util.Base64;
+import org.keycloak.representations.IDToken;
+
+/**
+ * Tests binary LDAP attribute
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LDAPPictureServlet extends HttpServlet {
+
+    @Override
+    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        resp.setContentType("image/jpeg");
+        ServletOutputStream outputStream = resp.getOutputStream();
+
+        KeycloakSecurityContext securityContext = (KeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName());
+        IDToken idToken = securityContext.getIdToken();
+
+        // TODO: Use idToken.getPicture() instead
+        Object profilePicture = idToken.getOtherClaims().get("profile_picture");
+
+        if (profilePicture != null) {
+            String base64EncodedPicture = getBase64EncodedPicture(profilePicture);
+            byte[] decodedPicture = Base64.decode(base64EncodedPicture);
+            outputStream.write(decodedPicture);
+        }
+
+        outputStream.flush();
+    }
+
+    private String getBase64EncodedPicture(Object profilePicture) {
+        if (profilePicture instanceof List) {
+            return ((List) profilePicture).get(0).toString();
+        } else {
+            return profilePicture.toString();
+        }
+    }
+}
diff --git a/examples/ldap/src/main/webapp/index.jsp b/examples/ldap/src/main/webapp/index.jsp
index a40c382..1156049 100644
--- a/examples/ldap/src/main/webapp/index.jsp
+++ b/examples/ldap/src/main/webapp/index.jsp
@@ -43,9 +43,15 @@
         <h2>ID Token - other claims</h2>
 <%
     for (Map.Entry<String, Object> claim : idToken.getOtherClaims().entrySet()) {
+        if (!claim.getKey().equals("profile_picture")) {
 %>
         <p><b><%= claim.getKey() %>: </b><%= claim.getValue().toString() %>
 <%
+        } else {
+%>
+        <p><b>Profile picture: </b><img src="/ldap-portal/picture" />
+<%
+        }
     }
 %>
         <hr />
diff --git a/examples/ldap/src/main/webapp/WEB-INF/web.xml b/examples/ldap/src/main/webapp/WEB-INF/web.xml
index 23d8896..64f703a 100644
--- a/examples/ldap/src/main/webapp/WEB-INF/web.xml
+++ b/examples/ldap/src/main/webapp/WEB-INF/web.xml
@@ -23,6 +23,16 @@
 
     <module-name>ldap-portal</module-name>
 
+    <servlet>
+        <servlet-name>Picture</servlet-name>
+        <servlet-class>org.keycloak.example.ldap.LDAPPictureServlet</servlet-class>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>Picture</servlet-name>
+        <url-pattern>/picture/*</url-pattern>
+    </servlet-mapping>
+
     <security-constraint>
         <web-resource-collection>
             <web-resource-name>LDAPApp</web-resource-name>
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java
index b36c17c..fec84b2 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java
@@ -18,6 +18,7 @@
 package org.keycloak.storage.ldap.idm.store.ldap;
 
 import org.jboss.logging.Logger;
+import org.keycloak.common.util.Base64;
 import org.keycloak.models.LDAPConstants;
 import org.keycloak.models.ModelException;
 import org.keycloak.storage.ldap.LDAPConfig;
@@ -40,6 +41,8 @@ import javax.naming.directory.DirContext;
 import javax.naming.directory.ModificationItem;
 import javax.naming.directory.SearchControls;
 import javax.naming.directory.SearchResult;
+
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -323,8 +326,15 @@ public class LDAPIdentityStore implements IdentityStore {
                     Set<String> attrValues = new LinkedHashSet<>();
                     NamingEnumeration<?> enumm = ldapAttribute.getAll();
                     while (enumm.hasMoreElements()) {
-                        String attrVal = enumm.next().toString().trim();
-                        attrValues.add(attrVal);
+                        Object val = enumm.next();
+
+                        if (val instanceof byte[]) { // byte[]
+                            String attrVal = Base64.encodeBytes((byte[]) val);
+                            attrValues.add(attrVal);
+                        } else { // String
+                            String attrVal = val.toString().trim();
+                            attrValues.add(attrVal);
+                        }
                     }
 
                     if (ldapAttributeName.equalsIgnoreCase(LDAPConstants.OBJECT_CLASS)) {
@@ -377,7 +387,18 @@ public class LDAPIdentityStore implements IdentityStore {
                     if (val == null || val.toString().trim().length() == 0) {
                         val = LDAPConstants.EMPTY_ATTRIBUTE_VALUE;
                     }
-                    attr.add(val);
+
+                    if (getConfig().getBinaryAttributeNames().contains(attrName)) {
+                        // Binary attribute
+                        try {
+                            byte[] bytes = Base64.decode(val);
+                            attr.add(bytes);
+                        } catch (IOException ioe) {
+                            logger.warnf("Wasn't able to Base64 decode the attribute value. Ignoring attribute update. LDAP DN: %s, Attribute: %s, Attribute value: %s" + ldapObject.getDn(), attrName, attrValue);
+                        }
+                    } else {
+                        attr.add(val);
+                    }
                 }
 
                 entryAttributes.put(attr);
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java
index cb58a1e..cf421e7 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java
@@ -515,8 +515,17 @@ public class LDAPOperationManager {
             }
         }
 
+        StringBuilder binaryAttrsBuilder = new StringBuilder();
         if (this.config.isObjectGUID()) {
-            env.put("java.naming.ldap.attributes.binary", LDAPConstants.OBJECT_GUID);
+            binaryAttrsBuilder.append(LDAPConstants.OBJECT_GUID).append(" ");
+        }
+        for (String attrName : config.getBinaryAttributeNames()) {
+            binaryAttrsBuilder.append(attrName).append(" ");
+        }
+
+        String binaryAttrs = binaryAttrsBuilder.toString().trim();
+        if (!binaryAttrs.isEmpty()) {
+            env.put("java.naming.ldap.attributes.binary", binaryAttrs);
         }
 
         if (logger.isDebugEnabled()) {
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java
index 930837d..ee2a89f 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java
@@ -24,6 +24,7 @@ import org.keycloak.storage.UserStorageProvider;
 import javax.naming.directory.SearchControls;
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Properties;
 import java.util.Set;
 
@@ -34,6 +35,7 @@ import java.util.Set;
 public class LDAPConfig {
 
     private final MultivaluedHashMap<String, String> config;
+    private final Set<String> binaryAttributeNames = new HashSet<>();
 
     public LDAPConfig(MultivaluedHashMap<String, String> config) {
         this.config = config;
@@ -184,4 +186,39 @@ public class LDAPConfig {
             return UserStorageProvider.EditMode.valueOf(editModeString);
         }
     }
+
+    public void addBinaryAttribute(String attrName) {
+        binaryAttributeNames.add(attrName);
+    }
+
+    public Set<String> getBinaryAttributeNames() {
+        return binaryAttributeNames;
+    }
+
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) return true;
+        if (!(obj instanceof LDAPConfig)) return false;
+
+        LDAPConfig that = (LDAPConfig) obj;
+
+        if (!config.equals(that.config)) return false;
+        if (!binaryAttributeNames.equals(that.binaryAttributeNames)) return false;
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return config.hashCode() * 13 + binaryAttributeNames.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        MultivaluedHashMap<String, String> copy = new MultivaluedHashMap<String, String>(config);
+        copy.remove(LDAPConstants.BIND_CREDENTIAL);
+        return new StringBuilder(copy.toString())
+                .append(", binaryAttributes: ").append(binaryAttributeNames)
+                .toString();
+    }
 }
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPIdentityStoreRegistry.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPIdentityStoreRegistry.java
index 7dc3086..67d03f2 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPIdentityStoreRegistry.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPIdentityStoreRegistry.java
@@ -20,8 +20,8 @@ package org.keycloak.storage.ldap;
 import org.jboss.logging.Logger;
 import org.keycloak.common.util.MultivaluedHashMap;
 import org.keycloak.component.ComponentModel;
-import org.keycloak.models.LDAPConstants;
 import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
+import org.keycloak.storage.ldap.mappers.LDAPConfigDecorator;
 
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
@@ -33,37 +33,40 @@ public class LDAPIdentityStoreRegistry {
 
     private static final Logger logger = Logger.getLogger(LDAPIdentityStoreRegistry.class);
 
-    private Map<String, LDAPIdentityStoreContext> ldapStores = new ConcurrentHashMap<String, LDAPIdentityStoreContext>();
+    private Map<String, LDAPIdentityStoreContext> ldapStores = new ConcurrentHashMap<>();
 
-    public LDAPIdentityStore getLdapStore(ComponentModel model) {
-        LDAPIdentityStoreContext context = ldapStores.get(model.getId());
+    public LDAPIdentityStore getLdapStore(ComponentModel ldapModel, Map<ComponentModel, LDAPConfigDecorator> configDecorators) {
+        LDAPIdentityStoreContext context = ldapStores.get(ldapModel.getId());
 
         // Ldap config might have changed for the realm. In this case, we must re-initialize
-        MultivaluedHashMap<String, String> config = model.getConfig();
-        if (context == null || !config.equals(context.config)) {
-            logLDAPConfig(model.getName(), config);
+        MultivaluedHashMap<String, String> configModel = ldapModel.getConfig();
+        LDAPConfig ldapConfig = new LDAPConfig(configModel);
+        for (Map.Entry<ComponentModel, LDAPConfigDecorator> entry : configDecorators.entrySet()) {
+            ComponentModel mapperModel = entry.getKey();
+            LDAPConfigDecorator decorator = entry.getValue();
 
-            LDAPIdentityStore store = createLdapIdentityStore(config);
-            context = new LDAPIdentityStoreContext(config, store);
-            ldapStores.put(model.getId(), context);
+            decorator.updateLDAPConfig(ldapConfig, mapperModel);
+        }
+
+        if (context == null || !ldapConfig.equals(context.config)) {
+            logLDAPConfig(ldapModel.getName(), ldapConfig);
+
+            LDAPIdentityStore store = createLdapIdentityStore(ldapConfig);
+            context = new LDAPIdentityStoreContext(ldapConfig, store);
+            ldapStores.put(ldapModel.getId(), context);
         }
         return context.store;
     }
 
     // Don't log LDAP password
-    private void logLDAPConfig(String fedProviderDisplayName, MultivaluedHashMap<String, String> ldapConfig) {
-        MultivaluedHashMap<String, String> copy = new MultivaluedHashMap<String, String>(ldapConfig);
-        copy.remove(LDAPConstants.BIND_CREDENTIAL);
-        logger.infof("Creating new LDAP based partition manager for the Federation provider: " + fedProviderDisplayName + ", LDAP Configuration: " + copy);
+    private void logLDAPConfig(String fedProviderDisplayName, LDAPConfig ldapConfig) {
+        logger.infof("Creating new LDAP Store for the LDAP storage provider: '%s', LDAP Configuration: %s", fedProviderDisplayName, ldapConfig.toString());
     }
 
     /**
-     * @param ldapConfig from realm
-     * @return PartitionManager instance based on LDAP store
+     * Create LDAPIdentityStore to be cached in the local registry
      */
-    public static LDAPIdentityStore createLdapIdentityStore(MultivaluedHashMap<String, String> ldapConfig) {
-        LDAPConfig cfg = new LDAPConfig(ldapConfig);
-
+    public static LDAPIdentityStore createLdapIdentityStore(LDAPConfig cfg) {
         checkSystemProperty("com.sun.jndi.ldap.connect.pool.authentication", "none simple");
         checkSystemProperty("com.sun.jndi.ldap.connect.pool.initsize", "1");
         checkSystemProperty("com.sun.jndi.ldap.connect.pool.maxsize", "1000");
@@ -84,12 +87,12 @@ public class LDAPIdentityStoreRegistry {
 
     private class LDAPIdentityStoreContext {
 
-        private LDAPIdentityStoreContext(MultivaluedHashMap<String, String> config, LDAPIdentityStore store) {
+        private LDAPIdentityStoreContext(LDAPConfig config, LDAPIdentityStore store) {
             this.config = config;
             this.store = store;
         }
 
-        private MultivaluedHashMap<String, String> config;
+        private LDAPConfig config;
         private LDAPIdentityStore store;
     }
 }
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java
index 051ed55..d8d0497 100755
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java
@@ -48,7 +48,9 @@ import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
 import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
 import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
 import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory;
+import org.keycloak.storage.ldap.mappers.LDAPConfigDecorator;
 import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.LDAPStorageMapperFactory;
 import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper;
 import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory;
 import org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapperFactory;
@@ -57,7 +59,9 @@ import org.keycloak.storage.user.SynchronizationResult;
 import org.keycloak.utils.CredentialHelper;
 
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -179,10 +183,30 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
 
     @Override
     public LDAPStorageProvider create(KeycloakSession session, ComponentModel model) {
-        LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(model);
+        Map<ComponentModel, LDAPConfigDecorator> configDecorators = getLDAPConfigDecorators(session, model);
+
+        LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(model, configDecorators);
         return new LDAPStorageProvider(this, session, model, ldapIdentityStore);
     }
 
+
+    // Check if it's some performance overhead to create this map in every request. But probably not...
+    protected Map<ComponentModel, LDAPConfigDecorator> getLDAPConfigDecorators(KeycloakSession session, ComponentModel ldapModel) {
+        RealmModel realm = session.realms().getRealm(ldapModel.getParentId());
+        List<ComponentModel> mapperComponents = realm.getComponents(ldapModel.getId(), LDAPStorageMapper.class.getName());
+
+        Map<ComponentModel, LDAPConfigDecorator> result = new HashMap<>();
+        for (ComponentModel mapperModel : mapperComponents) {
+            LDAPStorageMapperFactory mapperFactory = (LDAPStorageMapperFactory) session.getKeycloakSessionFactory().getProviderFactory(LDAPStorageMapper.class, mapperModel.getProviderId());
+            if (mapperFactory instanceof LDAPConfigDecorator) {
+                result.put(mapperModel, (LDAPConfigDecorator) mapperFactory);
+            }
+        }
+
+        return result;
+    }
+
+
     @Override
     public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
         LDAPConfig cfg = new LDAPConfig(config.getConfig());
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPConfigDecorator.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPConfigDecorator.java
new file mode 100644
index 0000000..0da6df1
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPConfigDecorator.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed 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.keycloak.storage.ldap.mappers;
+
+import org.keycloak.component.ComponentModel;
+import org.keycloak.storage.ldap.LDAPConfig;
+
+/**
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface LDAPConfigDecorator {
+
+    void updateLDAPConfig(LDAPConfig ldapConfig, ComponentModel mapperModel);
+
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java
index 3b29091..900969c 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java
@@ -80,6 +80,7 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
     public static final String READ_ONLY = "read.only";
     public static final String ALWAYS_READ_VALUE_FROM_LDAP = "always.read.value.from.ldap";
     public static final String IS_MANDATORY_IN_LDAP = "is.mandatory.in.ldap";
+    public static final String IS_BINARY_ATTRIBUTE = "is.binary.attribute";
 
     public UserAttributeLDAPStorageMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider) {
         super(mapperModel, ldapProvider);
@@ -90,6 +91,12 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
         String userModelAttrName = mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
         String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
 
+        // We won't update binary attributes to Keycloak DB. They might be too big
+        boolean isBinaryAttribute = mapperModel.get(IS_BINARY_ATTRIBUTE, false);
+        if (isBinaryAttribute) {
+            return;
+        }
+
         Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
 
         if (userModelProperty != null) {
@@ -177,6 +184,7 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
         final String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
         boolean isAlwaysReadValueFromLDAP = parseBooleanParameter(mapperModel, ALWAYS_READ_VALUE_FROM_LDAP);
         final boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
+        final boolean isBinaryAttribute = parseBooleanParameter(mapperModel, IS_BINARY_ATTRIBUTE);
 
         // For writable mode, we want to propagate writing of attribute to LDAP as well
         if (ldapProvider.getEditMode() == UserStorageProvider.EditMode.WRITABLE && !isReadOnly()) {
@@ -185,20 +193,23 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
 
                 @Override
                 public void setSingleAttribute(String name, String value) {
-                    setLDAPAttribute(name, value);
-                    super.setSingleAttribute(name, value);
+                    if (setLDAPAttribute(name, value)) {
+                        super.setSingleAttribute(name, value);
+                    }
                 }
 
                 @Override
                 public void setAttribute(String name, List<String> values) {
-                    setLDAPAttribute(name, values);
-                    super.setAttribute(name, values);
+                    if (setLDAPAttribute(name, values)) {
+                        super.setAttribute(name, values);
+                    }
                 }
 
                 @Override
                 public void removeAttribute(String name) {
-                    setLDAPAttribute(name, null);
-                    super.removeAttribute(name);
+                    if ( setLDAPAttribute(name, null)) {
+                        super.removeAttribute(name);
+                    }
                 }
 
                 @Override
@@ -221,10 +232,10 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
                     super.setFirstName(firstName);
                 }
 
-                protected void setLDAPAttribute(String modelAttrName, Object value) {
+                protected boolean setLDAPAttribute(String modelAttrName, Object value) {
                     if (modelAttrName.equalsIgnoreCase(userModelAttrName)) {
-                        if (logger.isTraceEnabled()) {
-                            logger.tracef("Pushing user attribute to LDAP. username: %s, Model attribute name: %s, LDAP attribute name: %s, Attribute value: %s", getUsername(), modelAttrName, ldapAttrName, value);
+                        if (UserAttributeLDAPStorageMapper.logger.isTraceEnabled()) {
+                            UserAttributeLDAPStorageMapper.logger.tracef("Pushing user attribute to LDAP. username: %s, Model attribute name: %s, LDAP attribute name: %s, Attribute value: %s", getUsername(), modelAttrName, ldapAttrName, value);
                         }
 
                         ensureTransactionStarted();
@@ -245,7 +256,53 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
                                 ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<>(asList));
                             }
                         }
+
+                        if (isBinaryAttribute) {
+                            UserAttributeLDAPStorageMapper.logger.debugf("Skip writing model attribute '%s' to DB for user '%s' as it is mapped to binary LDAP attribute.", userModelAttrName, getUsername());
+                            return false;
+                        } else {
+                            return true;
+                        }
                     }
+
+                    return true;
+                }
+
+            };
+
+        } else if (isBinaryAttribute) {
+
+            delegate = new UserModelDelegate(delegate) {
+
+                @Override
+                public void setSingleAttribute(String name, String value) {
+                    if (name.equalsIgnoreCase(userModelAttrName)) {
+                        logSkipDBWrite();
+                    } else {
+                        super.setSingleAttribute(name, value);
+                    }
+                }
+
+                @Override
+                public void setAttribute(String name, List<String> values) {
+                    if (name.equalsIgnoreCase(userModelAttrName)) {
+                        logSkipDBWrite();
+                    } else {
+                        super.setAttribute(name, values);
+                    }
+                }
+
+                @Override
+                public void removeAttribute(String name) {
+                    if (name.equalsIgnoreCase(userModelAttrName)) {
+                        logSkipDBWrite();
+                    } else {
+                        super.removeAttribute(name);
+                    }
+                }
+
+                private void logSkipDBWrite() {
+                    logger.debugf("Skip writing model attribute '%s' to DB for user '%s' as it is mapped to binary LDAP attribute", userModelAttrName, getUsername());
                 }
 
             };
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapperFactory.java
index fcc5525..eece55b 100755
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapperFactory.java
@@ -32,7 +32,7 @@ import java.util.List;
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
-public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFactory {
+public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFactory implements LDAPConfigDecorator {
 
     public static final String PROVIDER_ID = "user-attribute-ldap-mapper";
     protected static final List<ProviderConfigProperty> configProperties;
@@ -69,6 +69,10 @@ public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMa
                                .helpText("If true, attribute is mandatory in LDAP. Hence if there is no value in Keycloak DB, the empty value will be set to be propagated to LDAP")
                                .type(ProviderConfigProperty.BOOLEAN_TYPE)
                                .defaultValue("false").add()
+                    .property().name(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE).label("Is Binary Attribute")
+                                .helpText("Should be true for binary LDAP attributes")
+                                .type(ProviderConfigProperty.BOOLEAN_TYPE)
+                                .defaultValue("false").add()
                     .build();
     }
 
@@ -92,6 +96,12 @@ public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMa
         checkMandatoryConfigAttribute(UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, "User Model Attribute", config);
         checkMandatoryConfigAttribute(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, "LDAP Attribute", config);
 
+        boolean isBinaryAttribute = config.get(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE, false);
+        boolean alwaysReadValueFromLDAP = config.get(UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, false);
+        if (isBinaryAttribute && !alwaysReadValueFromLDAP) {
+            throw new ComponentValidationException("With Binary attribute enabled, the ''Always read value from LDAP'' must be enabled too");
+        }
+
     }
 
     @Override
@@ -103,4 +113,14 @@ public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMa
     public List<ProviderConfigProperty> getConfigProperties(RealmModel realm, ComponentModel parent) {
         return getConfigProps(parent);
     }
+
+
+    @Override
+    public void updateLDAPConfig(LDAPConfig ldapConfig, ComponentModel mapperModel) {
+        boolean isBinaryAttribute = mapperModel.get(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE, false);
+        if (isBinaryAttribute) {
+            String ldapAttrName = mapperModel.getConfig().getFirst(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE);
+            ldapConfig.addBinaryAttribute(ldapAttrName);
+        }
+    }
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/models/LDAPConstants.java b/server-spi-private/src/main/java/org/keycloak/models/LDAPConstants.java
index 08be1c1..7bef0cb 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/LDAPConstants.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/LDAPConstants.java
@@ -88,6 +88,7 @@ public class LDAPConstants {
     public static final String OBJECT_CLASS = "objectclass";
     public static final String UID = "uid";
     public static final String USER_PASSWORD_ATTRIBUTE = "userpassword";
+    public static final String JPEG_PHOTO = "jpegPhoto";
     public static final String GROUP = "group";
     public static final String GROUP_OF_NAMES = "groupOfNames";
     public static final String GROUP_OF_ENTRIES = "groupOfEntries";
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index c866ac9..98d6641 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -182,8 +182,10 @@ public class UsersResource {
         } catch (ModelReadOnlyException re) {
             return ErrorResponse.exists("User is read only!");
         } catch (ModelException me) {
+            logger.warn("Could not update user!", me);
             return ErrorResponse.exists("Could not update user!");
-        } catch (Exception me) { // JPA may be committed by JTA which can't 
+        } catch (Exception me) { // JPA
+            logger.warn("Could not update user!", me);// may be committed by JTA which can't
             return ErrorResponse.exists("Could not update user!");
         }
     }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPBinaryAttributesTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPBinaryAttributesTest.java
new file mode 100644
index 0000000..68e98cf
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPBinaryAttributesTest.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed 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.keycloak.testsuite.federation.storage.ldap;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import javax.ws.rs.ClientErrorException;
+import javax.ws.rs.core.Response;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runners.MethodSorters;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.component.ComponentValidationException;
+import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.UserModelDelegate;
+import org.keycloak.representations.idm.ComponentRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.ldap.LDAPStorageProvider;
+import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
+import org.keycloak.storage.ldap.LDAPUtils;
+import org.keycloak.storage.ldap.idm.model.LDAPObject;
+import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.LDAPRule;
+
+import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MASTER;
+import static org.keycloak.models.AdminRoles.ADMIN;
+import static org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class LDAPBinaryAttributesTest {
+
+    private static LDAPRule ldapRule = new LDAPRule();
+
+    private static ComponentModel ldapModel = null;
+
+
+    private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+        @Override
+        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+            MultivaluedHashMap<String,String> ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule);
+            ldapConfig.putSingle(LDAPConstants.SYNC_REGISTRATIONS, "true");
+            ldapConfig.putSingle(LDAPConstants.EDIT_MODE, UserStorageProvider.EditMode.WRITABLE.toString());
+            UserStorageProviderModel model = new UserStorageProviderModel();
+            model.setLastSync(0);
+            model.setChangedSyncPeriod(-1);
+            model.setFullSyncPeriod(-1);
+            model.setName("test-ldap");
+            model.setPriority(0);
+            model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME);
+            model.setConfig(ldapConfig);
+
+            ldapModel = appRealm.addComponentModel(model);
+            LDAPTestUtils.addZipCodeLDAPMapper(appRealm, ldapModel);
+
+            // Delete all LDAP users and add some new for testing
+            LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm);
+
+//            LDAPObject john = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
+//            LDAPTestUtils.updateLDAPPassword(ldapFedProvider, john, "Password1");
+//
+//            LDAPObject existing = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "existing", "Existing", "Foo", "existing@email.org", null, "5678");
+
+            appRealm.getClientByClientId("test-app").setDirectAccessGrantsEnabled(true);
+        }
+    });
+
+    @ClassRule
+    public static TestRule chain = RuleChain
+            .outerRule(ldapRule)
+            .around(keycloakRule);
+
+    private static final String JPEG_PHOTO_BASE64 = "/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMDAwMDAwQEBAQFBQUFBQcHBgYHBwsICQgJCAsRCwwLCwwLEQ8SDw4PEg8bFRMTFRsfGhkaHyYiIiYwLTA+PlQBAwMDAwMDBAQEBAUFBQUFBwcGBgcHCwgJCAkICxELDAsLDAsRDxIPDg8SDxsVExMVGx8aGRofJiIiJjAtMD4+VP/CABEIAFIAWAMBIgACEQEDEQH/xAAdAAACAgMBAQEAAAAAAAAAAAAGCAUHAwQJAgAB/9oACAEBAAAAAElmzK1aOaraUmpiktrD10DayAIMkKunPQdk+hrZwkUNkMrM88VDtt7r7C1KCJtprbSBP2J6K+VDqUlwErkDnOm/HF9IPDaWQ+cP85sXS7OCj1Iybjj2zVvJA0b91BqJ1JuQDYfkuSWrGdFzsMsWkaIUGHh4IuY1glqtWrK8G89YeJk+ldfT1pHz9//EABoBAAIDAQEAAAAAAAAAAAAAAAUHAwYIBAD/2gAIAQIQAAAApKICefJOj5haleI6vyhC1+FEa9UydaFaD7hDLtU9jttBsCJni6f/xAAaAQADAQEBAQAAAAAAAAAAAAAFBgcDBAII/9oACAEDEAAAAF5+Mdc4X6LUuz2kyGquiYYI/PzvUctR8d5289ghjS48fuOK/wD/xAA5EAABAwIEBAMGAwcFAAAAAAABAgMEABEFBhIhEzFBURQiMgdCYXGBwSOhsRUkM3KCkdFSU2J04f/aAAgBAQABPwGHLbkIVbmjmPhSF60BSL2VUPDhLYLjL4KuotyP0NQPZq5iWBmUiWPGFarIV/DIHS/MGpeC4lhrnh5MdxLgJBuna/wPWsFyW2/kVaH2/wB7dK5LSyPMDbb6KtWD+zqBKypKTIZQJ89q4dPNG10f+1GydizuAPSSgstwlHiawQVnVby/LrTrfBOkqF6gyWmEJQSoKvfVYUiawVbrubc6bQEr4qSkKpDj/iEMKSdLoDYSm/XkRWSco+Bg4dORxo8lCyJcd1NgqxI68tjSG2Wm/wAFKUi97DYVi2GRcZh8F+9goLSR0IpC02TakqA2rE4acSgPw9WgPJ0qPYE71mL2fQJLanYiOGmJh5DKEep125IualtvxJDrDo0OtLKFjsQd6U8tJ2686deDa/Ii1/STvWU8KwDOOVoaZ8VPioV2VLQdC0WNwQRSU2QEk3sNyetPExzq9zr8KEwpulXTrTWJx0OLQp5IOskC/Sm5iFpSpCr3A/S9IeF9zQJPI2r2g5Tw3AWFS0uPOPzpay3q9KE8zc9Tvzpxv6HrWUcCh5hnmC/iQiv6bskp18XvbcVlXJE3K0xbrOJJfadFnGi2U/UG5peocqxPEmYrCtak3I2FTcckBwp1q9PIck87Hbn3oLllYWTvzvUbFpkE7XIv1+FYFizOIM60W18inqmmeIrnXtTaxmW82p2I61CYFkL2Ui6upI5XrhKS0nWL6VadQ95J7VkzKcZzEor4lsHgkL4KwSrWO1ulJdWbeUHbcjaluqAPkX+VZjxMLkur07Nk7cjYdjyvTSzJcceWLFe4+9Mt6x5eQNOBCiR8enSsqylxcSLSdR1g7X273pmVYedJT9Kx7L+G5mjBp911sp9KkqOx+KTsazDl+Rl3EBHcktvNKuptSFbEfEdCKyEGHNi+OKhZUGFNg/1JJ3BoSU99/lTuzZUs2FutZhaSZT5SggJVcG29r9bU1p8OhQuRalyHgBa4TyriWTfqayjEck4kp1CiA0j63VyplTqPKtQV89qnfsl5jhzyzw1/7igP7XrPOF4fgmMNrg4ozIjSFH8BCwtbI7E9u1ZCwvCpjrcyRKQHW3PLG1WJ+N/8U65o3sBSWCs8R3c9B2rN2BGWy7ISQDpsRb86SuRC1IlNLQb9ep+B60ZUbYnaokOXijwaYQdJ5q90bd6wDBF4JESlJC7+Zdhvc00W3htWbZmHYVhi3J8PxUVR0rb06t+l+3zrFXMLU6VQUvJJcWVBQ0pSOiUi6jt3Jpryea52r2c+KxKap9995TEVvyjWbFajtfvWsL+VKW1iBJQoKZbV03uof4qThEWWyriNJWee++9R8uYTH06YrYIH+kU9BaCNKU2t6aw93iJ0KO6axfFIOFSW1OSEsFwn1GwJHOsdbYzhlybEgy21ugC4QoG+nfSfnU6MAtY9JR7vfferqPQmsOzTOw9bDMdTLZTfhtcklZFrnufnWLe0HM+JLMbi+FSfIpDW3zuedZXQzGyxh/IDw4Wf6/Mo1h8pqbCaktnyOi6T8L1PxlqFj0OE5sJLK7fzDkKOlbYVWM4yzl7FY63dmJKTqI90o62r2pQXnDExpg8aK4gNmytkHoR8DTMx6MVLYdW2u1uo/NNL1JSt3UFq08qCVN+pR2pS9SiR32oyPElp824iLIc/5dlUjOstvLyMKCAAEFBdJ9y/Kms2QMNyvBjofC3ww2opQb8/MAfvWPY/IxzFvFqUW9B/BsfSByFSfaGpzAm+E4WZrTyF2725/Q9qx7NT+ZHY7i20t8JrTpBvc33NSJkzgmKH3THJ1cLUdN/lQTtvUSO2664FXA0LWr6C9KJJNConrP8AIaRzXTPo/t+tO/f71J9X0H6U1T1L+1QNncQ/6n3Ff//EACQQAQEAAgICAQQDAQAAAAAAAAERACExQVGBYXGRobEQwfDR/9oACAEBAAE/EFxCJHa+QcvGUwhESkN9fiYHv0VpOm0o9OFKc/OP0nyMqyNS7O0x2FTnAH6HHF+oMkQP6cPbVCpqdHusdzBsTjjKJKrAZwN411hOmhN3Q+OdYFicQWN6QTJzrwEHC7aJwbcdwysS/AW7GNhVpxF5hwesFHQhHh77IyY3UAIHQHgwuHXP36x4apQUBQcWcXJmcwJakbelduaVYQKsBS3eAVJyB+cQnvn1w6o9ZDI6pnUIiPfdxG9iCBQRXreNy18f2PjFCeB0fH3MMVGwtGz1hMB2JKPunWGBO9cLh1omlDTJZj6GlAPSm3RMCgLAgSfWedY3okyFyEYQWneraZLkQPZNfjHeyDpVdBP7dYuNwjIibANNDHmgaDAeXRptkBMDpGVE6Hqjvltx4aCVkJq9o4gOA4y6pRz7bXiMrUKoBArI5ZdacH0tDvKBFDsjg3oRL8G/tceEiW1Y6UAXW0jxK47AXAL0gOg3HkF1MeStC2JfnHWoCStw/wDe804AlIhCOmmMAu7jZ+Fw0B2U6H2hTGAuDJHQLuYV1xK4W0jhBExMKEcCV+5m01yqkMRn8VrZVUN24oNBOeYF1eI6cEGRpvfJrB+GQLVd9ZIRJROhHod4eCYNA8PTGhag8kNPJivYOADUtVxNVsC44dbHxjhJe2AYOXuc16+cQODu1ej3TF3gwCyreivI5JUksX4fesSG1rUxOwRWcCuJY2pBRrL0AQyrY62UfZjjgyEKuhQpIYLK9dtHzfVC4V2Gib2vOsI+EqqQI4gFwY1Py185FplUDmj8LU84jKWQGhYb+GYoKQdBVa8fLgkXPHI8s+uWSNh+pzh6/EJ6CF0MduPBMS2w88OjhgEW0VUC+Ht6xbrYBeevOWtwmrpdV6RSGDx83G9WqMDRe3oFCj2uUoN4FoGLoFWobz7guMGnj2W4jCzVWD1RwXq8VTsWxxafUEV2R2WOBoHSEnGyczAFEcmv43hrndLdMA4GT4cJ8xi4mEFtpKDrTMB0WxjaWEKwJpfaeX95xkQjKxHJ5FgWWx4WjvAwAztJmx6tsUyaoEOUrgVl3jsSC8V1lgV0fwbzc/pzg9R17zkOp+jGh6Z5/wDGuf2M4nvOf+9ZxOos/wD/xAAvEQABAwMCBQMDAwUAAAAAAAACAQMEAAUREiEGBxMxQSJRcSMzYRQygRUXQpGy/9oACAECAQE/AHIDLhl0i0+VQvHvXF3MCTaZ7kW0voatv+o1FP8ABcKBfhavPM+dcbZESOCsSQki49pzj0dk+Fqy81r3FkSBkfUbkzAdQjVS6I9lQUqw8SWzi6GRRjVSHTrBdiHPaisAEuUNa5mXmBDgLFC4lBubGHmCRDFT8YQsYwtQmHpkpBFOorpepPfPmoPAIq0RPvCCGKYRKv3B71pZV8fqte6ePmuVEmaxxMLDUkWmDTL45RNenslA5lNiT/aVzPmzJt7EHzJ1GQwKKONOd65cxYz9wdUw3ANqfaOQaiC6UHbHxVxYaGzTlfX6aNL37ZqG49CurT0UnWzE8obKay/hPNWtXpdqhSFdV7qsoqmrStqq+cj4WuYPB5SVfu7twJWmAXDKhnT7COKslxnW6cMtvIg0qIW22Pav7i2hwOq7EdF3VujZJiuI+MnrzDVuO30owEmtvuq/lVrh+2G1dbZLfbkDBceDW4iEmlM99SVFVhY7e6l6e5KhLTtmjXAFafQTbwuyjsua454JC4223QLXHajNhJQnhBNO2MZ/NWLl0/Luk39Q2rcQeu2Cki6s9gJK4e5ZXNqUSzxT9KXWbcHOC2T0qlcPWz+k2dmC+YPi1kQXTjLefShJ7pTUiO2pAaIqpjG2MJQft/irl+9Ka+4XwtJ9qmu4/NF99z4D/mv/xAAsEQACAQMCBAUDBQAAAAAAAAABAgMABBEFEgYhMUETFCIycVFSYRUzNGJy/9oACAEDAQE/AE09bmQ+F8kNWhcIsrs9wAYmHp+vMVYcIx291cO+JInQqoPXJ61c8MWzW6Rx5HhoQP7HHKtZ0W5tMJMMhjkN8V5B/uFcM2EodZDAjxMNpY9qtoRyIxtHUVLNawYyBQEM6+nGcVxVFB5F96bmHsI7ZokZ61wdEP0pJcDdJzPqzSnZESprWricTAktWgtNtBfJJ7GtRtYZonWRAQQeXatdsXtr90VNoxyAbIrhDXbaJYrKO1IZurA5yfqajYk7GwAwqWxikOSVNJFHbx5BBP4q9uikEojdC/hnCEg1LNHM5Zwuc9xWnRywSq8UrJjmDitP4ha2neS6kYnwiFB+7NXvFFtawxEOrTPtJUdgetapxVBDZFrdwZPSy5/PUGtS1iO7n8xEGQuPUuT1qEhgx25yerVb9Kv/AHU/8l/8VcftrSe5fmovYK//2Q==";
+
+
+
+    protected Keycloak adminClient;
+
+
+    @Before
+    public void before() {
+        adminClient = Keycloak.getInstance(AUTH_SERVER_ROOT, MASTER, ADMIN, ADMIN, Constants.ADMIN_CLI_CLIENT_ID);
+    }
+
+    @After
+    public void after() {
+        ComponentRepresentation jpegMapper = adminClient.realm("test").components().query(ldapModel.getId(), LDAPStorageMapper.class.getName(), "jpeg-mapper").get(0);
+        adminClient.realm("test").components().component(jpegMapper.getId()).remove();
+
+        adminClient.close();
+    }
+
+
+    // Test invalid mapper configuration - validation exception thrown
+    @Test
+    public void test01InvalidMapperConfiguration() {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmManager manager = new RealmManager(session);
+            RealmModel appRealm = manager.getRealm("test");
+
+            ComponentModel ldapComponentMapper = LDAPTestUtils.addUserAttributeMapper(appRealm, ldapModel, "jpeg-mapper", LDAPConstants.JPEG_PHOTO, LDAPConstants.JPEG_PHOTO);
+
+            ldapComponentMapper.put(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE, true);
+            try {
+                appRealm.updateComponent(ldapComponentMapper);
+                Assert.fail("Not expected to successfully update mapper");
+            } catch (ComponentValidationException cve) {
+                // Expected
+            }
+
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+    }
+
+
+    @Test
+    public void test02ReadOnly() {
+        String mapperId = addPhotoMapper();
+
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmManager manager = new RealmManager(session);
+            RealmModel appRealm = manager.getRealm("test");
+
+            // Add user directly to LDAP
+            LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            addLDAPUser(ldapFedProvider, appRealm, "johnphoto", "John", "Photo", "john@photo.org", JPEG_PHOTO_BASE64);
+
+            // Set mapper to be read-only
+            ComponentModel ldapComponentMapper = appRealm.getComponent(mapperId);
+            ldapComponentMapper.put(UserAttributeLDAPStorageMapper.READ_ONLY, true);
+            appRealm.updateComponent(ldapComponentMapper);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        // Assert john found
+        getUserAndAssertPhoto("johnphoto", true);
+    }
+
+
+    @Test
+    public void test03WritableMapper() {
+        String mapperId = addPhotoMapper();
+
+        // Create user joe with jpegPHoto
+        UserRepresentation joe = new UserRepresentation();
+        joe.setUsername("joephoto");
+        joe.setEmail("joe@photo.org");
+        joe.setAttributes(Collections.singletonMap(LDAPConstants.JPEG_PHOTO, Arrays.asList(JPEG_PHOTO_BASE64)));
+        Response response = adminClient.realm("test").users().create(joe);
+        response.close();
+
+
+        // Assert he is found including jpegPhoto
+        joe = getUserAndAssertPhoto("joephoto", true);
+
+
+        // Try to update him with some big non-LDAP mapped attribute. It will fail
+        try {
+            joe.getAttributes().put("someOtherPhoto", Arrays.asList(JPEG_PHOTO_BASE64));
+            adminClient.realm("test").users().get(joe.getId()).update(joe);
+            Assert.fail("Not expected to successfully update user");
+        } catch (ClientErrorException cee) {
+            // Expected
+        }
+
+
+        // Try to update him with some bad value for jpegPhoto. It will fail
+        try {
+            joe.getAttributes().remove("someOtherPhoto");
+            joe.getAttributes().put(LDAPConstants.JPEG_PHOTO, Arrays.asList("foobar"));
+            adminClient.realm("test").users().get(joe.getId()).update(joe);
+            Assert.fail("Not expected to successfully update user");
+        } catch (ClientErrorException cee) {
+            // Expected
+        }
+
+
+        // Remove jpegPhoto attribute and assert it was successfully removed
+        joe.getAttributes().remove(LDAPConstants.JPEG_PHOTO);
+        adminClient.realm("test").users().get(joe.getId()).update(joe);
+        getUserAndAssertPhoto("joephoto", false);
+    }
+
+
+    private String addPhotoMapper() {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmManager manager = new RealmManager(session);
+            RealmModel appRealm = manager.getRealm("test");
+
+            ComponentModel ldapComponentMapper = LDAPTestUtils.addUserAttributeMapper(appRealm, ldapModel, "jpeg-mapper", LDAPConstants.JPEG_PHOTO, LDAPConstants.JPEG_PHOTO);
+
+            ldapComponentMapper.put(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE, true);
+            ldapComponentMapper.put(UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, true);
+            appRealm.updateComponent(ldapComponentMapper);
+            return ldapComponentMapper.getId();
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+    }
+
+
+    private LDAPObject addLDAPUser(LDAPStorageProvider ldapProvider, RealmModel realm, final String username,
+                                         final String firstName, final String lastName, final String email, String jpegPhoto) {
+        UserModel helperUser = new UserModelDelegate(null) {
+
+            @Override
+            public String getUsername() {
+                return username;
+            }
+
+            @Override
+            public String getFirstName() {
+                return firstName;
+            }
+
+            @Override
+            public String getLastName() {
+                return lastName;
+            }
+
+            @Override
+            public String getEmail() {
+                return email;
+            }
+
+            @Override
+            public List<String> getAttribute(String name) {
+                if (LDAPConstants.JPEG_PHOTO.equals(name)) {
+                    return Arrays.asList(jpegPhoto);
+                } else {
+                    return Collections.emptyList();
+                }
+            }
+        };
+        return LDAPUtils.addUserToLDAP(ldapProvider, realm, helperUser);
+    }
+
+
+    private UserRepresentation getUserAndAssertPhoto(String username, boolean isPhotoExpected) {
+        List<UserRepresentation> johns = adminClient.realm("test").users().search(username, 0, 10);
+        Assert.assertEquals(1, johns.size());
+        UserRepresentation john = johns.get(0);
+        Assert.assertEquals(username, john.getUsername());
+        Assert.assertTrue(john.getAttributes().containsKey(LDAPConstants.LDAP_ID)); // Doublecheck it's the LDAP mapped user
+
+        if (isPhotoExpected) {
+            Assert.assertEquals(JPEG_PHOTO_BASE64, john.getAttributes().get(LDAPConstants.JPEG_PHOTO).get(0));
+        } else {
+            Assert.assertFalse(john.getAttributes().containsKey(LDAPConstants.JPEG_PHOTO));
+        }
+        return john;
+    }
+
+}
diff --git a/testsuite/integration/src/test/resources/log4j.properties b/testsuite/integration/src/test/resources/log4j.properties
index 2fa1d70..b7922ee 100755
--- a/testsuite/integration/src/test/resources/log4j.properties
+++ b/testsuite/integration/src/test/resources/log4j.properties
@@ -78,4 +78,5 @@ log4j.logger.org.apache.directory.server.ldap.LdapProtocolHandler=error
 #log4j.logger.org.apache.http.impl.conn=debug
 
 # Enable to view details from identity provider authenticator
-# log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace
\ No newline at end of file
+# log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace
+log4j.logger.org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper=debug
\ No newline at end of file
diff --git a/util/embedded-ldap/src/main/resources/ldap/default-users.ldif b/util/embedded-ldap/src/main/resources/ldap/default-users.ldif
index ac3ffcb..22f488a 100644
--- a/util/embedded-ldap/src/main/resources/ldap/default-users.ldif
+++ b/util/embedded-ldap/src/main/resources/ldap/default-users.ldif
@@ -45,6 +45,7 @@ postalCode: 88441
 postalCode: 77332
 street: Elm 5
 userPassword: password
+jpegPhoto:: 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
 
 dn: cn=ldap-user,ou=RealmRoles,dc=keycloak,dc=org
 objectclass: top