]> git.argeo.org Git - lgpl/argeo-commons.git/blob - security/runtime/org.argeo.security.ldap/src/main/java/org/argeo/security/ldap/jcr/JcrUserDetailsContextMapper.java
Introduce a latency in sync with LDAP
[lgpl/argeo-commons.git] / security / runtime / org.argeo.security.ldap / src / main / java / org / argeo / 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 if (userHome == null)
132 userHome = JcrUtils.createUserHome(session, homeBasePath,
133 username);
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
144 return userHomePath;
145 }
146 } else {
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);
152 }
153
154 for (String jcrProperty : propertyToAttributes.keySet())
155 ldapToJcr(userProfile, jcrProperty, ctx);
156
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()
163 + " "
164 + userProfile.getProperty(ARGEO_LAST_NAME).getString());
165 JcrUtils.updateLastModified(userProfile);
166 session.save();
167 if (log.isTraceEnabled())
168 log.trace("Mapped " + ctx.getDn() + " to " + userProfile);
169 return userHomePath;
170 } catch (Exception e) {
171 JcrUtils.discardQuietly(session);
172 throw new ArgeoException("Cannot synchronize JCR and LDAP", e);
173 }
174 }
175
176 public void mapUserToContext(UserDetails user, final DirContextAdapter ctx) {
177 if (!(user instanceof JcrUserDetails))
178 throw new ArgeoException("Unsupported user details: "
179 + user.getClass());
180
181 ctx.setAttributeValues("objectClass", userClasses);
182 ctx.setAttributeValue(usernameAttribute, user.getUsername());
183 ctx.setAttributeValue(passwordAttribute,
184 encodePassword(user.getPassword()));
185
186 final JcrUserDetails jcrUserDetails = (JcrUserDetails) user;
187 try {
188 Node userProfile = session.getNode(jcrUserDetails.getHomePath()
189 + '/' + ARGEO_PROFILE);
190 for (String jcrProperty : propertyToAttributes.keySet())
191 jcrToLdap(userProfile, jcrProperty, ctx);
192
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);
197 }
198 }
199
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);
205 } else {
206 return password;
207 }
208 }
209
210 protected void ldapToJcr(Node userProfile, String jcrProperty,
211 DirContextOperations ctx) {
212 try {
213 String ldapAttribute;
214 if (propertyToAttributes.containsKey(jcrProperty))
215 ldapAttribute = propertyToAttributes.get(jcrProperty);
216 else
217 throw new ArgeoException(
218 "No LDAP attribute mapped for JCR proprty "
219 + jcrProperty);
220
221 String value = ctx.getStringAttribute(ldapAttribute);
222 if (value == null)
223 return;
224 userProfile.setProperty(jcrProperty, value);
225 } catch (Exception e) {
226 throw new ArgeoException("Cannot map JCR property " + jcrProperty
227 + " from LDAP", e);
228 }
229 }
230
231 protected void jcrToLdap(Node userProfile, String jcrProperty,
232 DirContextOperations ctx) {
233 try {
234 String ldapAttribute;
235 if (propertyToAttributes.containsKey(jcrProperty))
236 ldapAttribute = propertyToAttributes.get(jcrProperty);
237 else
238 throw new ArgeoException(
239 "No LDAP attribute mapped for JCR proprty "
240 + jcrProperty);
241
242 // fix issue with empty 'sn' in LDAP
243 if (ldapAttribute.equals("sn")
244 && (!userProfile.hasProperty(jcrProperty) || userProfile
245 .getProperty(jcrProperty).getString().trim()
246 .equals("")))
247 userProfile.setProperty(jcrProperty, "empty");
248
249 if (ldapAttribute.equals("description")) {
250 String value = userProfile.getProperty(jcrProperty).getString();
251 if (value.trim().equals(""))
252 return;
253 }
254
255 if (!userProfile.hasProperty(jcrProperty))
256 return;
257 String value = userProfile.getProperty(jcrProperty).getString();
258
259 ctx.setAttributeValue(ldapAttribute, value);
260 } catch (Exception e) {
261 throw new ArgeoException("Cannot map JCR property " + jcrProperty
262 + " from LDAP", e);
263 }
264 }
265
266 public void setPropertyToAttributes(Map<String, String> propertyToAttributes) {
267 this.propertyToAttributes = propertyToAttributes;
268 }
269
270 public void setSystemExecutor(Executor systemExecutor) {
271 this.systemExecutor = systemExecutor;
272 }
273
274 public void setHomeBasePath(String homeBasePath) {
275 this.homeBasePath = homeBasePath;
276 }
277
278 public void setUsernameAttribute(String usernameAttribute) {
279 this.usernameAttribute = usernameAttribute;
280 }
281
282 public void setPasswordAttribute(String passwordAttribute) {
283 this.passwordAttribute = passwordAttribute;
284 }
285
286 public void setUserClasses(String[] userClasses) {
287 this.userClasses = userClasses;
288 }
289
290 public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
291 this.passwordEncoder = passwordEncoder;
292 }
293
294 public void setSession(Session session) {
295 this.session = session;
296 }
297
298 /**
299 * Time in ms during which the LDAP server is not checked. 0 is always sync.
300 */
301 public void setSyncLatency(Long syncLatency) {
302 this.syncLatency = syncLatency;
303 }
304
305 }