View Javadoc
1   package org.argeo.connect.core;
2   
3   import static org.argeo.naming.LdapAttrs.cn;
4   import static org.argeo.naming.LdapAttrs.description;
5   import static org.argeo.naming.LdapAttrs.owner;
6   
7   import java.time.ZoneOffset;
8   import java.time.ZonedDateTime;
9   import java.util.ArrayList;
10  import java.util.Arrays;
11  import java.util.Dictionary;
12  import java.util.HashMap;
13  import java.util.HashSet;
14  import java.util.List;
15  import java.util.Map;
16  import java.util.Set;
17  import java.util.UUID;
18  
19  import javax.jcr.Node;
20  import javax.naming.InvalidNameException;
21  import javax.naming.ldap.LdapName;
22  import javax.security.auth.Subject;
23  import javax.transaction.Status;
24  import javax.transaction.UserTransaction;
25  
26  import org.apache.commons.logging.Log;
27  import org.apache.commons.logging.LogFactory;
28  import org.argeo.cms.auth.CurrentUser;
29  import org.argeo.cms.util.UserAdminUtils;
30  import org.argeo.connect.ConnectException;
31  import org.argeo.connect.UserAdminService;
32  import org.argeo.jcr.JcrUtils;
33  import org.argeo.naming.LdapAttrs;
34  import org.argeo.naming.NamingUtils;
35  import org.argeo.naming.SharedSecret;
36  import org.argeo.node.NodeConstants;
37  import org.argeo.osgi.useradmin.TokenUtils;
38  import org.argeo.osgi.useradmin.UserAdminConf;
39  import org.argeo.people.PeopleNames;
40  import org.osgi.framework.InvalidSyntaxException;
41  import org.osgi.framework.ServiceReference;
42  import org.osgi.service.useradmin.Authorization;
43  import org.osgi.service.useradmin.Group;
44  import org.osgi.service.useradmin.Role;
45  import org.osgi.service.useradmin.User;
46  import org.osgi.service.useradmin.UserAdmin;
47  
48  /**
49   * Canonical implementation of the people {@link UserAdminService}. Wraps
50   * interaction with users and groups.
51   * 
52   * In a *READ-ONLY* mode. We want to be able to:
53   * <ul>
54   * <li>Retrieve my user and corresponding information (main info,
55   * groups...)</li>
56   * <li>List all local groups (not the system roles)</li>
57   * <li>If sufficient rights: retrieve a given user and its information</li>
58   * </ul>
59   */
60  public class UserAdminServiceImpl implements UserAdminService {
61  	private final static Log log = LogFactory.getLog(UserAdminServiceImpl.class);
62  
63  	private UserAdmin userAdmin;
64  	@Deprecated
65  	private ServiceReference<UserAdmin> userAdminServiceReference;
66  	private Map<String, String> serviceProperties;
67  	private UserTransaction userTransaction;
68  
69  	@Override
70  	public String getMyMail() {
71  		return getUserMail(CurrentUser.getUsername());
72  	}
73  
74  	@Override
75  	public Role[] getRoles(String filter) throws InvalidSyntaxException {
76  		return userAdmin.getRoles(filter);
77  	}
78  
79  	// ALL USER: WARNING access to this will be later reduced
80  
81  	/** Retrieve a user given his dn */
82  	public User getUser(String dn) {
83  		return (User) getUserAdmin().getRole(dn);
84  	}
85  
86  	/** Can be a group or a user */
87  	public String getUserDisplayName(String dn) {
88  		// FIXME: during initialisation phase, the system logs "admin" as user
89  		// name rather than the corresponding dn
90  		if ("admin".equals(dn))
91  			return "System Administrator";
92  		else
93  			return UserAdminUtils.getUserDisplayName(getUserAdmin(), dn);
94  	}
95  
96  	@Override
97  	public String getUserMail(String dn) {
98  		return UserAdminUtils.getUserMail(getUserAdmin(), dn);
99  	}
100 
101 	/** Lists all roles of the given user */
102 	@Override
103 	public String[] getUserRoles(String dn) {
104 		Authorization currAuth = getUserAdmin().getAuthorization(getUser(dn));
105 		return currAuth.getRoles();
106 	}
107 
108 	@Override
109 	public boolean isUserInRole(String userDn, String roleDn) {
110 		String[] roles = getUserRoles(userDn);
111 		for (String role : roles) {
112 			if (role.equalsIgnoreCase(roleDn))
113 				return true;
114 		}
115 		return false;
116 	}
117 
118 	private final String[] knownProps = { LdapAttrs.cn.name(), LdapAttrs.sn.name(), LdapAttrs.givenName.name(),
119 			LdapAttrs.uid.name() };
120 
121 	public Set<User> listUsersInGroup(String groupDn, String filter) {
122 		Group group = (Group) userAdmin.getRole(groupDn);
123 		if (group == null)
124 			throw new ConnectException("Group " + groupDn + " not found");
125 		Set<User> users = new HashSet<User>();
126 		addUsers(users, group, filter);
127 		return users;
128 	}
129 
130 	/** Recursively add users to list */
131 	private void addUsers(Set<User> users, Group group, String filter) {
132 		Role[] roles = group.getMembers();
133 		for (Role role : roles) {
134 			if (role.getType() == Role.GROUP) {
135 				addUsers(users, (Group) role, filter);
136 			} else if (role.getType() == Role.USER) {
137 				if (match(role, filter))
138 					users.add((User) role);
139 			} else {
140 				// ignore
141 			}
142 		}
143 	}
144 
145 	public List<User> listGroups(String filter, boolean includeUsers, boolean includeSystemRoles) {
146 		Role[] roles = null;
147 		try {
148 			roles = getUserAdmin().getRoles(null);
149 		} catch (InvalidSyntaxException e) {
150 			throw new ConnectException("Unable to get roles with filter: " + filter, e);
151 		}
152 
153 		List<User> users = new ArrayList<User>();
154 		for (Role role : roles) {
155 			if ((includeUsers && role.getType() == Role.USER || role.getType() == Role.GROUP) && !users.contains(role)
156 					&& (includeSystemRoles || !role.getName().toLowerCase().endsWith(NodeConstants.ROLES_BASEDN))) {
157 				if (match(role, filter))
158 					users.add((User) role);
159 			}
160 		}
161 		return users;
162 	}
163 
164 	private boolean match(Role role, String filter) {
165 		boolean doFilter = filter != null && !"".equals(filter);
166 		if (doFilter) {
167 			for (String prop : knownProps) {
168 				Object currProp = null;
169 				try {
170 					currProp = role.getProperties().get(prop);
171 				} catch (Exception e) {
172 					throw e;
173 				}
174 				if (currProp != null) {
175 					String currPropStr = ((String) currProp).toLowerCase();
176 					if (currPropStr.contains(filter.toLowerCase())) {
177 						return true;
178 					}
179 				}
180 			}
181 			return false;
182 		} else
183 			return true;
184 	}
185 
186 	@Override
187 	public User getUserFromLocalId(String localId) {
188 		User user = getUserAdmin().getUser(LdapAttrs.uid.name(), localId);
189 		if (user == null)
190 			user = getUserAdmin().getUser(LdapAttrs.cn.name(), localId);
191 		return user;
192 	}
193 
194 	@Override
195 	public String buildDefaultDN(String localId, int type) {
196 		return buildDistinguishedName(localId, getDefaultDomainName(), type);
197 	}
198 
199 	@Override
200 	public String getDefaultDomainName() {
201 		Map<String, String> dns = getKnownBaseDns(true);
202 		if (dns.size() == 1)
203 			return dns.keySet().iterator().next();
204 		else
205 			throw new ConnectException("Current context contains " + dns.size() + " base dns: "
206 					+ dns.keySet().toString() + ". Unable to chose a default one.");
207 	}
208 
209 	public Map<String, String> getKnownBaseDns(boolean onlyWritable) {
210 		Map<String, String> dns = new HashMap<String, String>();
211 		String[] propertyKeys = userAdminServiceReference != null ? userAdminServiceReference.getPropertyKeys()
212 				: serviceProperties.keySet().toArray(new String[serviceProperties.size()]);
213 		for (String uri : propertyKeys) {
214 			if (!uri.startsWith("/"))
215 				continue;
216 			Dictionary<String, ?> props = UserAdminConf.uriAsProperties(uri);
217 			String readOnly = UserAdminConf.readOnly.getValue(props);
218 			String baseDn = UserAdminConf.baseDn.getValue(props);
219 
220 			if (onlyWritable && "true".equals(readOnly))
221 				continue;
222 			if (baseDn.equalsIgnoreCase(NodeConstants.ROLES_BASEDN))
223 				continue;
224 			if (baseDn.equalsIgnoreCase(NodeConstants.TOKENS_BASEDN))
225 				continue;
226 			dns.put(baseDn, uri);
227 		}
228 		return dns;
229 	}
230 
231 	public String buildDistinguishedName(String localId, String baseDn, int type) {
232 		Map<String, String> dns = getKnownBaseDns(true);
233 		Dictionary<String, ?> props = UserAdminConf.uriAsProperties(dns.get(baseDn));
234 		String dn = null;
235 		if (Role.GROUP == type)
236 			dn = LdapAttrs.cn.name() + "=" + localId + "," + UserAdminConf.groupBase.getValue(props) + "," + baseDn;
237 		else if (Role.USER == type)
238 			dn = LdapAttrs.uid.name() + "=" + localId + "," + UserAdminConf.userBase.getValue(props) + "," + baseDn;
239 		else
240 			throw new ConnectException("Unknown role type. " + "Cannot deduce dn for " + localId);
241 		return dn;
242 	}
243 
244 	@Override
245 	public void changeOwnPassword(char[] oldPassword, char[] newPassword) {
246 		String name = CurrentUser.getUsername();
247 		LdapName dn;
248 		try {
249 			dn = new LdapName(name);
250 		} catch (InvalidNameException e) {
251 			throw new ConnectException("Invalid user dn " + name, e);
252 		}
253 		User user = (User) userAdmin.getRole(dn.toString());
254 		if (!user.hasCredential(null, oldPassword))
255 			throw new ConnectException("Invalid password");
256 		if (Arrays.equals(newPassword, new char[0]))
257 			throw new ConnectException("New password empty");
258 		try {
259 			userTransaction.begin();
260 			user.getCredentials().put(null, newPassword);
261 			userTransaction.commit();
262 		} catch (Exception e) {
263 			try {
264 				userTransaction.rollback();
265 			} catch (Exception e1) {
266 				log.error("Could not roll back", e1);
267 			}
268 			if (e instanceof RuntimeException)
269 				throw (RuntimeException) e;
270 			else
271 				throw new ConnectException("Cannot change password", e);
272 		}
273 	}
274 
275 	public void resetPassword(String username, char[] newPassword) {
276 		LdapName dn;
277 		try {
278 			dn = new LdapName(username);
279 		} catch (InvalidNameException e) {
280 			throw new ConnectException("Invalid user dn " + username, e);
281 		}
282 		User user = (User) userAdmin.getRole(dn.toString());
283 		if (Arrays.equals(newPassword, new char[0]))
284 			throw new ConnectException("New password empty");
285 		try {
286 			userTransaction.begin();
287 			user.getCredentials().put(null, newPassword);
288 			userTransaction.commit();
289 		} catch (Exception e) {
290 			try {
291 				userTransaction.rollback();
292 			} catch (Exception e1) {
293 				log.error("Could not roll back", e1);
294 			}
295 			if (e instanceof RuntimeException)
296 				throw (RuntimeException) e;
297 			else
298 				throw new ConnectException("Cannot change password", e);
299 		}
300 	}
301 
302 	public String addSharedSecret(String email, int hours) {
303 		User user = (User) userAdmin.getUser(LdapAttrs.mail.name(), email);
304 		try {
305 			userTransaction.begin();
306 			String uuid = UUID.randomUUID().toString();
307 			SharedSecret sharedSecret = new SharedSecret(hours, uuid);
308 			user.getCredentials().put(SharedSecret.X_SHARED_SECRET, sharedSecret.toAuthPassword());
309 			String tokenStr = sharedSecret.getAuthInfo() + '$' + sharedSecret.getAuthValue();
310 			userTransaction.commit();
311 			return tokenStr;
312 		} catch (Exception e) {
313 			try {
314 				userTransaction.rollback();
315 			} catch (Exception e1) {
316 				log.error("Could not roll back", e1);
317 			}
318 			if (e instanceof RuntimeException)
319 				throw (RuntimeException) e;
320 			else
321 				throw new ConnectException("Cannot change password", e);
322 		}
323 	}
324 
325 	@Override
326 	public String addSharedSecret(String username, String authInfo, String authToken) {
327 		try {
328 			userTransaction.begin();
329 			User user = (User) userAdmin.getRole(username);
330 			SharedSecret sharedSecret = new SharedSecret(authInfo, authToken);
331 			user.getCredentials().put(SharedSecret.X_SHARED_SECRET, sharedSecret.toAuthPassword());
332 			String tokenStr = sharedSecret.getAuthInfo() + '$' + sharedSecret.getAuthValue();
333 			userTransaction.commit();
334 			return tokenStr;
335 		} catch (Exception e1) {
336 			try {
337 				if (userTransaction.getStatus() != Status.STATUS_NO_TRANSACTION)
338 					userTransaction.rollback();
339 			} catch (Exception e2) {
340 				if (log.isTraceEnabled())
341 					log.trace("Cannot rollback transaction", e2);
342 			}
343 			throw new ConnectException("Cannot add sheared secret", e1);
344 		}
345 	}
346 
347 	@Override
348 	public void expireAuthToken(String token) {
349 		try {
350 			userTransaction.begin();
351 			String dn = cn + "=" + token + "," + NodeConstants.TOKENS_BASEDN;
352 			Group tokenGroup = (Group) userAdmin.getRole(dn);
353 			String ldapDate = NamingUtils.instantToLdapDate(ZonedDateTime.now(ZoneOffset.UTC));
354 			tokenGroup.getProperties().put(description.name(), ldapDate);
355 			userTransaction.commit();
356 			if (log.isDebugEnabled())
357 				log.debug("Token " + token + " expired.");
358 		} catch (Exception e1) {
359 			try {
360 				if (userTransaction.getStatus() != Status.STATUS_NO_TRANSACTION)
361 					userTransaction.rollback();
362 			} catch (Exception e2) {
363 				if (log.isTraceEnabled())
364 					log.trace("Cannot rollback transaction", e2);
365 			}
366 			throw new ConnectException("Cannot expire token", e1);
367 		}
368 	}
369 
370 	@Override
371 	public void expireAuthTokens(Subject subject) {
372 		Set<String> tokens = TokenUtils.tokensUsed(subject, NodeConstants.TOKENS_BASEDN);
373 		for (String token : tokens)
374 			expireAuthToken(token);
375 	}
376 
377 	@Override
378 	public void addAuthToken(String userDn, String token, Integer hours, String... roles) {
379 		try {
380 			userTransaction.begin();
381 			User user = (User) userAdmin.getRole(userDn);
382 			String tokenDn = cn + "=" + token + "," + NodeConstants.TOKENS_BASEDN;
383 			Group tokenGroup = (Group) userAdmin.createRole(tokenDn, Role.GROUP);
384 			for (String role : roles) {
385 				Role r = userAdmin.getRole(role);
386 				if (r != null)
387 					tokenGroup.addMember(r);
388 				else {
389 					if (!role.equals(NodeConstants.ROLE_USER)) {
390 						throw new ConnectException("Cannot add role " + role + " to token " + token + " for " + userDn);
391 					}
392 				}
393 			}
394 			tokenGroup.getProperties().put(owner.name(), user.getName());
395 			if (hours != null) {
396 				String ldapDate = NamingUtils.instantToLdapDate(ZonedDateTime.now().plusHours(hours));
397 				tokenGroup.getProperties().put(description.name(), ldapDate);
398 			}
399 			userTransaction.commit();
400 		} catch (Exception e1) {
401 			try {
402 				if (userTransaction.getStatus() != Status.STATUS_NO_TRANSACTION)
403 					userTransaction.rollback();
404 			} catch (Exception e2) {
405 				if (log.isTraceEnabled())
406 					log.trace("Cannot rollback transaction", e2);
407 			}
408 			throw new ConnectException("Cannot add token", e1);
409 		}
410 	}
411 
412 	public User createUserFromPerson(Node person) {
413 		String email = JcrUtils.get(person, PeopleNames.PEOPLE_PRIMARY_EMAIL);
414 		String dn = buildDefaultDN(email, Role.USER);
415 		User user;
416 		try {
417 			userTransaction.begin();
418 			user = (User) userAdmin.createRole(dn, Role.USER);
419 			Dictionary<String, Object> userProperties = user.getProperties();
420 			String name = JcrUtils.get(person, PeopleNames.PEOPLE_DISPLAY_NAME);
421 			userProperties.put(LdapAttrs.cn.name(), name);
422 			userProperties.put(LdapAttrs.displayName.name(), name);
423 			String givenName = JcrUtils.get(person, PeopleNames.PEOPLE_FIRST_NAME);
424 			String surname = JcrUtils.get(person, PeopleNames.PEOPLE_LAST_NAME);
425 			userProperties.put(LdapAttrs.givenName.name(), givenName);
426 			userProperties.put(LdapAttrs.sn.name(), surname);
427 			userProperties.put(LdapAttrs.mail.name(), email.toLowerCase());
428 			userTransaction.commit();
429 		} catch (Exception e) {
430 			try {
431 				userTransaction.rollback();
432 			} catch (Exception e1) {
433 				log.error("Could not roll back", e1);
434 			}
435 			if (e instanceof RuntimeException)
436 				throw (RuntimeException) e;
437 			else
438 				throw new ConnectException("Cannot create user", e);
439 		}
440 		return user;
441 	}
442 
443 	public UserAdmin getUserAdmin() {
444 		return userAdmin;
445 	}
446 
447 	public UserTransaction getUserTransaction() {
448 		return userTransaction;
449 	}
450 
451 	/* DEPENDENCY INJECTION */
452 	public void setUserAdmin(UserAdmin userAdmin, Map<String, String> serviceProperties) {
453 		this.userAdmin = userAdmin;
454 		this.serviceProperties = serviceProperties;
455 	}
456 
457 	@Deprecated
458 	public void setUserAdminOld(UserAdmin userAdmin) {
459 		this.userAdmin = userAdmin;
460 	}
461 
462 	public void setUserTransaction(UserTransaction userTransaction) {
463 		this.userTransaction = userTransaction;
464 	}
465 
466 	@Deprecated
467 	public void setUserAdminServiceReference(ServiceReference<UserAdmin> userAdminServiceReference) {
468 		this.userAdminServiceReference = userAdminServiceReference;
469 	}
470 }