2 * Copyright (C) 2007-2012 Argeo GmbH
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package org
.argeo
.security
.ldap
.jcr
;
18 import java
.security
.NoSuchAlgorithmException
;
19 import java
.security
.SecureRandom
;
20 import java
.util
.ArrayList
;
21 import java
.util
.Arrays
;
22 import java
.util
.Collection
;
23 import java
.util
.HashMap
;
24 import java
.util
.List
;
26 import java
.util
.Random
;
27 import java
.util
.SortedSet
;
29 import javax
.jcr
.Node
;
30 import javax
.jcr
.NodeIterator
;
31 import javax
.jcr
.Repository
;
32 import javax
.jcr
.RepositoryException
;
33 import javax
.jcr
.Session
;
34 import javax
.jcr
.query
.Query
;
35 import javax
.jcr
.version
.VersionManager
;
36 import javax
.naming
.Name
;
37 import javax
.naming
.directory
.BasicAttribute
;
38 import javax
.naming
.directory
.DirContext
;
39 import javax
.naming
.directory
.ModificationItem
;
41 import org
.apache
.commons
.logging
.Log
;
42 import org
.apache
.commons
.logging
.LogFactory
;
43 import org
.argeo
.ArgeoException
;
44 import org
.argeo
.jcr
.ArgeoNames
;
45 import org
.argeo
.jcr
.ArgeoTypes
;
46 import org
.argeo
.jcr
.JcrUtils
;
47 import org
.argeo
.security
.SecurityUtils
;
48 import org
.argeo
.security
.jcr
.JcrSecurityModel
;
49 import org
.argeo
.security
.jcr
.JcrUserDetails
;
50 import org
.argeo
.security
.jcr
.SimpleJcrSecurityModel
;
51 import org
.springframework
.ldap
.core
.ContextMapper
;
52 import org
.springframework
.ldap
.core
.DirContextAdapter
;
53 import org
.springframework
.ldap
.core
.DirContextOperations
;
54 import org
.springframework
.ldap
.core
.DistinguishedName
;
55 import org
.springframework
.ldap
.core
.LdapTemplate
;
56 import org
.springframework
.security
.authentication
.encoding
.PasswordEncoder
;
57 import org
.springframework
.security
.core
.GrantedAuthority
;
58 import org
.springframework
.security
.core
.userdetails
.UserDetails
;
59 import org
.springframework
.security
.ldap
.LdapUsernameToDnMapper
;
60 import org
.springframework
.security
.ldap
.userdetails
.UserDetailsContextMapper
;
62 /** Makes sure that LDAP and JCR are in line. */
63 public class JcrLdapSynchronizer
implements UserDetailsContextMapper
,
65 private final static Log log
= LogFactory
.getLog(JcrLdapSynchronizer
.class);
68 private LdapTemplate ldapTemplate
;
70 * LDAP template whose context source has an object factory set to null. see
72 * "http://forum.springsource.org/showthread.php?55955-Persistent-search-with-spring-ldap"
75 // private LdapTemplate rawLdapTemplate;
77 private String userBase
;
78 private String usernameAttribute
;
79 private String passwordAttribute
;
80 private String
[] userClasses
;
81 // private String defaultUserRole ="ROLE_USER";
83 // private NamingListener ldapUserListener;
84 // private SearchControls subTreeSearchControls;
85 private LdapUsernameToDnMapper usernameMapper
;
87 private PasswordEncoder passwordEncoder
;
88 private final Random random
;
91 /** Admin session on the main workspace */
92 private Session nodeSession
;
93 private Repository repository
;
95 // private JcrProfileListener jcrProfileListener;
96 private JcrSecurityModel jcrSecurityModel
= new SimpleJcrSecurityModel();
99 private Map
<String
, String
> propertyToAttributes
= new HashMap
<String
, String
>();
101 public JcrLdapSynchronizer() {
102 random
= createRandom();
107 nodeSession
= repository
.login();
109 // TODO put this in a different thread, and poll the LDAP server
115 // subTreeSearchControls = new SearchControls();
116 // subTreeSearchControls
117 // .setSearchScope(SearchControls.SUBTREE_SCOPE);
119 // ldapUserListener = new LdapUserListener();
120 // rawLdapTemplate.executeReadOnly(new ContextExecutor() {
121 // public Object executeWithContext(DirContext ctx)
122 // throws NamingException {
123 // EventDirContext ectx = (EventDirContext) ctx.lookup("");
124 // ectx.addNamingListener(userBase, "("
125 // + usernameAttribute + "=*)",
126 // subTreeSearchControls, ldapUserListener);
130 } catch (Exception e
) {
131 log
.error("Could not synchronize and listen to LDAP,"
132 + " probably because the LDAP server is not available."
133 + " Restart the system as soon as possible.", e
);
137 // String[] nodeTypes = { ArgeoTypes.ARGEO_USER_PROFILE };
138 // jcrProfileListener = new JcrProfileListener();
139 // noLocal is used so that we are not notified when we modify JCR
143 // .getObservationManager()
144 // .addEventListener(jcrProfileListener,
145 // Event.PROPERTY_CHANGED | Event.NODE_ADDED, "/",
146 // true, null, nodeTypes, true);
147 } catch (Exception e
) {
148 JcrUtils
.logoutQuietly(nodeSession
);
149 throw new ArgeoException("Cannot initialize LDAP/JCR synchronizer",
154 public void destroy() {
155 // JcrUtils.removeListenerQuietly(nodeSession, jcrProfileListener);
156 JcrUtils
.logoutQuietly(nodeSession
);
158 // rawLdapTemplate.executeReadOnly(new ContextExecutor() {
159 // public Object executeWithContext(DirContext ctx)
160 // throws NamingException {
161 // EventDirContext ectx = (EventDirContext) ctx.lookup("");
162 // ectx.removeNamingListener(ldapUserListener);
166 // } catch (Exception e) {
167 // // silent (LDAP server may have been shutdown already)
168 // if (log.isTraceEnabled())
169 // log.trace("Cannot remove LDAP listener", e);
176 /** Full synchronization between LDAP and JCR. LDAP has priority. */
177 protected void synchronize() {
179 Name userBaseName
= new DistinguishedName(userBase
);
180 // TODO subtree search?
181 @SuppressWarnings("unchecked")
182 List
<String
> userPaths
= (List
<String
>) ldapTemplate
.listBindings(
183 userBaseName
, new ContextMapper() {
184 public Object
mapFromContext(Object ctxObj
) {
186 return mapLdapToJcr((DirContextAdapter
) ctxObj
);
187 } catch (Exception e
) {
188 // do not break process because of error
190 "Could not LDAP->JCR synchronize user "
197 // create accounts which are not in LDAP
198 Query query
= nodeSession
202 "select * from [" + ArgeoTypes
.ARGEO_USER_PROFILE
203 + "]", Query
.JCR_SQL2
);
204 NodeIterator it
= query
.execute().getNodes();
205 while (it
.hasNext()) {
206 Node userProfile
= it
.nextNode();
207 String path
= userProfile
.getPath();
209 if (!userPaths
.contains(path
)) {
210 String username
= userProfile
211 .getProperty(ARGEO_USER_ID
).getString();
212 // GrantedAuthority[] authorities = {new
213 // GrantedAuthorityImpl(defaultUserRole)};
214 List
<GrantedAuthority
> authorities
= new ArrayList
<GrantedAuthority
>();
215 JcrUserDetails userDetails
= new JcrUserDetails(
216 userProfile
, username
, authorities
);
217 String dn
= createLdapUser(userDetails
);
218 log
.warn("Created ldap entry '" + dn
+ "' for user '"
221 // if(!userProfile.getProperty(ARGEO_ENABLED).getBoolean()){
222 // continue profiles;
227 // + " not found in LDAP, disabling user "
228 // + userProfile.getProperty(ArgeoNames.ARGEO_USER_ID)
231 // Temporary hack to repair previous behaviour
232 if (!userProfile
.getProperty(ARGEO_ENABLED
)
234 VersionManager versionManager
= nodeSession
235 .getWorkspace().getVersionManager();
236 versionManager
.checkout(userProfile
.getPath());
237 userProfile
.setProperty(ArgeoNames
.ARGEO_ENABLED
,
240 versionManager
.checkin(userProfile
.getPath());
243 } catch (Exception e
) {
244 log
.error("Cannot process " + path
, e
);
247 } catch (Exception e
) {
248 JcrUtils
.discardQuietly(nodeSession
);
249 log
.error("Cannot synchronize LDAP and JCR", e
);
250 // throw new ArgeoException("Cannot synchronize LDAP and JCR", e);
254 private String
createLdapUser(UserDetails user
) {
255 DirContextAdapter ctx
= new DirContextAdapter();
256 mapUserToContext(user
, ctx
);
257 DistinguishedName dn
= usernameMapper
.buildDn(user
.getUsername());
258 ldapTemplate
.bind(dn
, ctx
, null);
259 return dn
.toString();
262 /** Called during authentication in order to retrieve user details */
263 public UserDetails
mapUserFromContext(final DirContextOperations ctx
,
264 final String username
,
265 Collection
<?
extends GrantedAuthority
> authorities
) {
267 throw new ArgeoException("No LDAP information for user " + username
);
269 String ldapUsername
= ctx
.getStringAttribute(usernameAttribute
);
270 if (!ldapUsername
.equals(username
))
271 throw new ArgeoException("Logged in with username " + username
272 + " but LDAP user is " + ldapUsername
);
274 Node userProfile
= jcrSecurityModel
.sync(nodeSession
, username
,
275 SecurityUtils
.authoritiesToStringList(authorities
));
276 // JcrUserDetails.checkAccountStatus(userProfile);
279 SortedSet
<?
> passwordAttributes
= ctx
280 .getAttributeSortedStringSet(passwordAttribute
);
282 if (passwordAttributes
== null || passwordAttributes
.size() == 0) {
283 // throw new ArgeoException("No password found for user " +
287 byte[] arr
= (byte[]) passwordAttributes
.first();
288 password
= new String(arr
);
290 Arrays
.fill(arr
, (byte) 0);
294 return new JcrUserDetails(userProfile
, password
, authorities
);
295 } catch (RepositoryException e
) {
296 throw new ArgeoException("Cannot retrieve user details for "
302 * Writes an LDAP context to the JCR user profile.
304 * @return path to user profile
306 protected synchronized String
mapLdapToJcr(DirContextAdapter ctx
) {
307 Session session
= nodeSession
;
310 String username
= ctx
.getStringAttribute(usernameAttribute
);
312 Node userProfile
= jcrSecurityModel
.sync(session
, username
, null);
313 Map
<String
, String
> modifications
= new HashMap
<String
, String
>();
314 for (String jcrProperty
: propertyToAttributes
.keySet())
315 ldapToJcr(userProfile
, jcrProperty
, ctx
, modifications
);
317 int modifCount
= modifications
.size();
318 if (modifCount
> 0) {
319 session
.getWorkspace().getVersionManager()
320 .checkout(userProfile
.getPath());
321 for (String prop
: modifications
.keySet())
322 userProfile
.setProperty(prop
, modifications
.get(prop
));
323 JcrUtils
.updateLastModified(userProfile
);
325 session
.getWorkspace().getVersionManager()
326 .checkin(userProfile
.getPath());
327 if (log
.isDebugEnabled())
328 log
.debug("Mapped " + modifCount
+ " LDAP modification"
329 + (modifCount
== 1 ?
"" : "s") + " from "
330 + ctx
.getDn() + " to " + userProfile
);
332 return userProfile
.getPath();
333 } catch (Exception e
) {
334 JcrUtils
.discardQuietly(session
);
335 throw new ArgeoException("Cannot synchronize JCR and LDAP", e
);
339 /** Maps an LDAP property to a JCR property */
340 protected void ldapToJcr(Node userProfile
, String jcrProperty
,
341 DirContextOperations ctx
, Map
<String
, String
> modifications
) {
342 // TODO do we really need DirContextOperations?
344 String ldapAttribute
;
345 if (propertyToAttributes
.containsKey(jcrProperty
))
346 ldapAttribute
= propertyToAttributes
.get(jcrProperty
);
348 throw new ArgeoException(
349 "No LDAP attribute mapped for JCR proprty "
352 String value
= ctx
.getStringAttribute(ldapAttribute
);
353 String jcrValue
= userProfile
.hasProperty(jcrProperty
) ? userProfile
354 .getProperty(jcrProperty
).getString() : null;
355 if (value
!= null && jcrValue
!= null) {
356 if (!value
.equals(jcrValue
))
357 modifications
.put(jcrProperty
, value
);
358 } else if (value
!= null && jcrValue
== null) {
359 modifications
.put(jcrProperty
, value
);
360 } else if (value
== null && jcrValue
!= null) {
361 modifications
.put(jcrProperty
, value
);
363 } catch (Exception e
) {
364 throw new ArgeoException("Cannot map JCR property " + jcrProperty
373 public void mapUserToContext(UserDetails user
, final DirContextAdapter ctx
) {
374 if (!(user
instanceof JcrUserDetails
))
375 throw new ArgeoException("Unsupported user details: "
378 ctx
.setAttributeValues("objectClass", userClasses
);
379 ctx
.setAttributeValue(usernameAttribute
, user
.getUsername());
380 ctx
.setAttributeValue(passwordAttribute
,
381 encodePassword(user
.getPassword()));
383 final JcrUserDetails jcrUserDetails
= (JcrUserDetails
) user
;
385 Node userProfile
= nodeSession
386 .getNode(jcrUserDetails
.getHomePath()).getNode(
388 for (String jcrProperty
: propertyToAttributes
.keySet()) {
389 if (userProfile
.hasProperty(jcrProperty
)) {
390 ModificationItem mi
= jcrToLdap(jcrProperty
, userProfile
391 .getProperty(jcrProperty
).getString());
393 ctx
.setAttribute(mi
.getAttribute());
396 if (log
.isTraceEnabled())
397 log
.trace("Mapped " + userProfile
+ " to " + ctx
.getDn());
398 } catch (RepositoryException e
) {
399 throw new ArgeoException("Cannot synchronize JCR and LDAP", e
);
404 /** Maps a JCR property to an LDAP property */
405 protected ModificationItem
jcrToLdap(String jcrProperty
, String value
) {
406 // TODO do we really need DirContextOperations?
408 String ldapAttribute
;
409 if (propertyToAttributes
.containsKey(jcrProperty
))
410 ldapAttribute
= propertyToAttributes
.get(jcrProperty
);
414 // fix issue with empty 'sn' in LDAP
415 if (ldapAttribute
.equals("sn") && (value
.trim().equals("")))
417 // fix issue with empty 'description' in LDAP
418 if (ldapAttribute
.equals("description") && value
.trim().equals(""))
420 BasicAttribute attr
= new BasicAttribute(
421 propertyToAttributes
.get(jcrProperty
), value
);
422 ModificationItem mi
= new ModificationItem(
423 DirContext
.REPLACE_ATTRIBUTE
, attr
);
425 } catch (Exception e
) {
426 throw new ArgeoException("Cannot map JCR property " + jcrProperty
434 protected String
encodePassword(String password
) {
435 if (!password
.startsWith("{")) {
436 byte[] salt
= new byte[16];
437 random
.nextBytes(salt
);
438 return passwordEncoder
.encodePassword(password
, salt
);
444 private static Random
createRandom() {
446 return SecureRandom
.getInstance("SHA1PRNG");
447 } catch (NoSuchAlgorithmException e
) {
448 return new Random(System
.currentTimeMillis());
453 * DEPENDENCY INJECTION
456 public void setLdapTemplate(LdapTemplate ldapTemplate
) {
457 this.ldapTemplate
= ldapTemplate
;
460 public void setRawLdapTemplate(LdapTemplate rawLdapTemplate
) {
461 // this.rawLdapTemplate = rawLdapTemplate;
464 public void setRepository(Repository repository
) {
465 this.repository
= repository
;
468 public void setUserBase(String userBase
) {
469 this.userBase
= userBase
;
472 public void setUsernameAttribute(String usernameAttribute
) {
473 this.usernameAttribute
= usernameAttribute
;
476 public void setPropertyToAttributes(Map
<String
, String
> propertyToAttributes
) {
477 this.propertyToAttributes
= propertyToAttributes
;
480 public void setUsernameMapper(LdapUsernameToDnMapper usernameMapper
) {
481 this.usernameMapper
= usernameMapper
;
484 public void setPasswordAttribute(String passwordAttribute
) {
485 this.passwordAttribute
= passwordAttribute
;
488 public void setUserClasses(String
[] userClasses
) {
489 this.userClasses
= userClasses
;
492 public void setPasswordEncoder(PasswordEncoder passwordEncoder
) {
493 this.passwordEncoder
= passwordEncoder
;
496 public void setJcrSecurityModel(JcrSecurityModel jcrSecurityModel
) {
497 this.jcrSecurityModel
= jcrSecurityModel
;
500 /** Listen to LDAP */
501 // class LdapUserListener implements ObjectChangeListener,
502 // NamespaceChangeListener, UnsolicitedNotificationListener {
504 // public void namingExceptionThrown(NamingExceptionEvent evt) {
505 // evt.getException().printStackTrace();
508 // public void objectChanged(NamingEvent evt) {
509 // Binding user = evt.getNewBinding();
510 // // TODO find a way not to be called when JCR is the source of the
512 // DirContextAdapter ctx = (DirContextAdapter) ldapTemplate
513 // .lookup(user.getName());
514 // mapLdapToJcr(ctx);
517 // public void objectAdded(NamingEvent evt) {
518 // Binding user = evt.getNewBinding();
519 // DirContextAdapter ctx = (DirContextAdapter) ldapTemplate
520 // .lookup(user.getName());
521 // mapLdapToJcr(ctx);
524 // public void objectRemoved(NamingEvent evt) {
525 // if (log.isDebugEnabled())
529 // public void objectRenamed(NamingEvent evt) {
530 // if (log.isDebugEnabled())
534 // public void notificationReceived(UnsolicitedNotificationEvent evt) {
535 // UnsolicitedNotification notification = evt.getNotification();
536 // NamingException ne = notification.getException();
537 // String msg = "LDAP notification " + "ID=" + notification.getID()
538 // + ", referrals=" + notification.getReferrals();
540 // if (log.isTraceEnabled())
541 // log.trace(msg + ", exception= " + ne, ne);
543 // log.warn(msg + ", exception= " + ne);
544 // } else if (log.isDebugEnabled()) {
545 // log.debug("Unsollicited LDAP notification " + msg);
552 // class JcrProfileListener implements EventListener {
554 // public void onEvent(EventIterator events) {
556 // final Map<Name, List<ModificationItem>> modifications = new HashMap<Name,
557 // List<ModificationItem>>();
558 // while (events.hasNext()) {
559 // Event event = events.nextEvent();
561 // if (Event.PROPERTY_CHANGED == event.getType()) {
562 // Property property = (Property) nodeSession
563 // .getItem(event.getPath());
564 // String propertyName = property.getName();
565 // Node userProfile = property.getParent();
566 // String username = userProfile.getProperty(
567 // ARGEO_USER_ID).getString();
568 // if (propertyToAttributes.containsKey(propertyName)) {
569 // Name name = usernameMapper.buildDn(username);
570 // if (!modifications.containsKey(name))
571 // modifications.put(name,
572 // new ArrayList<ModificationItem>());
573 // String value = property.getString();
574 // ModificationItem mi = jcrToLdap(propertyName,
577 // modifications.get(name).add(mi);
579 // } else if (Event.NODE_ADDED == event.getType()) {
580 // Node userProfile = nodeSession.getNode(event
582 // String username = userProfile.getProperty(
583 // ARGEO_USER_ID).getString();
584 // Name name = usernameMapper.buildDn(username);
585 // for (String propertyName : propertyToAttributes
587 // if (!modifications.containsKey(name))
588 // modifications.put(name,
589 // new ArrayList<ModificationItem>());
590 // String value = userProfile.getProperty(
591 // propertyName).getString();
592 // ModificationItem mi = jcrToLdap(propertyName,
595 // modifications.get(name).add(mi);
598 // } catch (RepositoryException e) {
599 // throw new ArgeoException("Cannot process event "
604 // for (Name name : modifications.keySet()) {
605 // List<ModificationItem> userModifs = modifications.get(name);
606 // int modifCount = userModifs.size();
607 // ldapTemplate.modifyAttributes(name, userModifs
608 // .toArray(new ModificationItem[modifCount]));
609 // if (log.isDebugEnabled())
610 // log.debug("Mapped " + modifCount + " JCR modification"
611 // + (modifCount == 1 ? "" : "s") + " to " + name);
613 // } catch (Exception e) {
614 // // if (log.isDebugEnabled())
615 // // e.printStackTrace();
616 // throw new ArgeoException("Cannot process JCR events ("
617 // + e.getMessage() + ")", e);