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
.Arrays
;
21 import java
.util
.HashMap
;
22 import java
.util
.List
;
24 import java
.util
.Random
;
25 import java
.util
.SortedSet
;
26 import java
.util
.UUID
;
28 import javax
.jcr
.Node
;
29 import javax
.jcr
.NodeIterator
;
30 import javax
.jcr
.Repository
;
31 import javax
.jcr
.RepositoryException
;
32 import javax
.jcr
.Session
;
33 import javax
.jcr
.query
.Query
;
34 import javax
.jcr
.version
.VersionManager
;
35 import javax
.naming
.Name
;
36 import javax
.naming
.directory
.BasicAttribute
;
37 import javax
.naming
.directory
.DirContext
;
38 import javax
.naming
.directory
.ModificationItem
;
40 import org
.apache
.commons
.logging
.Log
;
41 import org
.apache
.commons
.logging
.LogFactory
;
42 import org
.argeo
.ArgeoException
;
43 import org
.argeo
.jcr
.ArgeoNames
;
44 import org
.argeo
.jcr
.ArgeoTypes
;
45 import org
.argeo
.jcr
.JcrUtils
;
46 import org
.argeo
.security
.SecurityUtils
;
47 import org
.argeo
.security
.jcr
.JcrSecurityModel
;
48 import org
.argeo
.security
.jcr
.JcrUserDetails
;
49 import org
.argeo
.security
.jcr
.SimpleJcrSecurityModel
;
50 import org
.springframework
.ldap
.core
.ContextMapper
;
51 import org
.springframework
.ldap
.core
.DirContextAdapter
;
52 import org
.springframework
.ldap
.core
.DirContextOperations
;
53 import org
.springframework
.ldap
.core
.DistinguishedName
;
54 import org
.springframework
.ldap
.core
.LdapTemplate
;
55 import org
.springframework
.security
.GrantedAuthority
;
56 import org
.springframework
.security
.ldap
.LdapUsernameToDnMapper
;
57 import org
.springframework
.security
.providers
.encoding
.PasswordEncoder
;
58 import org
.springframework
.security
.userdetails
.UserDetails
;
59 import org
.springframework
.security
.userdetails
.ldap
.UserDetailsContextMapper
;
61 /** Makes sure that LDAP and JCR are in line. */
62 public class JcrLdapSynchronizer
implements UserDetailsContextMapper
,
64 private final static Log log
= LogFactory
.getLog(JcrLdapSynchronizer
.class);
67 private LdapTemplate ldapTemplate
;
69 * LDAP template whose context source has an object factory set to null. see
71 * "http://forum.springsource.org/showthread.php?55955-Persistent-search-with-spring-ldap"
74 // private LdapTemplate rawLdapTemplate;
76 private String userBase
;
77 private String usernameAttribute
;
78 private String passwordAttribute
;
79 private String
[] userClasses
;
80 // private String defaultUserRole ="ROLE_USER";
82 // private NamingListener ldapUserListener;
83 // private SearchControls subTreeSearchControls;
84 private LdapUsernameToDnMapper usernameMapper
;
86 private PasswordEncoder passwordEncoder
;
87 private final Random random
;
90 /** Admin session on the main workspace */
91 private Session nodeSession
;
92 private Repository repository
;
94 // private JcrProfileListener jcrProfileListener;
95 private JcrSecurityModel jcrSecurityModel
= new SimpleJcrSecurityModel();
98 private Map
<String
, String
> propertyToAttributes
= new HashMap
<String
, String
>();
100 public JcrLdapSynchronizer() {
101 random
= createRandom();
106 nodeSession
= repository
.login();
108 // TODO put this in a different thread, and poll the LDAP server
114 // subTreeSearchControls = new SearchControls();
115 // subTreeSearchControls
116 // .setSearchScope(SearchControls.SUBTREE_SCOPE);
118 // ldapUserListener = new LdapUserListener();
119 // rawLdapTemplate.executeReadOnly(new ContextExecutor() {
120 // public Object executeWithContext(DirContext ctx)
121 // throws NamingException {
122 // EventDirContext ectx = (EventDirContext) ctx.lookup("");
123 // ectx.addNamingListener(userBase, "("
124 // + usernameAttribute + "=*)",
125 // subTreeSearchControls, ldapUserListener);
129 } catch (Exception e
) {
130 log
.error("Could not synchronize and listen to LDAP,"
131 + " probably because the LDAP server is not available."
132 + " Restart the system as soon as possible.", e
);
136 // String[] nodeTypes = { ArgeoTypes.ARGEO_USER_PROFILE };
137 // jcrProfileListener = new JcrProfileListener();
138 // noLocal is used so that we are not notified when we modify JCR
142 // .getObservationManager()
143 // .addEventListener(jcrProfileListener,
144 // Event.PROPERTY_CHANGED | Event.NODE_ADDED, "/",
145 // true, null, nodeTypes, true);
146 } catch (Exception e
) {
147 JcrUtils
.logoutQuietly(nodeSession
);
148 throw new ArgeoException("Cannot initialize LDAP/JCR synchronizer",
153 public void destroy() {
154 // JcrUtils.removeListenerQuietly(nodeSession, jcrProfileListener);
155 JcrUtils
.logoutQuietly(nodeSession
);
157 // rawLdapTemplate.executeReadOnly(new ContextExecutor() {
158 // public Object executeWithContext(DirContext ctx)
159 // throws NamingException {
160 // EventDirContext ectx = (EventDirContext) ctx.lookup("");
161 // ectx.removeNamingListener(ldapUserListener);
165 // } catch (Exception e) {
166 // // silent (LDAP server may have been shutdown already)
167 // if (log.isTraceEnabled())
168 // log.trace("Cannot remove LDAP listener", e);
175 /** Full synchronization between LDAP and JCR. LDAP has priority. */
176 protected void synchronize() {
178 Name userBaseName
= new DistinguishedName(userBase
);
179 // TODO subtree search?
180 @SuppressWarnings("unchecked")
181 List
<String
> userPaths
= (List
<String
>) ldapTemplate
.listBindings(
182 userBaseName
, new ContextMapper() {
183 public Object
mapFromContext(Object ctxObj
) {
185 return mapLdapToJcr((DirContextAdapter
) ctxObj
);
186 } catch (Exception e
) {
187 // do not break process because of error
189 "Could not LDAP->JCR synchronize user "
196 // create accounts which are not in LDAP
197 Query query
= nodeSession
201 "select * from [" + ArgeoTypes
.ARGEO_USER_PROFILE
202 + "]", Query
.JCR_SQL2
);
203 NodeIterator it
= query
.execute().getNodes();
204 while (it
.hasNext()) {
205 Node userProfile
= it
.nextNode();
206 String path
= userProfile
.getPath();
208 if (!userPaths
.contains(path
)) {
209 String username
= userProfile
210 .getProperty(ARGEO_USER_ID
).getString();
211 // GrantedAuthority[] authorities = {new
212 // GrantedAuthorityImpl(defaultUserRole)};
213 GrantedAuthority
[] authorities
= {};
214 JcrUserDetails userDetails
= new JcrUserDetails(
215 userProfile
, username
, authorities
);
216 String dn
= createLdapUser(userDetails
);
217 log
.warn("Created ldap entry '" + dn
+ "' for user '"
220 // if(!userProfile.getProperty(ARGEO_ENABLED).getBoolean()){
221 // continue profiles;
226 // + " not found in LDAP, disabling user "
227 // + userProfile.getProperty(ArgeoNames.ARGEO_USER_ID)
230 // Temporary hack to repair previous behaviour
231 if (!userProfile
.getProperty(ARGEO_ENABLED
)
233 VersionManager versionManager
= nodeSession
234 .getWorkspace().getVersionManager();
235 versionManager
.checkout(userProfile
.getPath());
236 userProfile
.setProperty(ArgeoNames
.ARGEO_ENABLED
,
239 versionManager
.checkin(userProfile
.getPath());
242 } catch (Exception e
) {
243 log
.error("Cannot process " + path
, e
);
246 } catch (Exception e
) {
247 JcrUtils
.discardQuietly(nodeSession
);
248 log
.error("Cannot synchronize LDAP and JCR", e
);
249 // throw new ArgeoException("Cannot synchronize LDAP and JCR", e);
253 private String
createLdapUser(UserDetails user
) {
254 DirContextAdapter ctx
= new DirContextAdapter();
255 mapUserToContext(user
, ctx
);
256 DistinguishedName dn
= usernameMapper
.buildDn(user
.getUsername());
257 ldapTemplate
.bind(dn
, ctx
, null);
258 return dn
.toString();
261 /** Called during authentication in order to retrieve user details */
262 public UserDetails
mapUserFromContext(final DirContextOperations ctx
,
263 final String username
, GrantedAuthority
[] authorities
) {
265 throw new ArgeoException("No LDAP information for user " + username
);
267 String ldapUsername
= ctx
.getStringAttribute(usernameAttribute
);
268 if (!ldapUsername
.equals(username
))
269 throw new ArgeoException("Logged in with username " + username
270 + " but LDAP user is " + ldapUsername
);
272 Node userProfile
= jcrSecurityModel
.sync(nodeSession
, username
,
273 SecurityUtils
.authoritiesToStringList(authorities
));
274 // JcrUserDetails.checkAccountStatus(userProfile);
277 SortedSet
<?
> passwordAttributes
= ctx
278 .getAttributeSortedStringSet(passwordAttribute
);
280 if (passwordAttributes
== null || passwordAttributes
.size() == 0) {
281 //throw new ArgeoException("No password found for user " + username);
284 byte[] arr
= (byte[]) passwordAttributes
.first();
285 password
= new String(arr
);
287 Arrays
.fill(arr
, (byte) 0);
291 return new JcrUserDetails(userProfile
, password
, authorities
);
292 } catch (RepositoryException e
) {
293 throw new ArgeoException("Cannot retrieve user details for "
299 * Writes an LDAP context to the JCR user profile.
301 * @return path to user profile
303 protected synchronized String
mapLdapToJcr(DirContextAdapter ctx
) {
304 Session session
= nodeSession
;
307 String username
= ctx
.getStringAttribute(usernameAttribute
);
309 Node userProfile
= jcrSecurityModel
.sync(session
, username
, null);
310 Map
<String
, String
> modifications
= new HashMap
<String
, String
>();
311 for (String jcrProperty
: propertyToAttributes
.keySet())
312 ldapToJcr(userProfile
, jcrProperty
, ctx
, modifications
);
314 int modifCount
= modifications
.size();
315 if (modifCount
> 0) {
316 session
.getWorkspace().getVersionManager()
317 .checkout(userProfile
.getPath());
318 for (String prop
: modifications
.keySet())
319 userProfile
.setProperty(prop
, modifications
.get(prop
));
320 JcrUtils
.updateLastModified(userProfile
);
322 session
.getWorkspace().getVersionManager()
323 .checkin(userProfile
.getPath());
324 if (log
.isDebugEnabled())
325 log
.debug("Mapped " + modifCount
+ " LDAP modification"
326 + (modifCount
== 1 ?
"" : "s") + " from "
327 + ctx
.getDn() + " to " + userProfile
);
329 return userProfile
.getPath();
330 } catch (Exception e
) {
331 JcrUtils
.discardQuietly(session
);
332 throw new ArgeoException("Cannot synchronize JCR and LDAP", e
);
336 /** Maps an LDAP property to a JCR property */
337 protected void ldapToJcr(Node userProfile
, String jcrProperty
,
338 DirContextOperations ctx
, Map
<String
, String
> modifications
) {
339 // TODO do we really need DirContextOperations?
341 String ldapAttribute
;
342 if (propertyToAttributes
.containsKey(jcrProperty
))
343 ldapAttribute
= propertyToAttributes
.get(jcrProperty
);
345 throw new ArgeoException(
346 "No LDAP attribute mapped for JCR proprty "
349 String value
= ctx
.getStringAttribute(ldapAttribute
);
350 String jcrValue
= userProfile
.hasProperty(jcrProperty
) ? userProfile
351 .getProperty(jcrProperty
).getString() : null;
352 if (value
!= null && jcrValue
!= null) {
353 if (!value
.equals(jcrValue
))
354 modifications
.put(jcrProperty
, value
);
355 } else if (value
!= null && jcrValue
== null) {
356 modifications
.put(jcrProperty
, value
);
357 } else if (value
== null && jcrValue
!= null) {
358 modifications
.put(jcrProperty
, value
);
360 } catch (Exception e
) {
361 throw new ArgeoException("Cannot map JCR property " + jcrProperty
370 public void mapUserToContext(UserDetails user
, final DirContextAdapter ctx
) {
371 if (!(user
instanceof JcrUserDetails
))
372 throw new ArgeoException("Unsupported user details: "
375 ctx
.setAttributeValues("objectClass", userClasses
);
376 ctx
.setAttributeValue(usernameAttribute
, user
.getUsername());
377 ctx
.setAttributeValue(passwordAttribute
,
378 encodePassword(user
.getPassword()));
380 final JcrUserDetails jcrUserDetails
= (JcrUserDetails
) user
;
382 Node userProfile
= nodeSession
383 .getNode(jcrUserDetails
.getHomePath()).getNode(
385 for (String jcrProperty
: propertyToAttributes
.keySet()) {
386 if (userProfile
.hasProperty(jcrProperty
)) {
387 ModificationItem mi
= jcrToLdap(jcrProperty
, userProfile
388 .getProperty(jcrProperty
).getString());
390 ctx
.setAttribute(mi
.getAttribute());
393 if (log
.isTraceEnabled())
394 log
.trace("Mapped " + userProfile
+ " to " + ctx
.getDn());
395 } catch (RepositoryException e
) {
396 throw new ArgeoException("Cannot synchronize JCR and LDAP", e
);
401 /** Maps a JCR property to an LDAP property */
402 protected ModificationItem
jcrToLdap(String jcrProperty
, String value
) {
403 // TODO do we really need DirContextOperations?
405 String ldapAttribute
;
406 if (propertyToAttributes
.containsKey(jcrProperty
))
407 ldapAttribute
= propertyToAttributes
.get(jcrProperty
);
411 // fix issue with empty 'sn' in LDAP
412 if (ldapAttribute
.equals("sn") && (value
.trim().equals("")))
414 // fix issue with empty 'description' in LDAP
415 if (ldapAttribute
.equals("description") && value
.trim().equals(""))
417 BasicAttribute attr
= new BasicAttribute(
418 propertyToAttributes
.get(jcrProperty
), value
);
419 ModificationItem mi
= new ModificationItem(
420 DirContext
.REPLACE_ATTRIBUTE
, attr
);
422 } catch (Exception e
) {
423 throw new ArgeoException("Cannot map JCR property " + jcrProperty
431 protected String
encodePassword(String password
) {
432 if (!password
.startsWith("{")) {
433 byte[] salt
= new byte[16];
434 random
.nextBytes(salt
);
435 return passwordEncoder
.encodePassword(password
, salt
);
441 private static Random
createRandom() {
443 return SecureRandom
.getInstance("SHA1PRNG");
444 } catch (NoSuchAlgorithmException e
) {
445 return new Random(System
.currentTimeMillis());
450 * DEPENDENCY INJECTION
453 public void setLdapTemplate(LdapTemplate ldapTemplate
) {
454 this.ldapTemplate
= ldapTemplate
;
457 public void setRawLdapTemplate(LdapTemplate rawLdapTemplate
) {
458 // this.rawLdapTemplate = rawLdapTemplate;
461 public void setRepository(Repository repository
) {
462 this.repository
= repository
;
465 public void setUserBase(String userBase
) {
466 this.userBase
= userBase
;
469 public void setUsernameAttribute(String usernameAttribute
) {
470 this.usernameAttribute
= usernameAttribute
;
473 public void setPropertyToAttributes(Map
<String
, String
> propertyToAttributes
) {
474 this.propertyToAttributes
= propertyToAttributes
;
477 public void setUsernameMapper(LdapUsernameToDnMapper usernameMapper
) {
478 this.usernameMapper
= usernameMapper
;
481 public void setPasswordAttribute(String passwordAttribute
) {
482 this.passwordAttribute
= passwordAttribute
;
485 public void setUserClasses(String
[] userClasses
) {
486 this.userClasses
= userClasses
;
489 public void setPasswordEncoder(PasswordEncoder passwordEncoder
) {
490 this.passwordEncoder
= passwordEncoder
;
493 public void setJcrSecurityModel(JcrSecurityModel jcrSecurityModel
) {
494 this.jcrSecurityModel
= jcrSecurityModel
;
497 /** Listen to LDAP */
498 // class LdapUserListener implements ObjectChangeListener,
499 // NamespaceChangeListener, UnsolicitedNotificationListener {
501 // public void namingExceptionThrown(NamingExceptionEvent evt) {
502 // evt.getException().printStackTrace();
505 // public void objectChanged(NamingEvent evt) {
506 // Binding user = evt.getNewBinding();
507 // // TODO find a way not to be called when JCR is the source of the
509 // DirContextAdapter ctx = (DirContextAdapter) ldapTemplate
510 // .lookup(user.getName());
511 // mapLdapToJcr(ctx);
514 // public void objectAdded(NamingEvent evt) {
515 // Binding user = evt.getNewBinding();
516 // DirContextAdapter ctx = (DirContextAdapter) ldapTemplate
517 // .lookup(user.getName());
518 // mapLdapToJcr(ctx);
521 // public void objectRemoved(NamingEvent evt) {
522 // if (log.isDebugEnabled())
526 // public void objectRenamed(NamingEvent evt) {
527 // if (log.isDebugEnabled())
531 // public void notificationReceived(UnsolicitedNotificationEvent evt) {
532 // UnsolicitedNotification notification = evt.getNotification();
533 // NamingException ne = notification.getException();
534 // String msg = "LDAP notification " + "ID=" + notification.getID()
535 // + ", referrals=" + notification.getReferrals();
537 // if (log.isTraceEnabled())
538 // log.trace(msg + ", exception= " + ne, ne);
540 // log.warn(msg + ", exception= " + ne);
541 // } else if (log.isDebugEnabled()) {
542 // log.debug("Unsollicited LDAP notification " + msg);
549 // class JcrProfileListener implements EventListener {
551 // public void onEvent(EventIterator events) {
553 // final Map<Name, List<ModificationItem>> modifications = new HashMap<Name,
554 // List<ModificationItem>>();
555 // while (events.hasNext()) {
556 // Event event = events.nextEvent();
558 // if (Event.PROPERTY_CHANGED == event.getType()) {
559 // Property property = (Property) nodeSession
560 // .getItem(event.getPath());
561 // String propertyName = property.getName();
562 // Node userProfile = property.getParent();
563 // String username = userProfile.getProperty(
564 // ARGEO_USER_ID).getString();
565 // if (propertyToAttributes.containsKey(propertyName)) {
566 // Name name = usernameMapper.buildDn(username);
567 // if (!modifications.containsKey(name))
568 // modifications.put(name,
569 // new ArrayList<ModificationItem>());
570 // String value = property.getString();
571 // ModificationItem mi = jcrToLdap(propertyName,
574 // modifications.get(name).add(mi);
576 // } else if (Event.NODE_ADDED == event.getType()) {
577 // Node userProfile = nodeSession.getNode(event
579 // String username = userProfile.getProperty(
580 // ARGEO_USER_ID).getString();
581 // Name name = usernameMapper.buildDn(username);
582 // for (String propertyName : propertyToAttributes
584 // if (!modifications.containsKey(name))
585 // modifications.put(name,
586 // new ArrayList<ModificationItem>());
587 // String value = userProfile.getProperty(
588 // propertyName).getString();
589 // ModificationItem mi = jcrToLdap(propertyName,
592 // modifications.get(name).add(mi);
595 // } catch (RepositoryException e) {
596 // throw new ArgeoException("Cannot process event "
601 // for (Name name : modifications.keySet()) {
602 // List<ModificationItem> userModifs = modifications.get(name);
603 // int modifCount = userModifs.size();
604 // ldapTemplate.modifyAttributes(name, userModifs
605 // .toArray(new ModificationItem[modifCount]));
606 // if (log.isDebugEnabled())
607 // log.debug("Mapped " + modifCount + " JCR modification"
608 // + (modifCount == 1 ? "" : "s") + " to " + name);
610 // } catch (Exception e) {
611 // // if (log.isDebugEnabled())
612 // // e.printStackTrace();
613 // throw new ArgeoException("Cannot process JCR events ("
614 // + e.getMessage() + ")", e);