]> git.argeo.org Git - lgpl/argeo-commons.git/blobdiff - security/runtime/org.argeo.security.ldap/src/main/java/org/argeo/security/ldap/jcr/JcrUserDetailsContextMapper.java
First working remote node
[lgpl/argeo-commons.git] / security / runtime / org.argeo.security.ldap / src / main / java / org / argeo / security / ldap / jcr / JcrUserDetailsContextMapper.java
index b5b77474cb2e40bc71c3a29d797640cc03e72f7e..b6657f0c597998dc015290ac713d7e1e6651161e 100644 (file)
 package org.argeo.security.ldap.jcr;
 
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.SortedSet;
+import java.util.concurrent.Executor;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
 import javax.jcr.Session;
 
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.ArgeoException;
+import org.argeo.jcr.ArgeoNames;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.security.jcr.JcrUserDetails;
 import org.springframework.ldap.core.DirContextAdapter;
 import org.springframework.ldap.core.DirContextOperations;
+import org.springframework.security.BadCredentialsException;
 import org.springframework.security.GrantedAuthority;
+import org.springframework.security.context.SecurityContextHolder;
+import org.springframework.security.providers.encoding.PasswordEncoder;
 import org.springframework.security.userdetails.UserDetails;
 import org.springframework.security.userdetails.ldap.UserDetailsContextMapper;
 
