diff --git a/examples/ldap/ldap-example-users.ldif b/examples/ldap/ldap-example-users.ldif index d503255d9c..082ee53ce2 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 ef1e2ae7bf..8a04b3e63e 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" + } } ] } @@ -247,6 +262,19 @@ "always.read.value.from.ldap" : "false" } }, + { + "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", 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 0000000000..3ca7856254 --- /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 Marek Posolda + */ +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/WEB-INF/web.xml b/examples/ldap/src/main/webapp/WEB-INF/web.xml index 23d88965d4..64f703ac89 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 @@ ldap-portal + + Picture + org.keycloak.example.ldap.LDAPPictureServlet + + + + Picture + /picture/* + + LDAPApp diff --git a/examples/ldap/src/main/webapp/index.jsp b/examples/ldap/src/main/webapp/index.jsp index a40c382898..11560493b7 100644 --- a/examples/ldap/src/main/webapp/index.jsp +++ b/examples/ldap/src/main/webapp/index.jsp @@ -43,9 +43,15 @@

ID Token - other claims

<% for (Map.Entry claim : idToken.getOtherClaims().entrySet()) { + if (!claim.getKey().equals("profile_picture")) { %>

<%= claim.getKey() %>: <%= claim.getValue().toString() %> <% + } else { +%> +

Profile picture: +<% + } } %>


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 930837d037..ee2a89f670 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 config; + private final Set binaryAttributeNames = new HashSet<>(); public LDAPConfig(MultivaluedHashMap 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 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 copy = new MultivaluedHashMap(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 7dc3086ba4..67d03f2dc7 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 ldapStores = new ConcurrentHashMap(); + private Map ldapStores = new ConcurrentHashMap<>(); - public LDAPIdentityStore getLdapStore(ComponentModel model) { - LDAPIdentityStoreContext context = ldapStores.get(model.getId()); + public LDAPIdentityStore getLdapStore(ComponentModel ldapModel, Map configDecorators) { + LDAPIdentityStoreContext context = ldapStores.get(ldapModel.getId()); // Ldap config might have changed for the realm. In this case, we must re-initialize - MultivaluedHashMap config = model.getConfig(); - if (context == null || !config.equals(context.config)) { - logLDAPConfig(model.getName(), config); + MultivaluedHashMap configModel = ldapModel.getConfig(); + LDAPConfig ldapConfig = new LDAPConfig(configModel); + for (Map.Entry 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 ldapConfig) { - MultivaluedHashMap copy = new MultivaluedHashMap(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 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 config, LDAPIdentityStore store) { + private LDAPIdentityStoreContext(LDAPConfig config, LDAPIdentityStore store) { this.config = config; this.store = store; } - private MultivaluedHashMap 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 051ed55327..d8d04977b7 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 Marek Posolda @@ -179,10 +183,30 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory 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 getLDAPConfigDecorators(KeycloakSession session, ComponentModel ldapModel) { + RealmModel realm = session.realms().getRealm(ldapModel.getParentId()); + List mapperComponents = realm.getComponents(ldapModel.getId(), LDAPStorageMapper.class.getName()); + + Map 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/idm/store/ldap/LDAPIdentityStore.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java index b36c17c60a..fec84b2e5b 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 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 cb58a1e174..cf421e7d20 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/mappers/LDAPConfigDecorator.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPConfigDecorator.java new file mode 100644 index 0000000000..0da6df14ec --- /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 Marek Posolda + */ +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 3b29091e1d..900969ce30 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 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 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 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 fcc5525f70..eece55b835 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 Marek Posolda */ -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 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 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 08be1c1bd2..7bef0cbee1 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 c866ac9f62..98d664166e 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 0000000000..68e98cfb76 --- /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 Marek Posolda + */ +@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 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 = "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"; + + + + 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 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 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 2fa1d70fbc..b7922eec8b 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 ac3ffcb099..22f488a552 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