X-Git-Url: http://git.argeo.org/?a=blobdiff_plain;f=org.argeo.cms%2Fsrc%2Forg%2Fargeo%2Fcms%2Finternal%2Fuseradmin%2Fldap%2FJcrLdapSynchronizer.java;fp=org.argeo.cms%2Fsrc%2Forg%2Fargeo%2Fcms%2Finternal%2Fuseradmin%2Fldap%2FJcrLdapSynchronizer.java;h=de28c7f496f81582793ba5fed973c439c3876f1c;hb=6ddb7b6b224a00344a182761e42b2241a721224f;hp=0000000000000000000000000000000000000000;hpb=92dee1606725534bdfc067cd2cae017f0501ac5d;p=lgpl%2Fargeo-commons.git diff --git a/org.argeo.cms/src/org/argeo/cms/internal/useradmin/ldap/JcrLdapSynchronizer.java b/org.argeo.cms/src/org/argeo/cms/internal/useradmin/ldap/JcrLdapSynchronizer.java new file mode 100644 index 000000000..de28c7f49 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/internal/useradmin/ldap/JcrLdapSynchronizer.java @@ -0,0 +1,623 @@ +/* + * Copyright (C) 2007-2012 Argeo GmbH + * + * 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.argeo.cms.internal.useradmin.ldap; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.SortedSet; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.query.Query; +import javax.jcr.version.VersionManager; +import javax.naming.Name; +import javax.naming.directory.BasicAttribute; +import javax.naming.directory.DirContext; +import javax.naming.directory.ModificationItem; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.ArgeoException; +import org.argeo.cms.internal.useradmin.SimpleJcrSecurityModel; +import org.argeo.jcr.ArgeoNames; +import org.argeo.jcr.ArgeoTypes; +import org.argeo.jcr.JcrUtils; +import org.argeo.security.SecurityUtils; +import org.argeo.security.jcr.JcrSecurityModel; +import org.argeo.security.jcr.JcrUserDetails; +import org.springframework.ldap.core.ContextMapper; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.security.authentication.encoding.PasswordEncoder; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.ldap.LdapUsernameToDnMapper; +import org.springframework.security.ldap.userdetails.UserDetailsContextMapper; + +/** Makes sure that LDAP and JCR are in line. */ +@SuppressWarnings("deprecation") +public class JcrLdapSynchronizer implements UserDetailsContextMapper, + ArgeoNames { + private final static Log log = LogFactory.getLog(JcrLdapSynchronizer.class); + + // LDAP + private LdapTemplate ldapTemplate; + /** + * LDAP template whose context source has an object factory set to null. see + * this + */ + // private LdapTemplate rawLdapTemplate; + + private String userBase; + private String usernameAttribute; + private String passwordAttribute; + private String[] userClasses; + // private String defaultUserRole ="ROLE_USER"; + + // private NamingListener ldapUserListener; + // private SearchControls subTreeSearchControls; + private LdapUsernameToDnMapper usernameMapper; + + private PasswordEncoder passwordEncoder; + private final Random random; + + // JCR + /** Admin session on the main workspace */ + private Session nodeSession; + private Repository repository; + + // private JcrProfileListener jcrProfileListener; + private JcrSecurityModel jcrSecurityModel = new SimpleJcrSecurityModel(); + + // Mapping + private Map propertyToAttributes = new HashMap(); + + public JcrLdapSynchronizer() { + random = createRandom(); + } + + public void init() { + try { + nodeSession = repository.login(); + + // TODO put this in a different thread, and poll the LDAP server + // until it is up + try { + synchronize(); + + // LDAP + // subTreeSearchControls = new SearchControls(); + // subTreeSearchControls + // .setSearchScope(SearchControls.SUBTREE_SCOPE); + // LDAP listener + // ldapUserListener = new LdapUserListener(); + // rawLdapTemplate.executeReadOnly(new ContextExecutor() { + // public Object executeWithContext(DirContext ctx) + // throws NamingException { + // EventDirContext ectx = (EventDirContext) ctx.lookup(""); + // ectx.addNamingListener(userBase, "(" + // + usernameAttribute + "=*)", + // subTreeSearchControls, ldapUserListener); + // return null; + // } + // }); + } catch (Exception e) { + log.error("Could not synchronize and listen to LDAP," + + " probably because the LDAP server is not available." + + " Restart the system as soon as possible.", e); + } + + // JCR + // String[] nodeTypes = { ArgeoTypes.ARGEO_USER_PROFILE }; + // jcrProfileListener = new JcrProfileListener(); + // noLocal is used so that we are not notified when we modify JCR + // from LDAP + // nodeSession + // .getWorkspace() + // .getObservationManager() + // .addEventListener(jcrProfileListener, + // Event.PROPERTY_CHANGED | Event.NODE_ADDED, "/", + // true, null, nodeTypes, true); + } catch (Exception e) { + JcrUtils.logoutQuietly(nodeSession); + throw new ArgeoException("Cannot initialize LDAP/JCR synchronizer", + e); + } + } + + public void destroy() { + // JcrUtils.removeListenerQuietly(nodeSession, jcrProfileListener); + JcrUtils.logoutQuietly(nodeSession); + // try { + // rawLdapTemplate.executeReadOnly(new ContextExecutor() { + // public Object executeWithContext(DirContext ctx) + // throws NamingException { + // EventDirContext ectx = (EventDirContext) ctx.lookup(""); + // ectx.removeNamingListener(ldapUserListener); + // return null; + // } + // }); + // } catch (Exception e) { + // // silent (LDAP server may have been shutdown already) + // if (log.isTraceEnabled()) + // log.trace("Cannot remove LDAP listener", e); + // } + } + + /* + * LDAP TO JCR + */ + /** Full synchronization between LDAP and JCR. LDAP has priority. */ + protected void synchronize() { + try { + Name userBaseName = new DistinguishedName(userBase); + // TODO subtree search? + @SuppressWarnings("unchecked") + List userPaths = (List) ldapTemplate.listBindings( + userBaseName, new ContextMapper() { + public Object mapFromContext(Object ctxObj) { + try { + return mapLdapToJcr((DirContextAdapter) ctxObj); + } catch (Exception e) { + // do not break process because of error + log.error( + "Could not LDAP->JCR synchronize user " + + ctxObj, e); + return null; + } + } + }); + + // create accounts which are not in LDAP + Query query = nodeSession + .getWorkspace() + .getQueryManager() + .createQuery( + "select * from [" + ArgeoTypes.ARGEO_USER_PROFILE + + "]", Query.JCR_SQL2); + NodeIterator it = query.execute().getNodes(); + while (it.hasNext()) { + Node userProfile = it.nextNode(); + String path = userProfile.getPath(); + try { + if (!userPaths.contains(path)) { + String username = userProfile + .getProperty(ARGEO_USER_ID).getString(); + // GrantedAuthority[] authorities = {new + // GrantedAuthorityImpl(defaultUserRole)}; + List authorities = new ArrayList(); + JcrUserDetails userDetails = new JcrUserDetails( + userProfile, username, authorities); + String dn = createLdapUser(userDetails); + log.warn("Created ldap entry '" + dn + "' for user '" + + username + "'"); + + // if(!userProfile.getProperty(ARGEO_ENABLED).getBoolean()){ + // continue profiles; + // } + // + // log.warn("Path " + // + path + // + " not found in LDAP, disabling user " + // + userProfile.getProperty(ArgeoNames.ARGEO_USER_ID) + // .getString()); + + // Temporary hack to repair previous behaviour + if (!userProfile.getProperty(ARGEO_ENABLED) + .getBoolean()) { + VersionManager versionManager = nodeSession + .getWorkspace().getVersionManager(); + versionManager.checkout(userProfile.getPath()); + userProfile.setProperty(ArgeoNames.ARGEO_ENABLED, + true); + nodeSession.save(); + versionManager.checkin(userProfile.getPath()); + } + } + } catch (Exception e) { + log.error("Cannot process " + path, e); + } + } + } catch (Exception e) { + JcrUtils.discardQuietly(nodeSession); + log.error("Cannot synchronize LDAP and JCR", e); + // throw new ArgeoException("Cannot synchronize LDAP and JCR", e); + } + } + + private String createLdapUser(UserDetails user) { + DirContextAdapter ctx = new DirContextAdapter(); + mapUserToContext(user, ctx); + DistinguishedName dn = usernameMapper.buildDn(user.getUsername()); + ldapTemplate.bind(dn, ctx, null); + return dn.toString(); + } + + /** Called during authentication in order to retrieve user details */ + public UserDetails mapUserFromContext(final DirContextOperations ctx, + final String username, + Collection authorities) { + if (ctx == null) + throw new ArgeoException("No LDAP information for user " + username); + + String ldapUsername = ctx.getStringAttribute(usernameAttribute); + if (!ldapUsername.equals(username)) + throw new ArgeoException("Logged in with username " + username + + " but LDAP user is " + ldapUsername); + + Node userProfile = jcrSecurityModel.sync(nodeSession, username, + SecurityUtils.authoritiesToStringList(authorities)); + // JcrUserDetails.checkAccountStatus(userProfile); + + // password + SortedSet passwordAttributes = ctx + .getAttributeSortedStringSet(passwordAttribute); + String password; + if (passwordAttributes == null || passwordAttributes.size() == 0) { + // throw new ArgeoException("No password found for user " + + // username); + password = "NULL"; + } else { + byte[] arr = (byte[]) passwordAttributes.first(); + password = new String(arr); + // erase password + Arrays.fill(arr, (byte) 0); + } + + try { + return new JcrUserDetails(userProfile, password, authorities); + } catch (RepositoryException e) { + throw new ArgeoException("Cannot retrieve user details for " + + username, e); + } + } + + /** + * Writes an LDAP context to the JCR user profile. + * + * @return path to user profile + */ + protected synchronized String mapLdapToJcr(DirContextAdapter ctx) { + Session session = nodeSession; + try { + // process + String username = ctx.getStringAttribute(usernameAttribute); + + Node userProfile = jcrSecurityModel.sync(session, username, null); + Map modifications = new HashMap(); + for (String jcrProperty : propertyToAttributes.keySet()) + ldapToJcr(userProfile, jcrProperty, ctx, modifications); + + int modifCount = modifications.size(); + if (modifCount > 0) { + session.getWorkspace().getVersionManager() + .checkout(userProfile.getPath()); + for (String prop : modifications.keySet()) + userProfile.setProperty(prop, modifications.get(prop)); + JcrUtils.updateLastModified(userProfile); + session.save(); + session.getWorkspace().getVersionManager() + .checkin(userProfile.getPath()); + if (log.isDebugEnabled()) + log.debug("Mapped " + modifCount + " LDAP modification" + + (modifCount == 1 ? "" : "s") + " from " + + ctx.getDn() + " to " + userProfile); + } + return userProfile.getPath(); + } catch (Exception e) { + JcrUtils.discardQuietly(session); + throw new ArgeoException("Cannot synchronize JCR and LDAP", e); + } + } + + /** Maps an LDAP property to a JCR property */ + protected void ldapToJcr(Node userProfile, String jcrProperty, + DirContextOperations ctx, Map modifications) { + // TODO do we really need DirContextOperations? + 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)) + modifications.put(jcrProperty, value); + } else if (value != null && jcrValue == null) { + modifications.put(jcrProperty, value); + } else if (value == null && jcrValue != null) { + modifications.put(jcrProperty, value); + } + } catch (Exception e) { + throw new ArgeoException("Cannot map JCR property " + jcrProperty + + " from LDAP", e); + } + } + + /* + * JCR to LDAP + */ + + 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 = nodeSession + .getNode(jcrUserDetails.getHomePath()).getNode( + ARGEO_PROFILE); + for (String jcrProperty : propertyToAttributes.keySet()) { + if (userProfile.hasProperty(jcrProperty)) { + ModificationItem mi = jcrToLdap(jcrProperty, userProfile + .getProperty(jcrProperty).getString()); + if (mi != null) + ctx.setAttribute(mi.getAttribute()); + } + } + if (log.isTraceEnabled()) + log.trace("Mapped " + userProfile + " to " + ctx.getDn()); + } catch (RepositoryException e) { + throw new ArgeoException("Cannot synchronize JCR and LDAP", e); + } + + } + + /** Maps a JCR property to an LDAP property */ + protected ModificationItem jcrToLdap(String jcrProperty, String value) { + // TODO do we really need DirContextOperations? + try { + String ldapAttribute; + if (propertyToAttributes.containsKey(jcrProperty)) + ldapAttribute = propertyToAttributes.get(jcrProperty); + else + return null; + + // fix issue with empty 'sn' in LDAP + if (ldapAttribute.equals("sn") && (value.trim().equals(""))) + return null; + // fix issue with empty 'description' in LDAP + if (ldapAttribute.equals("description") && value.trim().equals("")) + return null; + BasicAttribute attr = new BasicAttribute( + propertyToAttributes.get(jcrProperty), value); + ModificationItem mi = new ModificationItem( + DirContext.REPLACE_ATTRIBUTE, attr); + return mi; + } catch (Exception e) { + throw new ArgeoException("Cannot map JCR property " + jcrProperty + + " from LDAP", e); + } + } + + /* + * UTILITIES + */ + protected String encodePassword(String password) { + if (!password.startsWith("{")) { + byte[] salt = new byte[16]; + random.nextBytes(salt); + return passwordEncoder.encodePassword(password, salt); + } else { + return password; + } + } + + private static Random createRandom() { + try { + return SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + return new Random(System.currentTimeMillis()); + } + } + + /* + * DEPENDENCY INJECTION + */ + + public void setLdapTemplate(LdapTemplate ldapTemplate) { + this.ldapTemplate = ldapTemplate; + } + + public void setRawLdapTemplate(LdapTemplate rawLdapTemplate) { + // this.rawLdapTemplate = rawLdapTemplate; + } + + public void setRepository(Repository repository) { + this.repository = repository; + } + + public void setUserBase(String userBase) { + this.userBase = userBase; + } + + public void setUsernameAttribute(String usernameAttribute) { + this.usernameAttribute = usernameAttribute; + } + + public void setPropertyToAttributes(Map propertyToAttributes) { + this.propertyToAttributes = propertyToAttributes; + } + + public void setUsernameMapper(LdapUsernameToDnMapper usernameMapper) { + this.usernameMapper = usernameMapper; + } + + 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 setJcrSecurityModel(JcrSecurityModel jcrSecurityModel) { + this.jcrSecurityModel = jcrSecurityModel; + } + + /** Listen to LDAP */ + // class LdapUserListener implements ObjectChangeListener, + // NamespaceChangeListener, UnsolicitedNotificationListener { + // + // public void namingExceptionThrown(NamingExceptionEvent evt) { + // evt.getException().printStackTrace(); + // } + // + // public void objectChanged(NamingEvent evt) { + // Binding user = evt.getNewBinding(); + // // TODO find a way not to be called when JCR is the source of the + // // modification + // DirContextAdapter ctx = (DirContextAdapter) ldapTemplate + // .lookup(user.getName()); + // mapLdapToJcr(ctx); + // } + // + // public void objectAdded(NamingEvent evt) { + // Binding user = evt.getNewBinding(); + // DirContextAdapter ctx = (DirContextAdapter) ldapTemplate + // .lookup(user.getName()); + // mapLdapToJcr(ctx); + // } + // + // public void objectRemoved(NamingEvent evt) { + // if (log.isDebugEnabled()) + // log.debug(evt); + // } + // + // public void objectRenamed(NamingEvent evt) { + // if (log.isDebugEnabled()) + // log.debug(evt); + // } + // + // public void notificationReceived(UnsolicitedNotificationEvent evt) { + // UnsolicitedNotification notification = evt.getNotification(); + // NamingException ne = notification.getException(); + // String msg = "LDAP notification " + "ID=" + notification.getID() + // + ", referrals=" + notification.getReferrals(); + // if (ne != null) { + // if (log.isTraceEnabled()) + // log.trace(msg + ", exception= " + ne, ne); + // else + // log.warn(msg + ", exception= " + ne); + // } else if (log.isDebugEnabled()) { + // log.debug("Unsollicited LDAP notification " + msg); + // } + // } + // + // } + + /** Listen to JCR */ + // class JcrProfileListener implements EventListener { + // + // public void onEvent(EventIterator events) { + // try { + // final Map> modifications = new HashMap>(); + // while (events.hasNext()) { + // Event event = events.nextEvent(); + // try { + // if (Event.PROPERTY_CHANGED == event.getType()) { + // Property property = (Property) nodeSession + // .getItem(event.getPath()); + // String propertyName = property.getName(); + // Node userProfile = property.getParent(); + // String username = userProfile.getProperty( + // ARGEO_USER_ID).getString(); + // if (propertyToAttributes.containsKey(propertyName)) { + // Name name = usernameMapper.buildDn(username); + // if (!modifications.containsKey(name)) + // modifications.put(name, + // new ArrayList()); + // String value = property.getString(); + // ModificationItem mi = jcrToLdap(propertyName, + // value); + // if (mi != null) + // modifications.get(name).add(mi); + // } + // } else if (Event.NODE_ADDED == event.getType()) { + // Node userProfile = nodeSession.getNode(event + // .getPath()); + // String username = userProfile.getProperty( + // ARGEO_USER_ID).getString(); + // Name name = usernameMapper.buildDn(username); + // for (String propertyName : propertyToAttributes + // .keySet()) { + // if (!modifications.containsKey(name)) + // modifications.put(name, + // new ArrayList()); + // String value = userProfile.getProperty( + // propertyName).getString(); + // ModificationItem mi = jcrToLdap(propertyName, + // value); + // if (mi != null) + // modifications.get(name).add(mi); + // } + // } + // } catch (RepositoryException e) { + // throw new ArgeoException("Cannot process event " + // + event, e); + // } + // } + // + // for (Name name : modifications.keySet()) { + // List userModifs = modifications.get(name); + // int modifCount = userModifs.size(); + // ldapTemplate.modifyAttributes(name, userModifs + // .toArray(new ModificationItem[modifCount])); + // if (log.isDebugEnabled()) + // log.debug("Mapped " + modifCount + " JCR modification" + // + (modifCount == 1 ? "" : "s") + " to " + name); + // } + // } catch (Exception e) { + // // if (log.isDebugEnabled()) + // // e.printStackTrace(); + // throw new ArgeoException("Cannot process JCR events (" + // + e.getMessage() + ")", e); + // } + // } + // + // } +}