Merge pull request #3642 from mposolda/master

KEYCLOAK-3921 LDAP binary attributes
This commit is contained in:
Marek Posolda 2016-12-13 21:11:41 +01:00 committed by GitHub
commit 0b701b3cf9
18 changed files with 655 additions and 38 deletions

File diff suppressed because one or more lines are too long

View file

@ -119,6 +119,21 @@
"id.token.claim" : "true", "id.token.claim" : "true",
"access.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" "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", "name" : "realm roles",
"federationMapperType" : "role-ldap-mapper", "federationMapperType" : "role-ldap-mapper",

View file

@ -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();
}
}
}

View file

@ -23,6 +23,16 @@
<module-name>ldap-portal</module-name> <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> <security-constraint>
<web-resource-collection> <web-resource-collection>
<web-resource-name>LDAPApp</web-resource-name> <web-resource-name>LDAPApp</web-resource-name>

View file

@ -43,9 +43,15 @@
<h2>ID Token - other claims</h2> <h2>ID Token - other claims</h2>
<% <%
for (Map.Entry<String, Object> claim : idToken.getOtherClaims().entrySet()) { for (Map.Entry<String, Object> claim : idToken.getOtherClaims().entrySet()) {
if (!claim.getKey().equals("profile_picture")) {
%> %>
<p><b><%= claim.getKey() %>: </b><%= claim.getValue().toString() %> <p><b><%= claim.getKey() %>: </b><%= claim.getValue().toString() %>
<% <%
} else {
%>
<p><b>Profile picture: </b><img src="/ldap-portal/picture" />
<%
}
} }
%> %>
<hr /> <hr />

View file

