]> git.argeo.org Git - lgpl/argeo-commons.git/blob - security/ldap/jcr/JcrUserDetailsContextMapper.java
Prepare next development cycle
[lgpl/argeo-commons.git] / security / ldap / jcr / JcrUserDetailsContextMapper.java
1 package org.argeo.security.ldap.jcr;
2
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;
8 import java.util.Map;
9 import java.util.Random;
10 import java.util.SortedSet;
11 import java.util.concurrent.Executor;
12
13 import javax.jcr.Node;
14 import javax.jcr.Property;
15 import javax.jcr.RepositoryException;
16 import javax.jcr.Session;
17
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;
32
33 /**
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
36 * level.
37 */
38 public class JcrUserDetailsContextMapper implements UserDetailsContextMapper,
39 ArgeoNames {
40 private final static Log log = LogFactory
41 .getLog(JcrUserDetailsContextMapper.class);
42
43 private String usernameAttribute;
44 private String passwordAttribute;
45 private String homeBasePath;
46 private String[] userClasses;
47
48 private Map<String, String> propertyToAttributes = new HashMap<String, String>();
49 private Executor systemExecutor;
50 private Session session;
51
52 private PasswordEncoder passwordEncoder;
53 private final Random random;
54
55 /** 0 is always sync */
56 private Long syncLatency = 10 * 60 * 1000l;
57
58 public JcrUserDetailsContextMapper() {
59 random = createRandom();
60 }
61
62 private static Random createRandom() {
63 try {
64 return SecureRandom.getInstance("SHA1PRNG");
65 } catch (NoSuchAlgorithmException e) {
66 return new Random(System.currentTimeMillis());
67 }
68 }
69
70 public UserDetails mapUserFromContext(final DirContextOperations ctx,
71 final String username, GrantedAuthority[] authorities) {
72 if (ctx == null)
73 throw new ArgeoException("No LDAP information found for user "
74 + username);
75
76 final StringBuffer userHomePathT = new StringBuffer("");
77 Runnable action = new Runnable() {
78 public void run() {
79 String userHomepath = mapLdapToJcr(username, ctx);
80 userHomePathT.append(userHomepath);
81 }
82 };
83
84 if (SecurityContextHolder.getContext().getAuthentication() == null) {
85 // authentication
86 try {
87 systemExecutor.execute(action);
88 } finally {
89 JcrUtils.logoutQuietly(session);
90 }
91 } else {
92 // authenticated user
93 action.run();
94 }
95
96 // password
97 SortedSet<?> passwordAttributes = ctx
98 .getAttributeSortedStringSet(passwordAttribute);
99 String password;
100 if (passwordAttributes == null || passwordAttributes.size() == 0) {
101 throw new ArgeoException("No password found for user " + username);
102 } else {
103 byte[] arr = (byte[]) passwordAttributes.first();
104 password = new String(arr);
105 // erase password
106 Arrays.fill(arr, (byte) 0);
107 }
108 JcrUserDetails userDetails = new JcrUserDetails(
109 userHomePathT.toString(), username, password, true, true, true,
110 true, authorities);
111 return userDetails;
112 }
113
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
124 log.error(msg);
125 throw new BadCredentialsException(msg);
126 }
127
128 try {
129
130 Node userHome = JcrUtils.getUserHome(session, username);
131 boolean justCreatedHome = false;
132 if (userHome == null) {
133 userHome = JcrUtils.createUserHome(session, homeBasePath,
134 username);
135 justCreatedHome = true;
136 }
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
147 return userHomePath;
148 }
149 } else {
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);
155 }
156
157 session.getWorkspace().getVersionManager()
158 .checkout(userProfile.getPath());
159 for (String jcrProperty : propertyToAttributes.keySet())
160 ldapToJcr(userProfile, jcrProperty, ctx);
161
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()
168 + " "
169 + userProfile.getProperty(ARGEO_LAST_NAME).getString());
170 JcrUtils.updateLastModified(userProfile);
171 session.save();
172 session.getWorkspace().getVersionManager()
173 .checkin(userProfile.getPath());
174 if (log.isTraceEnabled())
175 log.trace("Mapped " + ctx.getDn() + " to " + userProfile);
176 return userHomePath;
177 } catch (Exception e) {
178 JcrUtils.discardQuietly(session);
179 throw new ArgeoException("Cannot synchronize JCR and LDAP", e);
180 }
181 }
182
183 public void mapUserToContext(UserDetails user, final DirContextAdapter ctx) {
184 if (!(user instanceof JcrUserDetails))
185 throw new ArgeoException("Unsupported user details: "
186 + user.getClass());
187
188 ctx.setAttributeValues("objectClass", userClasses);
189 ctx.setAttributeValue(usernameAttribute, user.getUsername());
190 ctx.setAttributeValue(passwordAttribute,
191 encodePassword(user.getPassword()));
192
193 final JcrUserDetails jcrUserDetails = (JcrUserDetails) user;
194 try {
195 Node userProfile = session.getNode(jcrUserDetails.getHomePath()
196 + '/' + ARGEO_PROFILE);
197 for (String jcrProperty : propertyToAttributes.keySet())
198 jcrToLdap(userProfile, jcrProperty, ctx);
199
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);
204 }
205 }
206
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);
212 } else {
213 return password;
214 }
215 }
216
217 protected void ldapToJcr(Node userProfile, String jcrProperty,
218 DirContextOperations ctx) {
219 try {
220 String ldapAttribute;
221 if (propertyToAttributes.containsKey(jcrProperty))
222 ldapAttribute = propertyToAttributes.get(jcrProperty);
223 else
224 throw new ArgeoException(
225 "No LDAP attribute mapped for JCR proprty "
226 + jcrProperty);
227
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);
238 }
239 } catch (Exception e) {
240 throw new ArgeoException("Cannot map JCR property " + jcrProperty
241 + " from LDAP", e);
242 }
243 }
244
245 protected void jcrToLdap(Node userProfile, String jcrProperty,
246 DirContextOperations ctx) {
247 try {
248 String ldapAttribute;
249 if (propertyToAttributes.containsKey(jcrProperty))
250 ldapAttribute = propertyToAttributes.get(jcrProperty);
251 else
252 throw new ArgeoException(
253 "No LDAP attribute mapped for JCR proprty "
254 + jcrProperty);
255
256 // fix issue with empty 'sn' in LDAP
257 if (ldapAttribute.equals("sn")
258 && (!userProfile.hasProperty(jcrProperty) || userProfile
259 .getProperty(jcrProperty).getString().trim()
260 .equals("")))
261 userProfile.setProperty(jcrProperty, "empty");
262
263 if (ldapAttribute.equals("description")) {
264 String value = userProfile.getProperty(jcrProperty).getString();
265 if (value.trim().equals(""))
266 return;
267 }
268
269 if (!userProfile.hasProperty(jcrProperty))
270 return;
271 String value = userProfile.getProperty(jcrProperty).getString();
272
273 ctx.setAttributeValue(ldapAttribute, value);
274 } catch (Exception e) {
275 throw new ArgeoException("Cannot map JCR property " + jcrProperty
276 + " from LDAP", e);
277 }
278 }
279
280 public void setPropertyToAttributes(Map<String, String> propertyToAttributes) {
281 this.propertyToAttributes = propertyToAttributes;
282 }
283
284 public void setSystemExecutor(Executor systemExecutor) {
285 this.systemExecutor = systemExecutor;
286 }
287
288 public void setHomeBasePath(String homeBasePath) {
289 this.homeBasePath = homeBasePath;
290 }
291
292 public void setUsernameAttribute(String usernameAttribute) {
293 this.usernameAttribute = usernameAttribute;
294 }
295
296 public void setPasswordAttribute(String passwordAttribute) {
297 this.passwordAttribute = passwordAttribute;
298 }
299
300 public void setUserClasses(String[] userClasses) {
301 this.userClasses = userClasses;
302 }
303
304 public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
305 this.passwordEncoder = passwordEncoder;
306 }
307
308 public void setSession(Session session) {
309 this.session = session;
310 }
311
312 /**
313 * Time in ms during which the LDAP server is not checked. 0 is always sync.
314 */
315 public void setSyncLatency(Long syncLatency) {
316 this.syncLatency = syncLatency;
317 }
318
319 }