+/*
+ * 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.security.ldap.jcr;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
-import javax.jcr.Property;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
-import javax.jcr.observation.Event;
-import javax.jcr.observation.EventIterator;
-import javax.jcr.observation.EventListener;
import javax.jcr.query.Query;
-import javax.naming.Binding;
+import javax.jcr.version.VersionManager;
import javax.naming.Name;
-import javax.naming.NamingException;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
-import javax.naming.directory.SearchControls;
-import javax.naming.event.EventDirContext;
-import javax.naming.event.NamespaceChangeListener;
-import javax.naming.event.NamingEvent;
-import javax.naming.event.NamingExceptionEvent;
-import javax.naming.event.NamingListener;
-import javax.naming.event.ObjectChangeListener;
-import javax.naming.ldap.UnsolicitedNotification;
-import javax.naming.ldap.UnsolicitedNotificationEvent;
-import javax.naming.ldap.UnsolicitedNotificationListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.ContextExecutor;
+import org.argeo.security.jcr.SimpleJcrSecurityModel;
import org.springframework.ldap.core.ContextMapper;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.userdetails.ldap.UserDetailsContextMapper;
-/** Guarantees that LDAP and JCR are in line. */
+/** Makes sure that LDAP and JCR are in line. */
public class JcrLdapSynchronizer implements UserDetailsContextMapper,
ArgeoNames {
private final static Log log = LogFactory.getLog(JcrLdapSynchronizer.class);
* "http://forum.springsource.org/showthread.php?55955-Persistent-search-with-spring-ldap"
* >this</a>
*/
- private LdapTemplate rawLdapTemplate;
+ // 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 NamingListener ldapUserListener;
+ // private SearchControls subTreeSearchControls;
private LdapUsernameToDnMapper usernameMapper;
private PasswordEncoder passwordEncoder;
private final Random random;
// JCR
- /** Admin session on the security workspace */
- private Session securitySession;
+ /** Admin session on the main workspace */
+ private Session nodeSession;
private Repository repository;
- private String securityWorkspace = "security";
-
- private JcrProfileListener jcrProfileListener;
+ // private JcrProfileListener jcrProfileListener;
+ private JcrSecurityModel jcrSecurityModel = new SimpleJcrSecurityModel();
// Mapping
private Map<String, String> propertyToAttributes = new HashMap<String, String>();
public void init() {
try {
- securitySession = repository.login(securityWorkspace);
-
- 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;
- }
- });
+ 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();
+ // String[] nodeTypes = { ArgeoTypes.ARGEO_USER_PROFILE };
+ // jcrProfileListener = new JcrProfileListener();
// noLocal is used so that we are not notified when we modify JCR
// from LDAP
- securitySession
- .getWorkspace()
- .getObservationManager()
- .addEventListener(jcrProfileListener,
- Event.PROPERTY_CHANGED | Event.NODE_ADDED, "/",
- true, null, nodeTypes, true);
+ // nodeSession
+ // .getWorkspace()
+ // .getObservationManager()
+ // .addEventListener(jcrProfileListener,
+ // Event.PROPERTY_CHANGED | Event.NODE_ADDED, "/",
+ // true, null, nodeTypes, true);
} catch (Exception e) {
- JcrUtils.logoutQuietly(securitySession);
+ JcrUtils.logoutQuietly(nodeSession);
throw new ArgeoException("Cannot initialize LDAP/JCR synchronizer",
e);
}
}
public void destroy() {
- JcrUtils.removeListenerQuietly(securitySession, jcrProfileListener);
- JcrUtils.logoutQuietly(securitySession);
- 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);
- }
+ // 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);
+ // }
}
/*
List<String> userPaths = (List<String>) ldapTemplate.listBindings(
userBaseName, new ContextMapper() {
public Object mapFromContext(Object ctxObj) {
- return mapLdapToJcr((DirContextAdapter) 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;
+ }
}
});
- // disable accounts which are not in LDAP
- Query query = securitySession
+ // create accounts which are not in LDAP
+ Query query = nodeSession
.getWorkspace()
.getQueryManager()
.createQuery(
while (it.hasNext()) {
Node userProfile = it.nextNode();
String path = userProfile.getPath();
- if (!userPaths.contains(path)) {
- userProfile.setProperty(ArgeoNames.ARGEO_ENABLED, false);
+ try {
+ if (!userPaths.contains(path)) {
+ String username = userProfile
+ .getProperty(ARGEO_USER_ID).getString();
+ // GrantedAuthority[] authorities = {new
+ // GrantedAuthorityImpl(defaultUserRole)};
+ GrantedAuthority[] authorities = {};
+ 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) {
- throw new ArgeoException("Cannot synchronized LDAP and JCR", 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, GrantedAuthority[] authorities) {
if (ctx == null)
throw new ArgeoException("No LDAP information for user " + username);
- Node userHome = JcrUtils.getUserHome(securitySession, username);
- if (userHome == null)
- throw new ArgeoException("No JCR 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
}
try {
- return new JcrUserDetails(userHome.getNode(ARGEO_PROFILE),
- password, authorities);
+ return new JcrUserDetails(userProfile, password, authorities);
} catch (RepositoryException e) {
throw new ArgeoException("Cannot retrieve user details for "
+ username, e);
*
* @return path to user profile
*/
- protected String mapLdapToJcr(DirContextAdapter ctx) {
- Session session = securitySession;
+ protected synchronized String mapLdapToJcr(DirContextAdapter ctx) {
+ Session session = nodeSession;
try {
// process
String username = ctx.getStringAttribute(usernameAttribute);
- Node userHome = JcrUtils.createUserHomeIfNeeded(session, username);
- Node userProfile; // = userHome.getNode(ARGEO_PROFILE);
- if (userHome.hasNode(ARGEO_PROFILE)) {
- userProfile = userHome.getNode(ARGEO_PROFILE);
- } else {
- userProfile = JcrUtils.createUserProfile(securitySession,
- username);
- userProfile.getSession().save();
- userProfile.getSession().getWorkspace().getVersionManager()
- .checkin(userProfile.getPath());
- }
+ Node userProfile = jcrSecurityModel.sync(session, username, null);
Map<String, String> modifications = new HashMap<String, String>();
for (String jcrProperty : propertyToAttributes.keySet())
ldapToJcr(userProfile, jcrProperty, ctx, modifications);
- // assign default values
- // if (!userProfile.hasProperty(Property.JCR_DESCRIPTION)
- // && !modifications.containsKey(Property.JCR_DESCRIPTION))
- // modifications.put(Property.JCR_DESCRIPTION, "");
- // if (!userProfile.hasProperty(Property.JCR_TITLE))
- // modifications.put(Property.JCR_TITLE,
- // userProfile.getProperty(ARGEO_FIRST_NAME).getString()
- // + " "
- // + userProfile.getProperty(ARGEO_LAST_NAME)
- // .getString());
int modifCount = modifications.size();
if (modifCount > 0) {
session.getWorkspace().getVersionManager()
+ jcrProperty);
String value = ctx.getStringAttribute(ldapAttribute);
- // if (value == null && Property.JCR_TITLE.equals(jcrProperty))
- // value = "";
- // if (value == null &&
- // Property.JCR_DESCRIPTION.equals(jcrProperty))
- // value = "";
String jcrValue = userProfile.hasProperty(jcrProperty) ? userProfile
.getProperty(jcrProperty).getString() : null;
if (value != null && jcrValue != null) {
final JcrUserDetails jcrUserDetails = (JcrUserDetails) user;
try {
- Node userProfile = securitySession.getNode(
- jcrUserDetails.getHomePath()).getNode(ARGEO_PROFILE);
+ Node userProfile = nodeSession
+ .getNode(jcrUserDetails.getHomePath()).getNode(
+ ARGEO_PROFILE);
for (String jcrProperty : propertyToAttributes.keySet()) {
if (userProfile.hasProperty(jcrProperty)) {
ModificationItem mi = jcrToLdap(jcrProperty, userProfile
}
public void setRawLdapTemplate(LdapTemplate rawLdapTemplate) {
- this.rawLdapTemplate = rawLdapTemplate;
+ // this.rawLdapTemplate = rawLdapTemplate;
}
public void setRepository(Repository repository) {
this.repository = repository;
}
- public void setSecurityWorkspace(String securityWorkspace) {
- this.securityWorkspace = securityWorkspace;
- }
-
public void setUserBase(String userBase) {
this.userBase = userBase;
}
this.passwordEncoder = passwordEncoder;
}
- /** 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);
- }
- }
-
+ public void setJcrSecurityModel(JcrSecurityModel jcrSecurityModel) {
+ this.jcrSecurityModel = jcrSecurityModel;
}
- /** Listen to JCR */
- class JcrProfileListener implements EventListener {
-
- public void onEvent(EventIterator events) {
- try {
- final Map<Name, List<ModificationItem>> modifications = new HashMap<Name, List<ModificationItem>>();
- while (events.hasNext()) {
- Event event = events.nextEvent();
- try {
- if (Event.PROPERTY_CHANGED == event.getType()) {
- Property property = (Property) securitySession
- .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<ModificationItem>());
- 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 = securitySession.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<ModificationItem>());
- 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<ModificationItem> 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);
- }
- }
+ /** 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<Name, List<ModificationItem>> modifications = new HashMap<Name,
+ // List<ModificationItem>>();
+ // 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<ModificationItem>());
+ // 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<ModificationItem>());
+ // 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<ModificationItem> 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);
+ // }
+ // }
+ //
+ // }
}