]> git.argeo.org Git - lgpl/argeo-commons.git/blob - JcrLdapSynchronizer.java
e0519c37c1762808b6c3e88ec4ada9f685e6443f
[lgpl/argeo-commons.git] / JcrLdapSynchronizer.java
1 /*
2 * Copyright (C) 2007-2012 Argeo GmbH
3 *
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
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
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.
15 */
16 package org.argeo.security.ldap.jcr;
17
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;
25 import java.util.Map;
26 import java.util.Random;
27 import java.util.SortedSet;
28
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;
40
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;
61
62 /** Makes sure that LDAP and JCR are in line. */
63 public class JcrLdapSynchronizer implements UserDetailsContextMapper,
64 ArgeoNames {
65 private final static Log log = LogFactory.getLog(JcrLdapSynchronizer.class);
66
67 // LDAP
68 private LdapTemplate ldapTemplate;
69 /**
70 * LDAP template whose context source has an object factory set to null. see
71 * <a href=
72 * "http://forum.springsource.org/showthread.php?55955-Persistent-search-with-spring-ldap"
73 * >this</a>
74 */
75 // private LdapTemplate rawLdapTemplate;
76
77 private String userBase;
78 private String usernameAttribute;
79 private String passwordAttribute;
80 private String[] userClasses;
81 // private String defaultUserRole ="ROLE_USER";
82
83 // private NamingListener ldapUserListener;
84 // private SearchControls subTreeSearchControls;
85 private LdapUsernameToDnMapper usernameMapper;
86
87 private PasswordEncoder passwordEncoder;
88 private final Random random;
89
90 // JCR
91 /** Admin session on the main workspace */
92 private Session nodeSession;
93 private Repository repository;
94
95 // private JcrProfileListener jcrProfileListener;
96 private JcrSecurityModel jcrSecurityModel = new SimpleJcrSecurityModel();
97
98 // Mapping
99 private Map<String, String> propertyToAttributes = new HashMap<String, String>();
100
101 public JcrLdapSynchronizer() {
102 random = createRandom();
103 }
104
105 public void init() {
106 try {
107 nodeSession = repository.login();
108
109 // TODO put this in a different thread, and poll the LDAP server
110 // until it is up
111 try {
112 synchronize();
113
114 // LDAP
115 // subTreeSearchControls = new SearchControls();
116 // subTreeSearchControls
117 // .setSearchScope(SearchControls.SUBTREE_SCOPE);
118 // LDAP listener
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);
127 // return null;
128 // }
129 // });
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);
134 }
135
136 // JCR
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
140 // from LDAP
141 // nodeSession
142 // .getWorkspace()
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",
150 e);
151 }
152 }
153
154 public void destroy() {
155 // JcrUtils.removeListenerQuietly(nodeSession, jcrProfileListener);
156 JcrUtils.logoutQuietly(nodeSession);
157 // try {
158 // rawLdapTemplate.executeReadOnly(new ContextExecutor() {
159 // public Object executeWithContext(DirContext ctx)
160 // throws NamingException {
161 // EventDirContext ectx = (EventDirContext) ctx.lookup("");
162 // ectx.removeNamingListener(ldapUserListener);
163 // return null;
164 // }
165 // });
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);
170 // }
171 }
172
173 /*
174 * LDAP TO JCR
175 */
176 /** Full synchronization between LDAP and JCR. LDAP has priority. */
177 protected void synchronize() {
178 try {
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) {
185 try {
186 return mapLdapToJcr((DirContextAdapter) ctxObj);
187 } catch (Exception e) {
188 // do not break process because of error
189 log.error(
190 "Could not LDAP->JCR synchronize user "
191 + ctxObj, e);
192 return null;
193 }
194 }
195 });
196
197 // create accounts which are not in LDAP
198 Query query = nodeSession
199 .getWorkspace()
200 .getQueryManager()
201 .createQuery(
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();
208 try {
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 '"
219 + username + "'");
220
221 // if(!userProfile.getProperty(ARGEO_ENABLED).getBoolean()){
222 // continue profiles;
223 // }
224 //
225 // log.warn("Path "
226 // + path
227 // + " not found in LDAP, disabling user "
228 // + userProfile.getProperty(ArgeoNames.ARGEO_USER_ID)
229 // .getString());
230
231 // Temporary hack to repair previous behaviour
232 if (!userProfile.getProperty(ARGEO_ENABLED)
233 .getBoolean()) {
234 VersionManager versionManager = nodeSession
235 .getWorkspace().getVersionManager();
236 versionManager.checkout(userProfile.getPath());
237 userProfile.setProperty(ArgeoNames.ARGEO_ENABLED,
238 true);
239 nodeSession.save();
240 versionManager.checkin(userProfile.getPath());
241 }
242 }
243 } catch (Exception e) {
244 log.error("Cannot process " + path, e);
245 }
246 }
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);
251 }
252 }
253
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();
260 }
261
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) {
266 if (ctx == null)
267 throw new ArgeoException("No LDAP information for user " + username);
268
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);
273
274 Node userProfile = jcrSecurityModel.sync(nodeSession, username,
275 SecurityUtils.authoritiesToStringList(authorities));
276 // JcrUserDetails.checkAccountStatus(userProfile);
277
278 // password
279 SortedSet<?> passwordAttributes = ctx
280 .getAttributeSortedStringSet(passwordAttribute);
281 String password;
282 if (passwordAttributes == null || passwordAttributes.size() == 0) {
283 // throw new ArgeoException("No password found for user " +
284 // username);
285 password = "NULL";
286 } else {
287 byte[] arr = (byte[]) passwordAttributes.first();
288 password = new String(arr);
289 // erase password
290 Arrays.fill(arr, (byte) 0);
291 }
292
293 try {
294 return new JcrUserDetails(userProfile, password, authorities);
295 } catch (RepositoryException e) {
296 throw new ArgeoException("Cannot retrieve user details for "
297 + username, e);
298 }
299 }
300
301 /**
302 * Writes an LDAP context to the JCR user profile.
303 *
304 * @return path to user profile
305 */
306 protected synchronized String mapLdapToJcr(DirContextAdapter ctx) {
307 Session session = nodeSession;
308 try {
309 // process
310 String username = ctx.getStringAttribute(usernameAttribute);
311
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);
316
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);
324 session.save();
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);
331 }
332 return userProfile.getPath();
333 } catch (Exception e) {
334 JcrUtils.discardQuietly(session);
335 throw new ArgeoException("Cannot synchronize JCR and LDAP", e);
336 }
337 }
338
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?
343 try {
344 String ldapAttribute;
345 if (propertyToAttributes.containsKey(jcrProperty))
346 ldapAttribute = propertyToAttributes.get(jcrProperty);
347 else
348 throw new ArgeoException(
349 "No LDAP attribute mapped for JCR proprty "
350 + jcrProperty);
351
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);
362 }
363 } catch (Exception e) {
364 throw new ArgeoException("Cannot map JCR property " + jcrProperty
365 + " from LDAP", e);
366 }
367 }
368
369 /*
370 * JCR to LDAP
371 */
372
373 public void mapUserToContext(UserDetails user, final DirContextAdapter ctx) {
374 if (!(user instanceof JcrUserDetails))
375 throw new ArgeoException("Unsupported user details: "
376 + user.getClass());
377
378 ctx.setAttributeValues("objectClass", userClasses);
379 ctx.setAttributeValue(usernameAttribute, user.getUsername());
380 ctx.setAttributeValue(passwordAttribute,
381 encodePassword(user.getPassword()));
382
383 final JcrUserDetails jcrUserDetails = (JcrUserDetails) user;
384 try {
385 Node userProfile = nodeSession
386 .getNode(jcrUserDetails.getHomePath()).getNode(
387 ARGEO_PROFILE);
388 for (String jcrProperty : propertyToAttributes.keySet()) {
389 if (userProfile.hasProperty(jcrProperty)) {
390 ModificationItem mi = jcrToLdap(jcrProperty, userProfile
391 .getProperty(jcrProperty).getString());
392 if (mi != null)
393 ctx.setAttribute(mi.getAttribute());
394 }
395 }
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);
400 }
401
402 }
403
404 /** Maps a JCR property to an LDAP property */
405 protected ModificationItem jcrToLdap(String jcrProperty, String value) {
406 // TODO do we really need DirContextOperations?
407 try {
408 String ldapAttribute;
409 if (propertyToAttributes.containsKey(jcrProperty))
410 ldapAttribute = propertyToAttributes.get(jcrProperty);
411 else
412 return null;
413
414 // fix issue with empty 'sn' in LDAP
415 if (ldapAttribute.equals("sn") && (value.trim().equals("")))
416 return null;
417 // fix issue with empty 'description' in LDAP
418 if (ldapAttribute.equals("description") && value.trim().equals(""))
419 return null;
420 BasicAttribute attr = new BasicAttribute(
421 propertyToAttributes.get(jcrProperty), value);
422 ModificationItem mi = new ModificationItem(
423 DirContext.REPLACE_ATTRIBUTE, attr);
424 return mi;
425 } catch (Exception e) {
426 throw new ArgeoException("Cannot map JCR property " + jcrProperty
427 + " from LDAP", e);
428 }
429 }
430
431 /*
432 * UTILITIES
433 */
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);
439 } else {
440 return password;
441 }
442 }
443
444 private static Random createRandom() {
445 try {
446 return SecureRandom.getInstance("SHA1PRNG");
447 } catch (NoSuchAlgorithmException e) {
448 return new Random(System.currentTimeMillis());
449 }
450 }
451
452 /*
453 * DEPENDENCY INJECTION
454 */
455
456 public void setLdapTemplate(LdapTemplate ldapTemplate) {
457 this.ldapTemplate = ldapTemplate;
458 }
459
460 public void setRawLdapTemplate(LdapTemplate rawLdapTemplate) {
461 // this.rawLdapTemplate = rawLdapTemplate;
462 }
463
464 public void setRepository(Repository repository) {
465 this.repository = repository;
466 }
467
468 public void setUserBase(String userBase) {
469 this.userBase = userBase;
470 }
471
472 public void setUsernameAttribute(String usernameAttribute) {
473 this.usernameAttribute = usernameAttribute;
474 }
475
476 public void setPropertyToAttributes(Map<String, String> propertyToAttributes) {
477 this.propertyToAttributes = propertyToAttributes;
478 }
479
480 public void setUsernameMapper(LdapUsernameToDnMapper usernameMapper) {
481 this.usernameMapper = usernameMapper;
482 }
483
484 public void setPasswordAttribute(String passwordAttribute) {
485 this.passwordAttribute = passwordAttribute;
486 }
487
488 public void setUserClasses(String[] userClasses) {
489 this.userClasses = userClasses;
490 }
491
492 public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
493 this.passwordEncoder = passwordEncoder;
494 }
495
496 public void setJcrSecurityModel(JcrSecurityModel jcrSecurityModel) {
497 this.jcrSecurityModel = jcrSecurityModel;
498 }
499
500 /** Listen to LDAP */
501 // class LdapUserListener implements ObjectChangeListener,
502 // NamespaceChangeListener, UnsolicitedNotificationListener {
503 //
504 // public void namingExceptionThrown(NamingExceptionEvent evt) {
505 // evt.getException().printStackTrace();
506 // }
507 //
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
511 // // modification
512 // DirContextAdapter ctx = (DirContextAdapter) ldapTemplate
513 // .lookup(user.getName());
514 // mapLdapToJcr(ctx);
515 // }
516 //
517 // public void objectAdded(NamingEvent evt) {
518 // Binding user = evt.getNewBinding();
519 // DirContextAdapter ctx = (DirContextAdapter) ldapTemplate
520 // .lookup(user.getName());
521 // mapLdapToJcr(ctx);
522 // }
523 //
524 // public void objectRemoved(NamingEvent evt) {
525 // if (log.isDebugEnabled())
526 // log.debug(evt);
527 // }
528 //
529 // public void objectRenamed(NamingEvent evt) {
530 // if (log.isDebugEnabled())
531 // log.debug(evt);
532 // }
533 //
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();
539 // if (ne != null) {
540 // if (log.isTraceEnabled())
541 // log.trace(msg + ", exception= " + ne, ne);
542 // else
543 // log.warn(msg + ", exception= " + ne);
544 // } else if (log.isDebugEnabled()) {
545 // log.debug("Unsollicited LDAP notification " + msg);
546 // }
547 // }
548 //
549 // }
550
551 /** Listen to JCR */
552 // class JcrProfileListener implements EventListener {
553 //
554 // public void onEvent(EventIterator events) {
555 // try {
556 // final Map<Name, List<ModificationItem>> modifications = new HashMap<Name,
557 // List<ModificationItem>>();
558 // while (events.hasNext()) {
559 // Event event = events.nextEvent();
560 // try {
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,
575 // value);
576 // if (mi != null)
577 // modifications.get(name).add(mi);
578 // }
579 // } else if (Event.NODE_ADDED == event.getType()) {
580 // Node userProfile = nodeSession.getNode(event
581 // .getPath());
582 // String username = userProfile.getProperty(
583 // ARGEO_USER_ID).getString();
584 // Name name = usernameMapper.buildDn(username);
585 // for (String propertyName : propertyToAttributes
586 // .keySet()) {
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,
593 // value);
594 // if (mi != null)
595 // modifications.get(name).add(mi);
596 // }
597 // }
598 // } catch (RepositoryException e) {
599 // throw new ArgeoException("Cannot process event "
600 // + event, e);
601 // }
602 // }
603 //
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);
612 // }
613 // } catch (Exception e) {
614 // // if (log.isDebugEnabled())
615 // // e.printStackTrace();
616 // throw new ArgeoException("Cannot process JCR events ("
617 // + e.getMessage() + ")", e);
618 // }
619 // }
620 //
621 // }
622 }