Skip to content
Snippets Groups Projects
Commit ae502f39 authored by Thomas Mortagne's avatar Thomas Mortagne
Browse files

Merge pull request #476 from nE0sIghT/feature/ldap-photo

XWIKI-10481: implement avatar synchronization in ldap-authentificator
parents b426e52f c3fa2858
No related branches found
No related tags found
No related merge requests found
Showing
with 549 additions and 162 deletions
......@@ -70,6 +70,27 @@ public final class XWikiLDAPConfig
*/
public static final String PREF_LDAP_UID = "ldap_UID_attr";
/**
* Enable photo update property name in XWikiPreferences.
*
* @since 8.1M2
*/
public static final String PREF_LDAP_UPDATE_PHOTO = "ldap_update_photo";
/**
* Profile photo attachment name property name in XWikiPreferences.
*
* @since 8.1M2
*/
public static final String PREF_LDAP_PHOTO_ATTACHMENT_NAME = "ldap_photo_attachment_name";
/**
* LDAP photo property name in XWikiPreferences.
*
* @since 8.1M2
*/
public static final String PREF_LDAP_PHOTO_ATTRIBUTE = "ldap_photo_attribute";
/**
* Mapping fields separator.
*/
......@@ -94,6 +115,13 @@ public final class XWikiLDAPConfig
*/
public static final Set<String> DEFAULT_GROUP_MEMBERFIELDS = new HashSet<String>();
/**
* Default LDAP attribute name containing binary photo.
*
* @since 8.1M2
*/
public static final String DEFAULT_PHOTO_ATTRIBUTE = "thumbnailPhoto";
/**
* Logging tool.
*/
......@@ -516,4 +544,18 @@ public int getLDAPMaxResults(XWikiContext context)
{
return (int) getLDAPParamAsLong("ldap_maxresults", 1000, context);
}
/**
* @param context the XWiki context.
* @return set of LDAP attributes that should be treated as binary data.
* @since 8.1M2
*/
public Set<String> getBinaryAttributes(XWikiContext context)
{
Set<String> binaryAttributes = new HashSet<>();
binaryAttributes.add(getLDAPParam(XWikiLDAPConfig.PREF_LDAP_PHOTO_ATTRIBUTE, DEFAULT_PHOTO_ATTRIBUTE, context));
return binaryAttributes;
}
}
......@@ -42,6 +42,7 @@
import com.novell.ldap.LDAPSearchResults;
import com.novell.ldap.LDAPSocketFactory;
import com.xpn.xwiki.XWikiContext;
import java.util.HashSet;
/**
* LDAP communication tool.
......@@ -61,6 +62,11 @@ public class XWikiLDAPConnection
*/
private LDAPConnection connection;
/**
* LDAP attributes that should be treated as binary data.
*/
private Set<String> binaryAttributes = new HashSet<>();
/**
* @param context the XWiki context.
* @return the maximum number of milliseconds the client waits for any operation under these constraints to
......@@ -149,10 +155,11 @@ public boolean open(String ldapHost, int ldapPort, String loginDN, String passwo
port = ssl ? LDAPConnection.DEFAULT_SSL_PORT : LDAPConnection.DEFAULT_PORT;
}
XWikiLDAPConfig config = XWikiLDAPConfig.getInstance();
setBinaryAttributes(config.getBinaryAttributes(context));
try {
if (ssl) {
XWikiLDAPConfig config = XWikiLDAPConfig.getInstance();
// Dynamically set JSSE as a security provider
Security.addProvider(config.getSecureProvider(context));
......@@ -373,17 +380,31 @@ protected void ldapToXWikiAttribute(List<XWikiLDAPSearchAttribute> searchAttribu
for (LDAPAttribute attribute : (Set<LDAPAttribute>) attributeSet) {
String attributeName = attribute.getName();
LOGGER.debug(" - values for attribute [{}]", attributeName);
if(!isBinaryAttribute(attributeName)) {
LOGGER.debug(" - values for attribute [{}]", attributeName);
Enumeration<String> allValues = attribute.getStringValues();
Enumeration<String> allValues = attribute.getStringValues();
if (allValues != null) {
while (allValues.hasMoreElements()) {
String value = allValues.nextElement();
if (allValues != null) {
while (allValues.hasMoreElements()) {
String value = allValues.nextElement();
LOGGER.debug(" |- [{}]", value);
LOGGER.debug(" |- [{}]", value);
searchAttributeList.add(new XWikiLDAPSearchAttribute(attributeName, value));
searchAttributeList.add(new XWikiLDAPSearchAttribute(attributeName, value));
}
}
} else {
LOGGER.debug(" - attribute [{}] is binary", attributeName);
Enumeration<byte[]> allValues = attribute.getByteValues();
if (allValues != null) {
while (allValues.hasMoreElements()) {
byte[] value = allValues.nextElement();
searchAttributeList.add(new XWikiLDAPSearchAttribute(attributeName, value));
}
}
}
}
......@@ -440,4 +461,25 @@ public static String escapeLDAPSearchFilter(String value)
}
return sb.toString();
}
/**
* Update list of LDAP attributes that should be treated as binary data.
*
* @param binaryAttributes set of binary attributes
*/
private void setBinaryAttributes(Set<String> binaryAttributes)
{
this.binaryAttributes = binaryAttributes;
}
/**
* Checks whether attribute should be treated as binary data.
*
* @param attributeName name of attribute to check
* @return true if attribute should be treated as binary data.
*/
private boolean isBinaryAttribute(String attributeName)
{
return binaryAttributes.contains(attributeName);
}
}
......@@ -37,6 +37,13 @@ public class XWikiLDAPSearchAttribute
*/
public String value;
/**
* Attribute byte value.
*
* @since 8.1M2
*/
public byte[] byteValue;
/**
* Create attribute instance.
*
......@@ -47,11 +54,34 @@ public XWikiLDAPSearchAttribute(String name, String value)
{
this.name = name;
this.value = value;
this.byteValue = null;
}
/**
* Create attribute instance.
*
* @param name attribute name.
* @param byteValue attribute value.
* @since 8.1M2
*/
public XWikiLDAPSearchAttribute(String name, byte[] byteValue)
{
this.name = name;
this.byteValue = byteValue;
this.value = null;
}
@Override
public String toString()
{
return "{name=" + name + " value=" + value + "}";
StringBuilder stringBuilder = new StringBuilder("{name=").append(name);
if (value != null) {
stringBuilder.append(" value=").append(value);
} else {
stringBuilder.append(" byteValue length=").append((byteValue != null ? byteValue.length : 0));
}
return stringBuilder.append("}").toString();
}
}
......@@ -48,12 +48,23 @@
import com.novell.ldap.rfc2251.RfcFilter;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.doc.XWikiAttachment;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;
import com.xpn.xwiki.objects.BaseProperty;
import com.xpn.xwiki.objects.classes.BaseClass;
import com.xpn.xwiki.objects.classes.PropertyClass;
import com.xpn.xwiki.user.impl.LDAP.LDAPProfileXClass;
import com.xpn.xwiki.web.Utils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
/**
* LDAP communication tool.
......@@ -1129,6 +1140,11 @@ protected void createUserFromLDAP(XWikiDocument userProfile, List<XWikiLDAPSearc
XWikiDocument createdUserProfile = context.getWiki().getDocument(userProfile.getDocumentReference(), context);
LDAPProfileXClass ldapXClass = new LDAPProfileXClass(context);
if(config.getLDAPParam(XWikiLDAPConfig.PREF_LDAP_UPDATE_PHOTO, "0", context).equals("1")) {
// Add user photo from LDAP
updatePhotoFromLdap(ldapUid, createdUserProfile, context);
}
if (ldapXClass.updateLDAPObject(createdUserProfile, ldapDN, ldapUid)) {
context.getWiki().saveDocument(createdUserProfile, "Created user profile from LDAP server", context);
}
......@@ -1184,6 +1200,11 @@ protected void updateUserFromLDAP(XWikiDocument userProfile, List<XWikiLDAPSearc
needsUpdate = true;
}
if(config.getLDAPParam(XWikiLDAPConfig.PREF_LDAP_UPDATE_PHOTO, "0", context).equals("1")) {
// Sync user photo with LDAP
needsUpdate = updatePhotoFromLdap(ldapUid, userProfile, context) || needsUpdate;
}
// Update ldap profile object
LDAPProfileXClass ldaXClass = new LDAPProfileXClass(context);
needsUpdate |= ldaXClass.updateLDAPObject(userProfile, ldapDN, ldapUid);
......@@ -1193,6 +1214,152 @@ protected void updateUserFromLDAP(XWikiDocument userProfile, List<XWikiLDAPSearc
}
}
/**
* Sync user avatar with LDAP
*
* @param ldapUid value of the unique identifier for the user to update.
* @param userProfile the XWiki user profile document.
* @param context the XWiki context.
* @return true if avatar was updated, false otherwise.
* @throws XWikiException
*/
protected boolean updatePhotoFromLdap(String ldapUid, XWikiDocument userProfile, XWikiContext context) throws XWikiException
{
XWikiLDAPConfig config = XWikiLDAPConfig.getInstance();
BaseClass userClass = context.getWiki().getUserClass(context);
BaseObject userObj = userProfile.getXObject(userClass.getDocumentReference());
// Get current user avatar
String userAvatar = userObj.getStringValue("avatar");
XWikiAttachment currentPhoto = null;
if (userAvatar != null) {
currentPhoto = userProfile.getAttachment(userAvatar);
}
// Get properties
String photoAttachmentName = config.getLDAPParam(XWikiLDAPConfig.PREF_LDAP_PHOTO_ATTACHMENT_NAME, "ldapPhoto", context);
String ldapPhotoAttribute = config.getLDAPParam(XWikiLDAPConfig.PREF_LDAP_PHOTO_ATTRIBUTE, XWikiLDAPConfig.DEFAULT_PHOTO_ATTRIBUTE, context);
// Proceed only if any of conditions are true:
// 1. User do not have avatar currently
// 2. User have avatar and avatar file name is equals to PREF_LDAP_PHOTO_ATTACHMENT_NAME
if (StringUtils.isEmpty(userAvatar) || photoAttachmentName.equals(FilenameUtils.getBaseName(userAvatar)) || currentPhoto == null) {
// Obtain photo from LDAP
byte[] ldapPhoto = null;
List<XWikiLDAPSearchAttribute> ldapAttributes = searchUserAttributesByUid(
ldapUid, new String[]{ ldapPhotoAttribute }
);
if (ldapAttributes != null) {
// searchUserAttributesByUid method may return «dn» as 1st element
// Let's iterate over array and search ldapPhotoAttribute
for (XWikiLDAPSearchAttribute attribute : ldapAttributes) {
if (attribute.name.equals(ldapPhotoAttribute)) {
ldapPhoto = attribute.byteValue;
}
}
}
if (ldapPhoto != null) {
ByteArrayInputStream ldapPhotoInputStream = new ByteArrayInputStream(ldapPhoto);
// Try to guess image type
String ldapPhotoType = guessImageType(ldapPhotoInputStream);
ldapPhotoInputStream.reset();
if (ldapPhotoType != null) {
String photoAttachmentFullName = photoAttachmentName + "." + ldapPhotoType.toLowerCase();
if (!StringUtils.isEmpty(userAvatar) && currentPhoto != null) {
try {
// Compare current xwiki avatar and LDAP photo
if (!IOUtils.contentEquals(currentPhoto.getContentInputStream(context), ldapPhotoInputStream)) {
ldapPhotoInputStream.reset();
// Store photo
return addPhotoToProfile(userProfile, context, ldapPhotoInputStream, ldapPhoto.length, photoAttachmentFullName);
}
} catch (IOException ex) {
LOGGER.error(ex.getMessage());
}
} else if (addPhotoToProfile(userProfile, context, ldapPhotoInputStream, ldapPhoto.length, photoAttachmentFullName)) {
PropertyClass avatarProperty = (PropertyClass) userClass.getField("avatar");
userObj.safeput("avatar", avatarProperty.fromString(photoAttachmentFullName));
return true;
}
} else {
LOGGER.info("Unable to determine LDAP photo image type.");
}
} else if (currentPhoto != null) {
// Remove current avatar
PropertyClass avatarProperty = (PropertyClass) userClass.getField("avatar");
userObj.safeput("avatar", avatarProperty.fromString(""));
return true;
}
}
return false;
}
/**
* Add photo to user profile as attachment.
*
* @param userProfile the XWiki user profile document.
* @param context the XWiki context.
* @param photoInputStream InputStream containing photo.
* @param streamLength size of provided InputStream.
* @param attachmentName attachment name for provided photo.
* @return true if photo was saved to user profile, false otherwise.
*/
protected boolean addPhotoToProfile(XWikiDocument userProfile, XWikiContext context, InputStream photoInputStream, int streamLength, String attachmentName)
{
XWikiAttachment attachment;
try {
attachment = userProfile.addAttachment(attachmentName, photoInputStream, context);
} catch (IOException | XWikiException ex) {
LOGGER.error(ex.getMessage());
return false;
}
attachment.resetMimeType(context);
return true;
}
/**
* Guess image type of InputStream.
*
* @param imageInputStream InputStream containing image.
* @return type of image as String.
*/
protected String guessImageType(InputStream imageInputStream)
{
ImageInputStream imageStream;
try {
imageStream = ImageIO.createImageInputStream(imageInputStream);
} catch (IOException ex) {
LOGGER.error(ex.getMessage());
return null;
}
Iterator<ImageReader> it = ImageIO.getImageReaders(imageStream);
if(!it.hasNext()) {
LOGGER.warn("No image readers found for provided stream.");
return null;
}
ImageReader imageReader = it.next();
imageReader.setInput(imageStream);
try {
return imageReader.getFormatName();
} catch (IOException ex) {
LOGGER.error(ex.getMessage());
return null;
} finally {
imageReader.dispose();
}
}
/**
* Add user name to provided XWiki group.
*
......
......@@ -668,8 +668,8 @@
$xwiki.ssx.use('XWiki.AdminLdapSheet')
#set ($params = {
'ldap': ['ldap', 'ldap_server', 'ldap_port', 'ldap_bind_DN','ldap_bind_pass', 'ldap_user_group', 'ldap_exclude_group',
'ldap_base_DN', 'ldap_UID_attr', 'ldap_trylocal','ldap_update_user', 'ldap_fields_mapping','ldap_group_mapping',
'ldap_groupcache_expiration', 'ldap_mode_group_sync']
'ldap_base_DN', 'ldap_UID_attr', 'ldap_trylocal','ldap_update_user', 'ldap_update_photo', 'ldap_photo_attachment_name',
'ldap_photo_attribute', 'ldap_fields_mapping','ldap_group_mapping', 'ldap_groupcache_expiration', 'ldap_mode_group_sync']
})
Note : 'ldap_validate_password' has been voluntary left out, has it's xwiki.cfg documentation precises that "[it is] covering very rare and bad use cases."
......
......@@ -188,6 +188,9 @@ public boolean updateDocument(XWikiDocument document)
needsUpdate |= bclass.addTextField("ldap_UID_attr", "Ldap UID attribute name", 60);
needsUpdate |= bclass.addTextField("ldap_fields_mapping", "Ldap user fiels mapping", 60);
needsUpdate |= bclass.addBooleanField("ldap_update_user", "Update user from LDAP", "yesno");
needsUpdate |= bclass.addBooleanField("ldap_update_photo", "Update user photo from LDAP", "yesno");
needsUpdate |= bclass.addTextField("ldap_photo_attachment_name", "Attachment name to save LDAP photo", 30);
needsUpdate |= bclass.addTextField("ldap_photo_attribute", "Ldap photo attribute name", 60);
needsUpdate |= bclass.addTextAreaField("ldap_group_mapping", "Ldap groups mapping", 60, 5);
needsUpdate |= bclass.addTextField("ldap_groupcache_expiration", "LDAP groups members cache", 60);
needsUpdate |= bclass.addStaticListField("ldap_mode_group_sync", "LDAP groups sync mode", "|always|create");
......
......@@ -3319,6 +3319,12 @@ XWiki.XWikiPreferences_ldap_UID_attr.hint=Specifies the LDAP attribute containin
XWiki.XWikiPreferences_ldap_fields_mapping=Ldap user fields mapping
XWiki.XWikiPreferences_ldap_update_user=Update user from LDAP after login
XWiki.XWikiPreferences_ldap_update_user.hint=If not, the mapped attributes from LDAP to XWiki will be updated only when the user is created when login for the first time.
XWiki.XWikiPreferences_ldap_update_photo=Update user photo from LDAP
XWiki.XWikiPreferences_ldap_update_photo.hint=If enabled xwiki avatar will be synchronized with LDAP
XWiki.XWikiPreferences_ldap_photo_attachment_name=Attachment name used to save LDAP photo
XWiki.XWikiPreferences_ldap_photo_attachment_name.hint=Filename of LDAP photo that will be used in xwiki profile
XWiki.XWikiPreferences_ldap_photo_attribute=Ldap photo attribute name
XWiki.XWikiPreferences_ldap_photo_attribute.hint=Specifies the LDAP attribute containing photo image
XWiki.XWikiPreferences_ldap_group_mapping=Ldap groups mapping
XWiki.XWikiPreferences_ldap_groupcache_expiration=LDAP groups cache expiration
XWiki.XWikiPreferences_ldap_groupcache_expiration.hint=Time in seconds after which the list of members in a group is refreshed from LDAP. The default is 21600 (6 hours).
......
......@@ -476,6 +476,23 @@ xwiki.authentication.ldap.fields_mapping=last_name=sn,first_name=givenName,email
#-# The default is 0
xwiki.authentication.ldap.update_user=1
#-# [Since 8.1M2, XWikiLDAPUtils]
#-# On every login update photo from LDAP to XWiki avatar otherwise photo will not be updated.
#-# - 0: never
#-# - 1: at each authentication
#-# The default is 0
# xwiki.authentication.ldap.update_photo=0
#-# [Since 8.1M2, XWikiLDAPUtils]
#-# Profile attachment name which will be used to save LDAP photo.
#-# The default is ldapPhoto
# xwiki.authentication.ldap.photo_attachment_name=ldapPhoto
#-# [Since 8.1M2, XWikiLDAPUtils]
#-# Specifies the LDAP attribute containing the binary photo
#-# The default is thumbnailPhoto
# xwiki.authentication.ldap.photo_attribute=thumbnailPhoto
#-# [Since 1.3M2, XWikiLDAPAuthServiceImpl]
#-# Maps XWiki groups to LDAP groups, separator is "|".
#-# The following kind of groups are supported:
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment