1 package org
.argeo
.security
.ldap
.jcr
;
3 import java
.security
.NoSuchAlgorithmException
;
4 import java
.security
.SecureRandom
;
5 import java
.util
.Arrays
;
6 import java
.util
.Calendar
;
7 import java
.util
.HashMap
;
9 import java
.util
.Random
;
10 import java
.util
.SortedSet
;
11 import java
.util
.concurrent
.Executor
;
13 import javax
.jcr
.Node
;
14 import javax
.jcr
.Property
;
15 import javax
.jcr
.RepositoryException
;
16 import javax
.jcr
.Session
;
18 import org
.apache
.commons
.logging
.Log
;
19 import org
.apache
.commons
.logging
.LogFactory
;
20 import org
.argeo
.ArgeoException
;
21 import org
.argeo
.jcr
.ArgeoNames
;
22 import org
.argeo
.jcr
.JcrUtils
;
23 import org
.argeo
.security
.jcr
.JcrUserDetails
;
24 import org
.springframework
.ldap
.core
.DirContextAdapter
;
25 import org
.springframework
.ldap
.core
.DirContextOperations
;
26 import org
.springframework
.security
.BadCredentialsException
;
27 import org
.springframework
.security
.GrantedAuthority
;
28 import org
.springframework
.security
.context
.SecurityContextHolder
;
29 import org
.springframework
.security
.providers
.encoding
.PasswordEncoder
;
30 import org
.springframework
.security
.userdetails
.UserDetails
;
31 import org
.springframework
.security
.userdetails
.ldap
.UserDetailsContextMapper
;
34 * Maps LDAP attributes and JCR properties. This class is meant to be robust,
35 * checks of which values should be mandatory should be performed at a higher
38 public class JcrUserDetailsContextMapper
implements UserDetailsContextMapper
,
40 private final static Log log
= LogFactory
41 .getLog(JcrUserDetailsContextMapper
.class);
43 private String usernameAttribute
;
44 private String passwordAttribute
;
45 private String homeBasePath
;
46 private String
[] userClasses
;
48 private Map
<String
, String
> propertyToAttributes
= new HashMap
<String
, String
>();
49 private Executor systemExecutor
;
50 private Session session
;
52 private PasswordEncoder passwordEncoder
;
53 private final Random random
;
55 /** 0 is always sync */
56 private Long syncLatency
= 10 * 60 * 1000l;
58 public JcrUserDetailsContextMapper() {
59 random
= createRandom();
62 private static Random
createRandom() {
64 return SecureRandom
.getInstance("SHA1PRNG");
65 } catch (NoSuchAlgorithmException e
) {
66 return new Random(System
.currentTimeMillis());
70 public UserDetails
mapUserFromContext(final DirContextOperations ctx
,
71 final String username
, GrantedAuthority
[] authorities
) {
73 throw new ArgeoException("No LDAP information found for user "
76 final StringBuffer userHomePathT
= new StringBuffer("");
77 Runnable action
= new Runnable() {
79 String userHomepath
= mapLdapToJcr(username
, ctx
);
80 userHomePathT
.append(userHomepath
);
84 if (SecurityContextHolder
.getContext().getAuthentication() == null) {
87 systemExecutor
.execute(action
);
89 JcrUtils
.logoutQuietly(session
);
97 SortedSet
<?
> passwordAttributes
= ctx
98 .getAttributeSortedStringSet(passwordAttribute
);
100 if (passwordAttributes
== null || passwordAttributes
.size() == 0) {
101 throw new ArgeoException("No password found for user " + username
);
103 byte[] arr
= (byte[]) passwordAttributes
.first();
104 password
= new String(arr
);
106 Arrays
.fill(arr
, (byte) 0);
108 JcrUserDetails userDetails
= new JcrUserDetails(
109 userHomePathT
.toString(), username
, password
, true, true, true,
114 /** @return path to the user home node */
115 protected synchronized String
mapLdapToJcr(String username
,
116 DirContextOperations ctx
) {
117 String usernameLdap
= ctx
.getStringAttribute(usernameAttribute
);
118 // log.debug("username=" + username + ", usernameLdap=" + usernameLdap);
119 if (!username
.equals(usernameLdap
)) {
120 String msg
= "Provided username '" + username
121 + "' is different from username stored in LDAP '"
122 + usernameLdap
+ "'";
123 // we log it because the exception may not be displayed
125 throw new BadCredentialsException(msg
);
130 Node userHome
= JcrUtils
.getUserHome(session
, username
);
131 if (userHome
== null)
132 userHome
= JcrUtils
.createUserHome(session
, homeBasePath
,
134 String userHomePath
= userHome
.getPath();
135 Node userProfile
; // = userHome.getNode(ARGEO_PROFILE);
136 if (userHome
.hasNode(ARGEO_PROFILE
)) {
137 userProfile
= userHome
.getNode(ARGEO_PROFILE
);
138 if (syncLatency
!= 0) {
139 Calendar lastModified
= userProfile
.getProperty(
140 Property
.JCR_LAST_MODIFIED
).getDate();
141 long timeSinceLastUpdate
= System
.currentTimeMillis()
142 - lastModified
.getTimeInMillis();
143 if (timeSinceLastUpdate
< syncLatency
)// skip sync
147 throw new ArgeoException("We should never reach this point");
148 // userProfile = userHome.addNode(ARGEO_PROFILE);
149 // userProfile.addMixin(NodeType.MIX_TITLE);
150 // userProfile.addMixin(NodeType.MIX_CREATED);
151 // userProfile.addMixin(NodeType.MIX_LAST_MODIFIED);
154 for (String jcrProperty
: propertyToAttributes
.keySet())
155 ldapToJcr(userProfile
, jcrProperty
, ctx
);
157 // assign default values
158 if (!userProfile
.hasProperty(Property
.JCR_DESCRIPTION
))
159 userProfile
.setProperty(Property
.JCR_DESCRIPTION
, "");
160 if (!userProfile
.hasProperty(Property
.JCR_TITLE
))
161 userProfile
.setProperty(Property
.JCR_TITLE
, userProfile
162 .getProperty(ARGEO_FIRST_NAME
).getString()
164 + userProfile
.getProperty(ARGEO_LAST_NAME
).getString());
165 JcrUtils
.updateLastModified(userProfile
);
167 if (log
.isTraceEnabled())
168 log
.trace("Mapped " + ctx
.getDn() + " to " + userProfile
);
170 } catch (Exception e
) {
171 JcrUtils
.discardQuietly(session
);
172 throw new ArgeoException("Cannot synchronize JCR and LDAP", e
);
176 public void mapUserToContext(UserDetails user
, final DirContextAdapter ctx
) {
177 if (!(user
instanceof JcrUserDetails
))
178 throw new ArgeoException("Unsupported user details: "
181 ctx
.setAttributeValues("objectClass", userClasses
);
182 ctx
.setAttributeValue(usernameAttribute
, user
.getUsername());
183 ctx
.setAttributeValue(passwordAttribute
,
184 encodePassword(user
.getPassword()));
186 final JcrUserDetails jcrUserDetails
= (JcrUserDetails
) user
;
188 Node userProfile
= session
.getNode(jcrUserDetails
.getHomePath()
189 + '/' + ARGEO_PROFILE
);
190 for (String jcrProperty
: propertyToAttributes
.keySet())
191 jcrToLdap(userProfile
, jcrProperty
, ctx
);
193 if (log
.isTraceEnabled())
194 log
.trace("Mapped " + userProfile
+ " to " + ctx
.getDn());
195 } catch (RepositoryException e
) {
196 throw new ArgeoException("Cannot synchronize JCR and LDAP", e
);
200 protected String
encodePassword(String password
) {
201 if (!password
.startsWith("{")) {
202 byte[] salt
= new byte[16];
203 random
.nextBytes(salt
);
204 return passwordEncoder
.encodePassword(password
, salt
);
210 protected void ldapToJcr(Node userProfile
, String jcrProperty
,
211 DirContextOperations ctx
) {
213 String ldapAttribute
;
214 if (propertyToAttributes
.containsKey(jcrProperty
))
215 ldapAttribute
= propertyToAttributes
.get(jcrProperty
);
217 throw new ArgeoException(
218 "No LDAP attribute mapped for JCR proprty "
221 String value
= ctx
.getStringAttribute(ldapAttribute
);
224 userProfile
.setProperty(jcrProperty
, value
);
225 } catch (Exception e
) {
226 throw new ArgeoException("Cannot map JCR property " + jcrProperty
231 protected void jcrToLdap(Node userProfile
, String jcrProperty
,
232 DirContextOperations ctx
) {
234 String ldapAttribute
;
235 if (propertyToAttributes
.containsKey(jcrProperty
))
236 ldapAttribute
= propertyToAttributes
.get(jcrProperty
);
238 throw new ArgeoException(
239 "No LDAP attribute mapped for JCR proprty "
242 // fix issue with empty 'sn' in LDAP
243 if (ldapAttribute
.equals("sn")
244 && (!userProfile
.hasProperty(jcrProperty
) || userProfile
245 .getProperty(jcrProperty
).getString().trim()
247 userProfile
.setProperty(jcrProperty
, "empty");
249 if (ldapAttribute
.equals("description")) {
250 String value
= userProfile
.getProperty(jcrProperty
).getString();
251 if (value
.trim().equals(""))
255 if (!userProfile
.hasProperty(jcrProperty
))
257 String value
= userProfile
.getProperty(jcrProperty
).getString();
259 ctx
.setAttributeValue(ldapAttribute
, value
);
260 } catch (Exception e
) {
261 throw new ArgeoException("Cannot map JCR property " + jcrProperty
266 public void setPropertyToAttributes(Map
<String
, String
> propertyToAttributes
) {
267 this.propertyToAttributes
= propertyToAttributes
;
270 public void setSystemExecutor(Executor systemExecutor
) {
271 this.systemExecutor
= systemExecutor
;
274 public void setHomeBasePath(String homeBasePath
) {
275 this.homeBasePath
= homeBasePath
;
278 public void setUsernameAttribute(String usernameAttribute
) {
279 this.usernameAttribute
= usernameAttribute
;
282 public void setPasswordAttribute(String passwordAttribute
) {
283 this.passwordAttribute
= passwordAttribute
;
286 public void setUserClasses(String
[] userClasses
) {
287 this.userClasses
= userClasses
;
290 public void setPasswordEncoder(PasswordEncoder passwordEncoder
) {
291 this.passwordEncoder
= passwordEncoder
;
294 public void setSession(Session session
) {
295 this.session
= session
;
299 * Time in ms during which the LDAP server is not checked. 0 is always sync.
301 public void setSyncLatency(Long syncLatency
) {
302 this.syncLatency
= syncLatency
;