-public class JcrUserDetailsContextMapper implements UserDetailsContextMapper {
+/**
+ * Maps LDAP attributes and JCR properties. This class is meant to be robust,
+ * checks of which values should be mandatory should be performed at a higher
+ * level.
+ */
+public class JcrUserDetailsContextMapper implements UserDetailsContextMapper,
+               ArgeoNames {
+       private final static Log log = LogFactory
+                       .getLog(JcrUserDetailsContextMapper.class);
+
+       private String usernameAttribute;
+       private String passwordAttribute;
+       private String homeBasePath;
+       private String[] userClasses;
+
+       private Map<String, String> propertyToAttributes = new HashMap<String, String>();
+       private Executor systemExecutor;
        private Session session;
 
-       public UserDetails mapUserFromContext(DirContextOperations ctx,
-                       String username, GrantedAuthority[] authority) {
-               // TODO Auto-generated method stub
-               return null;
+       private PasswordEncoder passwordEncoder;
+       private final Random random;
+
+       /** 0 is always sync */
+       private Long syncLatency = 10 * 60 * 1000l;
+
+       public JcrUserDetailsContextMapper() {
+               random = createRandom();
+       }
+
+       private static Random createRandom() {
+               try {
+                       return SecureRandom.getInstance("SHA1PRNG");
+               } catch (NoSuchAlgorithmException e) {
+                       return new Random(System.currentTimeMillis());
+               }
+       }
+
+       public UserDetails mapUserFromContext(final DirContextOperations ctx,
+                       final String username, GrantedAuthority[] authorities) {
+               if (ctx == null)
+                       throw new ArgeoException("No LDAP information found for user "
+                                       + username);
+
+               final StringBuffer userHomePathT = new StringBuffer("");
+               Runnable action = new Runnable() {
+                       public void run() {
+                               String userHomepath = mapLdapToJcr(username, ctx);
+                               userHomePathT.append(userHomepath);
+                       }
+               };
+
+               if (SecurityContextHolder.getContext().getAuthentication() == null) {
+                       // authentication
+                       try {
+                               systemExecutor.execute(action);
+                       } finally {
+                               JcrUtils.logoutQuietly(session);
+                       }
+               } else {
+                       // authenticated user
+                       action.run();
+               }
+
+               // password
+               SortedSet<?> passwordAttributes = ctx
+                               .getAttributeSortedStringSet(passwordAttribute);
+               String password;
+               if (passwordAttributes == null || passwordAttributes.size() == 0) {
+                       throw new ArgeoException("No password found for user " + username);
+               } else {
+                       byte[] arr = (byte[]) passwordAttributes.first();
+                       password = new String(arr);
+                       // erase password
+                       Arrays.fill(arr, (byte) 0);
+               }
+               JcrUserDetails userDetails = new JcrUserDetails(
+                               userHomePathT.toString(), username, password, true, true, true,
+                               true, authorities);
+               return userDetails;
+       }
+
+       /** @return path to the user home node */
+       protected synchronized String mapLdapToJcr(String username,
+                       DirContextOperations ctx) {
+               String usernameLdap = ctx.getStringAttribute(usernameAttribute);
+               // log.debug("username=" + username + ", usernameLdap=" + usernameLdap);
+               if (!username.equals(usernameLdap)) {
+                       String msg = "Provided username '" + username
+                                       + "' is different from username stored in LDAP '"
+                                       + usernameLdap + "'";
+                       // we log it because the exception may not be displayed
+                       log.error(msg);
+                       throw new BadCredentialsException(msg);
+               }
+
+               try {
+
+                       Node userHome = JcrUtils.getUserHome(session, username);
+                       boolean justCreatedHome = false;
+                       if (userHome == null) {
+                               userHome = JcrUtils.createUserHome(session, homeBasePath,
+                                               username);
+                               justCreatedHome = true;
+                       }
+                       String userHomePath = userHome.getPath();
+                       Node userProfile; // = userHome.getNode(ARGEO_PROFILE);
+                       if (userHome.hasNode(ARGEO_PROFILE)) {
+                               userProfile = userHome.getNode(ARGEO_PROFILE);
+                               if (syncLatency != 0 && !justCreatedHome) {
+                                       Calendar lastModified = userProfile.getProperty(
+                                                       Property.JCR_LAST_MODIFIED).getDate();
+                                       long timeSinceLastUpdate = System.currentTimeMillis()
+                                                       - lastModified.getTimeInMillis();
+                                       if (timeSinceLastUpdate < syncLatency)// skip sync
+                                               return userHomePath;
+                               }
+                       } else {
+                               throw new ArgeoException("We should never reach this point");
+                               // userProfile = userHome.addNode(ARGEO_PROFILE);
+                               // userProfile.addMixin(NodeType.MIX_TITLE);
+                               // userProfile.addMixin(NodeType.MIX_CREATED);
+                               // userProfile.addMixin(NodeType.MIX_LAST_MODIFIED);
+                       }
+
+                       session.getWorkspace().getVersionManager()
+                                       .checkout(userProfile.getPath());
+                       for (String jcrProperty : propertyToAttributes.keySet())
+                               ldapToJcr(userProfile, jcrProperty, ctx);
+
+                       // assign default values
+                       if (!userProfile.hasProperty(Property.JCR_DESCRIPTION))
+                               userProfile.setProperty(Property.JCR_DESCRIPTION, "");
+                       if (!userProfile.hasProperty(Property.JCR_TITLE))
+                               userProfile.setProperty(Property.JCR_TITLE, userProfile
+                                               .getProperty(ARGEO_FIRST_NAME).getString()
+                                               + " "
+                                               + userProfile.getProperty(ARGEO_LAST_NAME).getString());
+                       JcrUtils.updateLastModified(userProfile);
+                       session.save();
+                       session.getWorkspace().getVersionManager()
+                                       .checkin(userProfile.getPath());
+                       if (log.isTraceEnabled())
+                               log.trace("Mapped " + ctx.getDn() + " to " + userProfile);
+                       return userHomePath;
+               } catch (Exception e) {
+                       JcrUtils.discardQuietly(session);
+                       throw new ArgeoException("Cannot synchronize JCR and LDAP", e);
+               }
+       }
+
+       public void mapUserToContext(UserDetails user, final DirContextAdapter ctx) {
+               if (!(user instanceof JcrUserDetails))
+                       throw new ArgeoException("Unsupported user details: "
+                                       + user.getClass());
+
+               ctx.setAttributeValues("objectClass", userClasses);
+               ctx.setAttributeValue(usernameAttribute, user.getUsername());
+               ctx.setAttributeValue(passwordAttribute,
+                               encodePassword(user.getPassword()));
+
+               final JcrUserDetails jcrUserDetails = (JcrUserDetails) user;
+               try {
+                       Node userProfile = session.getNode(jcrUserDetails.getHomePath()
+                                       + '/' + ARGEO_PROFILE);
+                       for (String jcrProperty : propertyToAttributes.keySet())
+                               jcrToLdap(userProfile, jcrProperty, ctx);
+
+                       if (log.isTraceEnabled())
+                               log.trace("Mapped " + userProfile + " to " + ctx.getDn());
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot synchronize JCR and LDAP", e);
+               }
+       }
+
+       protected String encodePassword(String password) {
+               if (!password.startsWith("{")) {
+                       byte[] salt = new byte[16];
+                       random.nextBytes(salt);
+                       return passwordEncoder.encodePassword(password, salt);
+               } else {
+                       return password;
+               }
        }
 
-       public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {
-               // TODO Auto-generated method stub
+       protected void ldapToJcr(Node userProfile, String jcrProperty,
+                       DirContextOperations ctx) {
+               try {
+                       String ldapAttribute;
+                       if (propertyToAttributes.containsKey(jcrProperty))
+                               ldapAttribute = propertyToAttributes.get(jcrProperty);
+                       else
+                               throw new ArgeoException(
+                                               "No LDAP attribute mapped for JCR proprty "
+                                                               + jcrProperty);
+
+                       String value = ctx.getStringAttribute(ldapAttribute);
+                       String jcrValue = userProfile.hasProperty(jcrProperty) ? userProfile
+                                       .getProperty(jcrProperty).getString() : null;
+                       if (value != null && jcrValue != null) {
+                               if (!value.equals(jcrValue))
+                                       userProfile.setProperty(jcrProperty, value);
+                       } else if (value != null && jcrValue == null) {
+                               userProfile.setProperty(jcrProperty, value);
+                       } else if (value == null && jcrValue != null) {
+                               userProfile.setProperty(jcrProperty, value);
+                       }
+               } catch (Exception e) {
+                       throw new ArgeoException("Cannot map JCR property " + jcrProperty
+                                       + " from LDAP", e);
+               }
+       }
+
+       protected void jcrToLdap(Node userProfile, String jcrProperty,
+                       DirContextOperations ctx) {
+               try {
+                       String ldapAttribute;
+                       if (propertyToAttributes.containsKey(jcrProperty))
+                               ldapAttribute = propertyToAttributes.get(jcrProperty);
+                       else
+                               throw new ArgeoException(
+                                               "No LDAP attribute mapped for JCR proprty "
+                                                               + jcrProperty);
+
+                       // fix issue with empty 'sn' in LDAP
+                       if (ldapAttribute.equals("sn")
+                                       && (!userProfile.hasProperty(jcrProperty) || userProfile
+                                                       .getProperty(jcrProperty).getString().trim()
+                                                       .equals("")))
+                               userProfile.setProperty(jcrProperty, "empty");
+
+                       if (ldapAttribute.equals("description")) {
+                               String value = userProfile.getProperty(jcrProperty).getString();
+                               if (value.trim().equals(""))
+                                       return;
+                       }
+
+                       if (!userProfile.hasProperty(jcrProperty))
+                               return;
+                       String value = userProfile.getProperty(jcrProperty).getString();
+
+                       ctx.setAttributeValue(ldapAttribute, value);
+               } catch (Exception e) {
+                       throw new ArgeoException("Cannot map JCR property " + jcrProperty
+                                       + " from LDAP", e);
+               }
+       }
+
+       public void setPropertyToAttributes(Map<String, String> propertyToAttributes) {
+               this.propertyToAttributes = propertyToAttributes;
+       }
+
+       public void setSystemExecutor(Executor systemExecutor) {
+               this.systemExecutor = systemExecutor;
+       }
+
+       public void setHomeBasePath(String homeBasePath) {
+               this.homeBasePath = homeBasePath;
+       }
+
+       public void setUsernameAttribute(String usernameAttribute) {
+               this.usernameAttribute = usernameAttribute;
+       }
+
+       public void setPasswordAttribute(String passwordAttribute) {
+               this.passwordAttribute = passwordAttribute;
+       }
+
+       public void setUserClasses(String[] userClasses) {
+               this.userClasses = userClasses;
+       }
+
+       public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
+               this.passwordEncoder = passwordEncoder;
+       }
+
+       public void setSession(Session session) {
+               this.session = session;
+       }
 
+       /**
+        * Time in ms during which the LDAP server is not checked. 0 is always sync.
+        */
+       public void setSyncLatency(Long syncLatency) {
+               this.syncLatency = syncLatency;
        }
 
 }