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 boolean justCreatedHome
= false;
132 if (userHome
== null) {
133 userHome
= JcrUtils
.createUserHome(session
, homeBasePath
,
135 justCreatedHome
= true;
137 String userHomePath
= userHome
.getPath();
138 Node userProfile
; // = userHome.getNode(ARGEO_PROFILE);
139 if (userHome
.hasNode(ARGEO_PROFILE
)) {
140 userProfile
= userHome
.getNode(ARGEO_PROFILE
);
141 if (syncLatency
!= 0 && !justCreatedHome
) {
142 Calendar lastModified
= userProfile
.getProperty(
143 Property
.JCR_LAST_MODIFIED
).getDate();
144 long timeSinceLastUpdate
= System
.currentTimeMillis()
145 - lastModified
.getTimeInMillis();
146 if (timeSinceLastUpdate
< syncLatency
)// skip sync
150 throw new ArgeoException("We should never reach this point");
151 // userProfile = userHome.addNode(ARGEO_PROFILE);
152 // userProfile.addMixin(NodeType.MIX_TITLE);
153 // userProfile.addMixin(NodeType.MIX_CREATED);
154 // userProfile.addMixin(NodeType.MIX_LAST_MODIFIED);
157 session
.getWorkspace().getVersionManager()
158 .checkout(userProfile
.getPath());
159 for (String jcrProperty
: propertyToAttributes
.keySet())
160 ldapToJcr(userProfile
, jcrProperty
, ctx
);
162 // assign default values
163 if (!userProfile
.hasProperty(Property
.JCR_DESCRIPTION
))
164 userProfile
.setProperty(Property
.JCR_DESCRIPTION
, "");
165 if (!userProfile
.hasProperty(Property
.JCR_TITLE
))
166 userProfile
.setProperty(Property
.JCR_TITLE
, userProfile
167 .getProperty(ARGEO_FIRST_NAME
).getString()
169 + userProfile
.getProperty(ARGEO_LAST_NAME
).getString());
170 JcrUtils
.updateLastModified(userProfile
);
172 session
.getWorkspace().getVersionManager()
173 .checkin(userProfile
.getPath());
174 if (log
.isTraceEnabled())
175 log
.trace("Mapped " + ctx
.getDn() + " to " + userProfile
);
177 } catch (Exception e
) {
178 JcrUtils
.discardQuietly(session
);
179 throw new ArgeoException("Cannot synchronize JCR and LDAP", e
);
183 public void mapUserToContext(UserDetails user
, final DirContextAdapter ctx
) {
184 if (!(user
instanceof JcrUserDetails
))
185 throw new ArgeoException("Unsupported user details: "
188 ctx
.setAttributeValues("objectClass", userClasses
);
189 ctx
.setAttributeValue(usernameAttribute
, user
.getUsername());
190 ctx
.setAttributeValue(passwordAttribute
,
191 encodePassword(user
.getPassword()));
193 final JcrUserDetails jcrUserDetails
= (JcrUserDetails
) user
;
195 Node userProfile
= session
.getNode(jcrUserDetails
.getHomePath()
196 + '/' + ARGEO_PROFILE
);
197 for (String jcrProperty
: propertyToAttributes
.keySet())
198 jcrToLdap(userProfile
, jcrProperty
, ctx
);
200 if (log
.isTraceEnabled())
201 log
.trace("Mapped " + userProfile
+ " to " + ctx
.getDn());
202 } catch (RepositoryException e
) {
203 throw new ArgeoException("Cannot synchronize JCR and LDAP", e
);
207 protected String
encodePassword(String password
) {
208 if (!password
.startsWith("{")) {
209 byte[] salt
= new byte[16];
210 random
.nextBytes(salt
);
211 return passwordEncoder
.encodePassword(password
, salt
);
217 protected void ldapToJcr(Node userProfile
, String jcrProperty
,
218 DirContextOperations ctx
) {
220 String ldapAttribute
;
221 if (propertyToAttributes
.containsKey(jcrProperty
))
222 ldapAttribute
= propertyToAttributes
.get(jcrProperty
);
224 throw new ArgeoException(
225 "No LDAP attribute mapped for JCR proprty "
228 String value
= ctx
.getStringAttribute(ldapAttribute
);
229 String jcrValue
= userProfile
.hasProperty(jcrProperty
) ? userProfile
230 .getProperty(jcrProperty
).getString() : null;
231 if (value
!= null && jcrValue
!= null) {
232 if (!value
.equals(jcrValue
))
233 userProfile
.setProperty(jcrProperty
, value
);
234 } else if (value
!= null && jcrValue
== null) {
235 userProfile
.setProperty(jcrProperty
, value
);
236 } else if (value
== null && jcrValue
!= null) {
237 userProfile
.setProperty(jcrProperty
, value
);
239 } catch (Exception e
) {
240 throw new ArgeoException("Cannot map JCR property " + jcrProperty
245 protected void jcrToLdap(Node userProfile
, String jcrProperty
,
246 DirContextOperations ctx
) {
248 String ldapAttribute
;
249 if (propertyToAttributes
.containsKey(jcrProperty
))
250 ldapAttribute
= propertyToAttributes
.get(jcrProperty
);
252 throw new ArgeoException(
253 "No LDAP attribute mapped for JCR proprty "
256 // fix issue with empty 'sn' in LDAP
257 if (ldapAttribute
.equals("sn")
258 && (!userProfile
.hasProperty(jcrProperty
) || userProfile
259 .getProperty(jcrProperty
).getString().trim()
261 userProfile
.setProperty(jcrProperty
, "empty");
263 if (ldapAttribute
.equals("description")) {
264 String value
= userProfile
.getProperty(jcrProperty
).getString();
265 if (value
.trim().equals(""))
269 if (!userProfile
.hasProperty(jcrProperty
))
271 String value
= userProfile
.getProperty(jcrProperty
).getString();
273 ctx
.setAttributeValue(ldapAttribute
, value
);
274 } catch (Exception e
) {
275 throw new ArgeoException("Cannot map JCR property " + jcrProperty
280 public void setPropertyToAttributes(Map
<String
, String
> propertyToAttributes
) {
281 this.propertyToAttributes
= propertyToAttributes
;
284 public void setSystemExecutor(Executor systemExecutor
) {
285 this.systemExecutor
= systemExecutor
;
288 public void setHomeBasePath(String homeBasePath
) {
289 this.homeBasePath
= homeBasePath
;
292 public void setUsernameAttribute(String usernameAttribute
) {
293 this.usernameAttribute
= usernameAttribute
;
296 public void setPasswordAttribute(String passwordAttribute
) {
297 this.passwordAttribute
= passwordAttribute
;
300 public void setUserClasses(String
[] userClasses
) {
301 this.userClasses
= userClasses
;
304 public void setPasswordEncoder(PasswordEncoder passwordEncoder
) {
305 this.passwordEncoder
= passwordEncoder
;
308 public void setSession(Session session
) {
309 this.session
= session
;
313 * Time in ms during which the LDAP server is not checked. 0 is always sync.
315 public void setSyncLatency(Long syncLatency
) {
316 this.syncLatency
= syncLatency
;