@ -24,6 +24,7 @@ import org.keycloak.storage.UserStorageProvider;
import javax.naming.directory.SearchControls; import javax.naming.directory.SearchControls;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
@ -34,6 +35,7 @@ import java.util.Set;
public class LDAPConfig { public class LDAPConfig {
private final MultivaluedHashMap<String, String> config; private final MultivaluedHashMap<String, String> config;
private final Set<String> binaryAttributeNames = new HashSet<>();
public LDAPConfig(MultivaluedHashMap<String, String> config) { public LDAPConfig(MultivaluedHashMap<String, String> config) {
this.config = config; this.config = config;
@ -184,4 +186,39 @@ public class LDAPConfig {
return UserStorageProvider.EditMode.valueOf(editModeString); 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();
}
} }

View file

@ -20,8 +20,8 @@ package org.keycloak.storage.ldap;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.models.LDAPConstants;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore; import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.storage.ldap.mappers.LDAPConfigDecorator;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -33,37 +33,40 @@ public class LDAPIdentityStoreRegistry {
private static final Logger logger = Logger.getLogger(LDAPIdentityStoreRegistry.class); 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) { public LDAPIdentityStore getLdapStore(ComponentModel ldapModel, Map<ComponentModel, LDAPConfigDecorator> configDecorators) {
LDAPIdentityStoreContext context = ldapStores.get(model.getId()); LDAPIdentityStoreContext context = ldapStores.get(ldapModel.getId());
// Ldap config might have changed for the realm. In this case, we must re-initialize // Ldap config might have changed for the realm. In this case, we must re-initialize
MultivaluedHashMap<String, String> config = model.getConfig(); MultivaluedHashMap<String, String> configModel = ldapModel.getConfig();
if (context == null || !config.equals(context.config)) { LDAPConfig ldapConfig = new LDAPConfig(configModel);
logLDAPConfig(model.getName(), config); for (Map.Entry<ComponentModel, LDAPConfigDecorator> entry : configDecorators.entrySet()) {
ComponentModel mapperModel = entry.getKey();
LDAPConfigDecorator decorator = entry.getValue();
LDAPIdentityStore store = createLdapIdentityStore(config); decorator.updateLDAPConfig(ldapConfig, mapperModel);
context = new LDAPIdentityStoreContext(config, store); }
ldapStores.put(model.getId(), context);
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; return context.store;
} }
// Don't log LDAP password // Don't log LDAP password
private void logLDAPConfig(String fedProviderDisplayName, MultivaluedHashMap<String, String> ldapConfig) { private void logLDAPConfig(String fedProviderDisplayName, LDAPConfig ldapConfig) {
MultivaluedHashMap<String, String> copy = new MultivaluedHashMap<String, String>(ldapConfig); logger.infof("Creating new LDAP Store for the LDAP storage provider: '%s', LDAP Configuration: %s", fedProviderDisplayName, ldapConfig.toString());
copy.remove(LDAPConstants.BIND_CREDENTIAL);
logger.infof("Creating new LDAP based partition manager for the Federation provider: " + fedProviderDisplayName + ", LDAP Configuration: " + copy);
} }
/** /**
* @param ldapConfig from realm * Create LDAPIdentityStore to be cached in the local registry
* @return PartitionManager instance based on LDAP store
*/ */
public static LDAPIdentityStore createLdapIdentityStore(MultivaluedHashMap<String, String> ldapConfig) { public static LDAPIdentityStore createLdapIdentityStore(LDAPConfig cfg) {
LDAPConfig cfg = new LDAPConfig(ldapConfig);
checkSystemProperty("com.sun.jndi.ldap.connect.pool.authentication", "none simple"); 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.initsize", "1");
checkSystemProperty("com.sun.jndi.ldap.connect.pool.maxsize", "1000"); checkSystemProperty("com.sun.jndi.ldap.connect.pool.maxsize", "1000");
@ -84,12 +87,12 @@ public class LDAPIdentityStoreRegistry {
private class LDAPIdentityStoreContext { private class LDAPIdentityStoreContext {
private LDAPIdentityStoreContext(MultivaluedHashMap<String, String> config, LDAPIdentityStore store) { private LDAPIdentityStoreContext(LDAPConfig config, LDAPIdentityStore store) {
this.config = config; this.config = config;
this.store = store; this.store = store;
} }
private MultivaluedHashMap<String, String> config; private LDAPConfig config;
private LDAPIdentityStore store; private LDAPIdentityStore store;
} }
} }

View file

@ -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.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper; import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory; 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.LDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper; import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory; import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapperFactory; import org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapperFactory;
@ -57,7 +59,9 @@ import org.keycloak.storage.user.SynchronizationResult;
import org.keycloak.utils.CredentialHelper; import org.keycloak.utils.CredentialHelper;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -179,10 +183,30 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
@Override @Override
public LDAPStorageProvider create(KeycloakSession session, ComponentModel model) { 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); 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 @Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException { public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
LDAPConfig cfg = new LDAPConfig(config.getConfig()); LDAPConfig cfg = new LDAPConfig(config.getConfig());

View file

@ -18,6 +18,7 @@
package org.keycloak.storage.ldap.idm.store.ldap; package org.keycloak.storage.ldap.idm.store.ldap;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.Base64;
import org.keycloak.models.LDAPConstants; import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException; import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPConfig; import org.keycloak.storage.ldap.LDAPConfig;
@ -40,6 +41,8 @@ import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem; import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls; import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult; import javax.naming.directory.SearchResult;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@ -323,8 +326,15 @@ public class LDAPIdentityStore implements IdentityStore {
Set<String> attrValues = new LinkedHashSet<>(); Set<String> attrValues = new LinkedHashSet<>();
NamingEnumeration<?> enumm = ldapAttribute.getAll(); NamingEnumeration<?> enumm = ldapAttribute.getAll();
while (enumm.hasMoreElements()) { while (enumm.hasMoreElements()) {
String attrVal = enumm.next().toString().trim(); Object val = enumm.next();
attrValues.add(attrVal);
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)) { if (ldapAttributeName.equalsIgnoreCase(LDAPConstants.OBJECT_CLASS)) {
@ -377,7 +387,18 @@ public class LDAPIdentityStore implements IdentityStore {
if (val == null || val.toString().trim().length() == 0) { if (val == null || val.toString().trim().length() == 0) {
val = LDAPConstants.EMPTY_ATTRIBUTE_VALUE; 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); entryAttributes.put(attr);

View file

@ -515,8 +515,17 @@ public class LDAPOperationManager {
} }
} }
StringBuilder binaryAttrsBuilder = new StringBuilder();
if (this.config.isObjectGUID()) { 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()) { if (logger.isDebugEnabled()) {

View file

@ -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);
}

View file

@ -80,6 +80,7 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
public static final String READ_ONLY = "read.only"; 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 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_MANDATORY_IN_LDAP = "is.mandatory.in.ldap";
public static final String IS_BINARY_ATTRIBUTE = "is.binary.attribute";
public UserAttributeLDAPStorageMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider) { public UserAttributeLDAPStorageMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider) {
super(mapperModel, ldapProvider); super(mapperModel, ldapProvider);
@ -90,6 +91,12 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
String userModelAttrName = mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE); String userModelAttrName = mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_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()); Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
if (userModelProperty != null) { if (userModelProperty != null) {
@ -177,6 +184,7 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
final String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE); final String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
boolean isAlwaysReadValueFromLDAP = parseBooleanParameter(mapperModel, ALWAYS_READ_VALUE_FROM_LDAP); boolean isAlwaysReadValueFromLDAP = parseBooleanParameter(mapperModel, ALWAYS_READ_VALUE_FROM_LDAP);
final boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_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 // For writable mode, we want to propagate writing of attribute to LDAP as well
if (ldapProvider.getEditMode() == UserStorageProvider.EditMode.WRITABLE && !isReadOnly()) { if (ldapProvider.getEditMode() == UserStorageProvider.EditMode.WRITABLE && !isReadOnly()) {
@ -185,20 +193,23 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
@Override @Override
public void setSingleAttribute(String name, String value) { public void setSingleAttribute(String name, String value) {
setLDAPAttribute(name, value); if (setLDAPAttribute(name, value)) {
super.setSingleAttribute(name, value); super.setSingleAttribute(name, value);
}
} }
@Override @Override
public void setAttribute(String name, List<String> values) { public void setAttribute(String name, List<String> values) {
setLDAPAttribute(name, values); if (setLDAPAttribute(name, values)) {
super.setAttribute(name, values); super.setAttribute(name, values);
}
} }
@Override @Override
public void removeAttribute(String name) { public void removeAttribute(String name) {
setLDAPAttribute(name, null); if ( setLDAPAttribute(name, null)) {
super.removeAttribute(name); super.removeAttribute(name);
}
} }
@Override @Override
@ -221,10 +232,10 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
super.setFirstName(firstName); super.setFirstName(firstName);
} }
protected void setLDAPAttribute(String modelAttrName, Object value) { protected boolean setLDAPAttribute(String modelAttrName, Object value) {
if (modelAttrName.equalsIgnoreCase(userModelAttrName)) { if (modelAttrName.equalsIgnoreCase(userModelAttrName)) {
if (logger.isTraceEnabled()) { if (UserAttributeLDAPStorageMapper.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); 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(); ensureTransactionStarted();
@ -245,7 +256,53 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<>(asList)); 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());
} }
}; };

View file

@ -32,7 +32,7 @@ import java.util.List;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @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"; public static final String PROVIDER_ID = "user-attribute-ldap-mapper";
protected static final List<ProviderConfigProperty> configProperties; 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") .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) .type(ProviderConfigProperty.BOOLEAN_TYPE)
.defaultValue("false").add() .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(); .build();
} }
@ -92,6 +96,12 @@ public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMa
checkMandatoryConfigAttribute(UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, "User Model Attribute", config); checkMandatoryConfigAttribute(UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, "User Model Attribute", config);
checkMandatoryConfigAttribute(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, "LDAP 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 @Override
@ -103,4 +113,14 @@ public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMa
public List<ProviderConfigProperty> getConfigProperties(RealmModel realm, ComponentModel parent) { public List<ProviderConfigProperty> getConfigProperties(RealmModel realm, ComponentModel parent) {
return getConfigProps(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);
}
}
} }

View file

@ -88,6 +88,7 @@ public class LDAPConstants {
public static final String OBJECT_CLASS = "objectclass"; public static final String OBJECT_CLASS = "objectclass";
public static final String UID = "uid"; public static final String UID = "uid";
public static final String USER_PASSWORD_ATTRIBUTE = "userpassword"; 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 = "group";
public static final String GROUP_OF_NAMES = "groupOfNames"; public static final String GROUP_OF_NAMES = "groupOfNames";
public static final String GROUP_OF_ENTRIES = "groupOfEntries"; public static final String GROUP_OF_ENTRIES = "groupOfEntries";

View file

@ -182,8 +182,10 @@ public class UsersResource {
} catch (ModelReadOnlyException re) { } catch (ModelReadOnlyException re) {
return ErrorResponse.exists("User is read only!"); return ErrorResponse.exists("User is read only!");
} catch (ModelException me) { } catch (ModelException me) {
logger.warn("Could not update user!", me);
return ErrorResponse.exists("Could not update user!"); 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!"); return ErrorResponse.exists("Could not update user!");
} }
} }

View file

@ -78,4 +78,5 @@ log4j.logger.org.apache.directory.server.ldap.LdapProtocolHandler=error
#log4j.logger.org.apache.http.impl.conn=debug #log4j.logger.org.apache.http.impl.conn=debug
# Enable to view details from identity provider authenticator # Enable to view details from identity provider authenticator
# log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace # log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace
log4j.logger.org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper=debug

File diff suppressed because one or more lines are too long