]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/internal/useradmin/ldap/JcrLdapSynchronizer.java
Enhance comments, remove typos
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / internal / useradmin / ldap / 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.cms.internal.useradmin.ldap;
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.cms.internal.auth.JcrSecurityModel;
45 import org.argeo.cms.internal.useradmin.SimpleJcrSecurityModel;
46 import org.argeo.jcr.ArgeoNames;
47 import org.argeo.jcr.ArgeoTypes;
48 import org.argeo.jcr.JcrUtils;
49 import org.argeo.security.SecurityUtils;
50 import org.argeo.security.jcr.JcrUserDetails;
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 @SuppressWarnings("deprecation")
64 public class JcrLdapSynchronizer implements UserDetailsContextMapper,
65 ArgeoNames {
66 private final static Log log = LogFactory.getLog(JcrLdapSynchronizer.class);
67
68 // LDAP
69 private LdapTemplate ldapTemplate;
70 /**
71 * LDAP template whose context source has an object factory set to null. see
72 * <a href=
73 * "http://forum.springsource.org/showthread.php?55955-Persistent-search-with-spring-ldap"
74 * >this</a>
75 */
76 // private LdapTemplate rawLdapTemplate;
77
78 private String userBase;
79 private String usernameAttribute;
80 private String passwordAttribute;
81 private String[] userClasses;
82 // private String defaultUserRole ="ROLE_USER";
83
84 // private NamingListener ldapUserListener;
85 // private SearchControls subTreeSearchControls;
86 private LdapUsernameToDnMapper usernameMapper;
87
88 private PasswordEncoder passwordEncoder;
89 private final Random random;
90
91 // JCR
92 /** Admin session on the main workspace */
93 private Session nodeSession;
94 private Repository repository;
95
96 // private JcrProfileListener jcrProfileListener;
97 private JcrSecurityModel jcrSecurityModel = new SimpleJcrSecurityModel();
98
99 // Mapping
100 private Map<String, String> propertyToAttributes = new HashMap<String, String>();
101
102 public JcrLdapSynchronizer() {
103 random = createRandom();
104 }
105
106 public void init() {
107 try {
108 nodeSession = repository.login();
109
110 // TODO put this in a different thread, and poll the LDAP server
111 // until it is up
112 try {
113 synchronize();
114
115 // LDAP
116 // subTreeSearchControls = new SearchControls();
117 // subTreeSearchControls
118 // .setSearchScope(SearchControls.SUBTREE_SCOPE);
119 // LDAP listener
120 // ldapUserListener = new LdapUserListener();
121 // rawLdapTemplate.executeReadOnly(new ContextExecutor() {
122 // public Object executeWithContext(DirContext ctx)
123 // throws NamingException {
124 // EventDirContext ectx = (EventDirContext) ctx.lookup("");
125 // ectx.addNamingListener(userBase, "("
126 // + usernameAttribute + "=*)",
127 // subTreeSearchControls, ldapUserListener);
128 // return null;
129 // }
130 // });
131 } catch (Exception e) {
132 log.error("Could not synchronize and listen to LDAP,"
133 + " probably because the LDAP server is not available."
134 + " Restart the system as soon as possible.", e);
135 }
136
137 // JCR
138 // String[] nodeTypes = { ArgeoTypes.ARGEO_USER_PROFILE };
139 // jcrProfileListener = new JcrProfileListener();
140 // noLocal is used so that we are not notified when we modify JCR
141 // from LDAP
142 // nodeSession
143 // .getWorkspace()
144 // .getObservationManager()
145 // .addEventListener(jcrProfileListener,
146 // Event.PROPERTY_CHANGED | Event.NODE_ADDED, "/",
147 // true, null, nodeTypes, true);
148 } catch (Exception e) {
149 JcrUtils.logoutQuietly(nodeSession);
150 throw new ArgeoException("Cannot initialize LDAP/JCR synchronizer",
151 e);
152 }
153 }
154
155 public void destroy() {
156 // JcrUtils.removeListenerQuietly(nodeSession, jcrProfileListener);
157 JcrUtils.logoutQuietly(nodeSession);
158 // try {
159 // rawLdapTemplate.executeReadOnly(new ContextExecutor() {
160 // public Object executeWithContext(DirContext ctx)
161 // throws NamingException {
162 // EventDirContext ectx = (EventDirContext) ctx.lookup("");
163 // ectx.removeNamingListener(ldapUserListener);
164 // return null;
165 // }
166 // });
167 // } catch (Exception e) {
168 // // silent (LDAP server may have been shutdown already)
169 // if (log.isTraceEnabled())
170 // log.trace("Cannot remove LDAP listener", e);
171 // }
172 }
173
174 /*
175 * LDAP TO JCR
176 */
177 /** Full synchronization between LDAP and JCR. LDAP has priority. */
178 protected void synchronize() {
179 try {
180 Name userBaseName = new DistinguishedName(userBase);
181 // TODO subtree search?
182 @SuppressWarnings("unchecked")
183 List<String> userPaths = (List<String>) ldapTemplate.listBindings(
184 userBaseName, new ContextMapper() {
185 public Object mapFromContext(Object ctxObj) {
186 try {
187 return mapLdapToJcr((DirContextAdapter) ctxObj);
188 } catch (Exception e) {
189 // do not break process because of error
190 log.error(
191 "Could not LDAP->JCR synchronize user "
192 + ctxObj, e);
193 return null;
194 }
195 }
196 });
197
198 // create accounts which are not in LDAP
199 Query query = nodeSession
200 .getWorkspace()
201 .getQueryManager()
202 .createQuery(
203 "select * from [" + ArgeoTypes.ARGEO_USER_PROFILE
204 + "]", Query.JCR_SQL2);
205 NodeIterator it = query.execute().getNodes();
206 while (it.hasNext()) {
207 Node userProfile = it.nextNode();
208 String path = userProfile.getPath();
209 try {
210 if (!userPaths.contains(path)) {
211 String username = userProfile
212 .getProperty(ARGEO_USER_ID).getString();
213 // GrantedAuthority[] authorities = {new
214 // GrantedAuthorityImpl(defaultUserRole)};
215 List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
216 JcrUserDetails userDetails = new JcrUserDetails(
217 userProfile, username, authorities);
218 String dn = createLdapUser(userDetails);
219 log.warn("Created ldap entry '" + dn + "' for user '"
220 + username + "'");
221
222 // if(!userProfile.getProperty(ARGEO_ENABLED).getBoolean()){
223 // continue profiles;
224 // }
225 //
226 // log.warn("Path "
227 // + path
228 // + " not found in LDAP, disabling user "
229 // + userProfile.getProperty(ArgeoNames.ARGEO_USER_ID)
230 // .getString());
231
232 // Temporary hack to repair previous behaviour
233 if (!userProfile.getProperty(ARGEO_ENABLED)
234 .getBoolean()) {
235 VersionManager versionManager = nodeSession
236 .getWorkspace().getVersionManager();
237 versionManager.checkout(userProfile.getPath());
238 userProfile.setProperty(ArgeoNames.ARGEO_ENABLED,
239 true);
240 nodeSession.save();
241 versionManager.checkin(userProfile.getPath());
242 }
243 }
244 } catch (Exception e) {
245 log.error("Cannot process " + path, e);
246 }
247 }
248 } catch (Exception e) {
249 JcrUtils.discardQuietly(nodeSession);
250 log.error("Cannot synchronize LDAP and JCR", e);
251 // throw new ArgeoException("Cannot synchronize LDAP and JCR", e);
252 }
253 }
254
255 private String createLdapUser(UserDetails user) {
256 DirContextAdapter ctx = new DirContextAdapter();
257 mapUserToContext(user, ctx);
258 DistinguishedName dn = usernameMapper.buildDn(user.getUsername());
259 ldapTemplate.bind(dn, ctx, null);
260 return dn.toString();
261 }
262
263 /** Called during authentication in order to retrieve user details */
264 public UserDetails mapUserFromContext(final DirContextOperations ctx,
265 final String username,
266 Collection<? extends GrantedAuthority> authorities) {
267 if (ctx == null)
268 throw new ArgeoException("No LDAP information for user " + username);
269
270 String ldapUsername = ctx.getStringAttribute(usernameAttribute);
271 if (!ldapUsername.equals(username))
272 throw new ArgeoException("Logged in with username " + username
273 + " but LDAP user is " + ldapUsername);
274
275 Node userProfile = jcrSecurityModel.sync(nodeSession, username,
276 SecurityUtils.authoritiesToStringList(authorities));
277 // JcrUserDetails.checkAccountStatus(userProfile);
278
279 // password
280 SortedSet<?> passwordAttributes = ctx
281 .getAttributeSortedStringSet(passwordAttribute);
282 String password;
283 if (passwordAttributes == null || passwordAttributes.size() == 0) {
284 // throw new ArgeoException("No password found for user " +
285 // username);
286 password = "NULL";
287 } else {
288 byte[] arr = (byte[]) passwordAttributes.first();
289 password = new String(arr);
290 // erase password
291 Arrays.fill(arr, (byte) 0);
292 }
293
294 try {
295 return new JcrUserDetails(userProfile, password, authorities);
296 } catch (RepositoryException e) {
297 throw new ArgeoException("Cannot retrieve user details for "
298 + username, e);
299 }
300 }
301
302 /**
303 * Writes an LDAP context to the JCR user profile.
304 *
305 * @return path to user profile
306 */
307 protected synchronized String mapLdapToJcr(DirContextAdapter ctx) {
308 Session session = nodeSession;
309 try {
310 // process
311 String username = ctx.getStringAttribute(usernameAttribute);
312
313 Node userProfile = jcrSecurityModel.sync(session, username, null);
314 Map<String, String> modifications = new HashMap<String, String>();
315 for (String jcrProperty : propertyToAttributes.keySet())
316 ldapToJcr(userProfile, jcrProperty, ctx, modifications);
317
318 int modifCount = modifications.size();
319 if (modifCount > 0) {
320 session.getWorkspace().getVersionManager()
321 .checkout(userProfile.getPath());
322 for (String prop : modifications.keySet())
323 userProfile.setProperty(prop, modifications.get(prop));
324 JcrUtils.updateLastModified(userProfile);
325 session.save();
326 session.getWorkspace().getVersionManager()
327 .checkin(userProfile.getPath());
328 if (log.isDebugEnabled())
329 log.debug("Mapped " + modifCount + " LDAP modification"
330 + (modifCount == 1 ? "" : "s") + " from "
331 + ctx.getDn() + " to " + userProfile);
332 }
333 return userProfile.getPath();
334 } catch (Exception e) {
335 JcrUtils.discardQuietly(session);
336 throw new ArgeoException("Cannot synchronize JCR and LDAP", e);
337 }
338 }
339
340 /** Maps an LDAP property to a JCR property */
341 protected void ldapToJcr(Node userProfile, String jcrProperty,
342 DirContextOperations ctx, Map<String, String> modifications) {
343 // TODO do we really need DirContextOperations?
344 try {
345 String ldapAttribute;
346 if (propertyToAttributes.containsKey(jcrProperty))
347 ldapAttribute = propertyToAttributes.get(jcrProperty);
348 else
349 throw new ArgeoException(
350 "No LDAP attribute mapped for JCR proprty "
351 + jcrProperty);
352
353 String value = ctx.getStringAttribute(ldapAttribute);
354 String jcrValue = userProfile.hasProperty(jcrProperty) ? userProfile
355 .getProperty(jcrProperty).getString() : null;
356 if (value != null && jcrValue != null) {
357 if (!value.equals(jcrValue))
358 modifications.put(jcrProperty, value);
359 } else if (value != null && jcrValue == null) {
360 modifications.put(jcrProperty, value);
361 } else if (value == null && jcrValue != null) {
362 modifications.put(jcrProperty, value);
363 }
364 } catch (Exception e) {
365 throw new ArgeoException("Cannot map JCR property " + jcrProperty
366 + " from LDAP", e);
367 }
368 }
369
370 /*
371 * JCR to LDAP
372 */
373
374 public void mapUserToContext(UserDetails user, final DirContextAdapter ctx) {
375 if (!(user instanceof JcrUserDetails))
376 throw new ArgeoException("Unsupported user details: "
377 + user.getClass());
378
379 ctx.setAttributeValues("objectClass", userClasses);
380 ctx.setAttributeValue(usernameAttribute, user.getUsername());
381 ctx.setAttributeValue(passwordAttribute,
382 encodePassword(user.getPassword()));
383
384 final JcrUserDetails jcrUserDetails = (JcrUserDetails) user;
385 try {
386 Node userProfile = nodeSession
387 .getNode(jcrUserDetails.getHomePath()).getNode(
388 ARGEO_PROFILE);
389 for (String jcrProperty : propertyToAttributes.keySet()) {
390 if (userProfile.hasProperty(jcrProperty)) {
391 ModificationItem mi = jcrToLdap(jcrProperty, userProfile
392 .getProperty(jcrProperty).getString());
393 if (mi != null)
394 ctx.setAttribute(mi.getAttribute());
395 }
396 }
397 if (log.isTraceEnabled())
398 log.trace("Mapped " + userProfile + " to " + ctx.getDn());
399 } catch (RepositoryException e) {
400 throw new ArgeoException("Cannot synchronize JCR and LDAP", e);
401 }
402
403 }
404
405 /** Maps a JCR property to an LDAP property */
406 protected ModificationItem jcrToLdap(String jcrProperty, String value) {
407 // TODO do we really need DirContextOperations?
408 try {
409 String ldapAttribute;
410 if (propertyToAttributes.containsKey(jcrProperty))
411 ldapAttribute = propertyToAttributes.get(jcrProperty);
412 else
413 return null;
414
415 // fix issue with empty 'sn' in LDAP
416 if (ldapAttribute.equals("sn") && (value.trim().equals("")))
417 return null;
418 // fix issue with empty 'description' in LDAP
419 if (ldapAttribute.equals("description") && value.trim().equals(""))
420 return null;
421 BasicAttribute attr = new BasicAttribute(
422 propertyToAttributes.get(jcrProperty), value);
423 ModificationItem mi = new ModificationItem(
424 DirContext.REPLACE_ATTRIBUTE, attr);
425 return mi;
426 } catch (Exception e) {
427 throw new ArgeoException("Cannot map JCR property " + jcrProperty
428 + " from LDAP", e);
429 }
430 }
431
432 /*
433 * UTILITIES
434 */
435 protected String encodePassword(String password) {
436 if (!password.startsWith("{")) {
437 byte[] salt = new byte[16];
438 random.nextBytes(salt);
439 return passwordEncoder.encodePassword(password, salt);
440 } else {
441 return password;
442 }
443 }
444
445 private static Random createRandom() {
446 try {
447 return SecureRandom.getInstance("SHA1PRNG");
448 } catch (NoSuchAlgorithmException e) {
449 return new Random(System.currentTimeMillis());
450 }
451 }
452
453 /*
454 * DEPENDENCY INJECTION
455 */
456
457 public void setLdapTemplate(LdapTemplate ldapTemplate) {
458 this.ldapTemplate = ldapTemplate;
459 }
460
461 public void setRawLdapTemplate(LdapTemplate rawLdapTemplate) {
462 // this.rawLdapTemplate = rawLdapTemplate;
463 }
464
465 public void setRepository(Repository repository) {
466 this.repository = repository;
467 }
468
469 public void setUserBase(String userBase) {
470 this.userBase = userBase;
471 }
472
473 public void setUsernameAttribute(String usernameAttribute) {
474 this.usernameAttribute = usernameAttribute;
475 }
476
477 public void setPropertyToAttributes(Map<String, String> propertyToAttributes) {
478 this.propertyToAttributes = propertyToAttributes;
479 }
480
481 public void setUsernameMapper(LdapUsernameToDnMapper usernameMapper) {
482 this.usernameMapper = usernameMapper;
483 }
484
485 public void setPasswordAttribute(String passwordAttribute) {
486 this.passwordAttribute = passwordAttribute;
487 }
488
489 public void setUserClasses(String[] userClasses) {
490 this.userClasses = userClasses;
491 }
492
493 public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
494 this.passwordEncoder = passwordEncoder;
495 }
496
497 public void setJcrSecurityModel(JcrSecurityModel jcrSecurityModel) {
498 this.jcrSecurityModel = jcrSecurityModel;
499 }
500
501 /** Listen to LDAP */
502 // class LdapUserListener implements ObjectChangeListener,
503 // NamespaceChangeListener, UnsolicitedNotificationListener {
504 //
505 // public void namingExceptionThrown(NamingExceptionEvent evt) {
506 // evt.getException().printStackTrace();
507 // }
508 //
509 // public void objectChanged(NamingEvent evt) {
510 // Binding user = evt.getNewBinding();
511 // // TODO find a way not to be called when JCR is the source of the
512 // // modification
513 // DirContextAdapter ctx = (DirContextAdapter) ldapTemplate
514 // .lookup(user.getName());
515 // mapLdapToJcr(ctx);
516 // }
517 //
518 // public void objectAdded(NamingEvent evt) {
519 // Binding user = evt.getNewBinding();
520 // DirContextAdapter ctx = (DirContextAdapter) ldapTemplate
521 // .lookup(user.getName());
522 // mapLdapToJcr(ctx);
523 // }
524 //
525 // public void objectRemoved(NamingEvent evt) {
526 // if (log.isDebugEnabled())
527 // log.debug(evt);
528 // }
529 //
530 // public void objectRenamed(NamingEvent evt) {
531 // if (log.isDebugEnabled())
532 // log.debug(evt);
533 // }
534 //
535 // public void notificationReceived(UnsolicitedNotificationEvent evt) {
536 // UnsolicitedNotification notification = evt.getNotification();
537 // NamingException ne = notification.getException();
538 // String msg = "LDAP notification " + "ID=" + notification.getID()
539 // + ", referrals=" + notification.getReferrals();
540 // if (ne != null) {
541 // if (log.isTraceEnabled())
542 // log.trace(msg + ", exception= " + ne, ne);
543 // else
544 // log.warn(msg + ", exception= " + ne);
545 // } else if (log.isDebugEnabled()) {
546 // log.debug("Unsollicited LDAP notification " + msg);
547 // }
548 // }
549 //
550 // }
551
552 /** Listen to JCR */
553 // class JcrProfileListener implements EventListener {
554 //
555 // public void onEvent(EventIterator events) {
556 // try {
557 // final Map<Name, List<ModificationItem>> modifications = new HashMap<Name,
558 // List<ModificationItem>>();
559 // while (events.hasNext()) {
560 // Event event = events.nextEvent();
561 // try {
562 // if (Event.PROPERTY_CHANGED == event.getType()) {
563 // Property property = (Property) nodeSession
564 // .getItem(event.getPath());
565 // String propertyName = property.getName();
566 // Node userProfile = property.getParent();
567 // String username = userProfile.getProperty(
568 // ARGEO_USER_ID).getString();
569 // if (propertyToAttributes.containsKey(propertyName)) {
570 // Name name = usernameMapper.buildDn(username);
571 // if (!modifications.containsKey(name))
572 // modifications.put(name,
573 // new ArrayList<ModificationItem>());
574 // String value = property.getString();
575 // ModificationItem mi = jcrToLdap(propertyName,
576 // value);
577 // if (mi != null)
578 // modifications.get(name).add(mi);
579 // }
580 // } else if (Event.NODE_ADDED == event.getType()) {
581 // Node userProfile = nodeSession.getNode(event
582 // .getPath());
583 // String username = userProfile.getProperty(
584 // ARGEO_USER_ID).getString();
585 // Name name = usernameMapper.buildDn(username);
586 // for (String propertyName : propertyToAttributes
587 // .keySet()) {
588 // if (!modifications.containsKey(name))
589 // modifications.put(name,
590 // new ArrayList<ModificationItem>());
591 // String value = userProfile.getProperty(
592 // propertyName).getString();
593 // ModificationItem mi = jcrToLdap(propertyName,
594 // value);
595 // if (mi != null)
596 // modifications.get(name).add(mi);
597 // }
598 // }
599 // } catch (RepositoryException e) {
600 // throw new ArgeoException("Cannot process event "
601 // + event, e);
602 // }
603 // }
604 //
605 // for (Name name : modifications.keySet()) {
606 // List<ModificationItem> userModifs = modifications.get(name);
607 // int modifCount = userModifs.size();
608 // ldapTemplate.modifyAttributes(name, userModifs
609 // .toArray(new ModificationItem[modifCount]));
610 // if (log.isDebugEnabled())
611 // log.debug("Mapped " + modifCount + " JCR modification"
612 // + (modifCount == 1 ? "" : "s") + " to " + name);
613 // }
614 // } catch (Exception e) {
615 // // if (log.isDebugEnabled())
616 // // e.printStackTrace();
617 // throw new ArgeoException("Cannot process JCR events ("
618 // + e.getMessage() + ")", e);
619 // }
620 // }
621 //
622 // }
623 }