]> git.argeo.org Git - lgpl/argeo-commons.git/blob - CmsUserManagerImpl.java
e69cda64459ac05058058b2a96d9ab317da149ae
[lgpl/argeo-commons.git] / CmsUserManagerImpl.java
1 package org.argeo.cms.internal.auth;
2
3 import static org.argeo.util.naming.LdapAttrs.cn;
4 import static org.argeo.util.naming.LdapAttrs.description;
5 import static org.argeo.util.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.Collections;
12 import java.util.Dictionary;
13 import java.util.HashMap;
14 import java.util.HashSet;
15 import java.util.Hashtable;
16 import java.util.LinkedHashMap;
17 import java.util.List;
18 import java.util.Map;
19 import java.util.NavigableMap;
20 import java.util.Set;
21 import java.util.TreeMap;
22 import java.util.TreeSet;
23 import java.util.UUID;
24
25 import javax.naming.InvalidNameException;
26 import javax.naming.ldap.LdapName;
27 import javax.security.auth.Subject;
28
29 import org.argeo.api.cms.CmsConstants;
30 import org.argeo.api.cms.CmsLog;
31 import org.argeo.cms.CmsUserManager;
32 import org.argeo.cms.auth.CurrentUser;
33 import org.argeo.cms.auth.UserAdminUtils;
34 import org.argeo.osgi.transaction.WorkTransaction;
35 import org.argeo.osgi.useradmin.TokenUtils;
36 import org.argeo.osgi.useradmin.UserAdminConf;
37 import org.argeo.osgi.useradmin.UserDirectory;
38 import org.argeo.util.naming.LdapAttrs;
39 import org.argeo.util.naming.NamingUtils;
40 import org.argeo.util.naming.SharedSecret;
41 import org.osgi.framework.InvalidSyntaxException;
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 CmsUserManager}. 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 CmsUserManagerImpl implements CmsUserManager {
61 private final static CmsLog log = CmsLog.getLog(CmsUserManagerImpl.class);
62
63 private UserAdmin userAdmin;
64 // private Map<String, String> serviceProperties;
65 private WorkTransaction userTransaction;
66
67 private Map<UserDirectory, Hashtable<String, Object>> userDirectories = Collections
68 .synchronizedMap(new LinkedHashMap<>());
69
70 @Override
71 public String getMyMail() {
72 return getUserMail(CurrentUser.getUsername());
73 }
74
75 @Override
76 public Role[] getRoles(String filter) throws InvalidSyntaxException {
77 return userAdmin.getRoles(filter);
78 }
79
80 // ALL USER: WARNING access to this will be later reduced
81
82 /** Retrieve a user given his dn */
83 public User getUser(String dn) {
84 return (User) getUserAdmin().getRole(dn);
85 }
86
87 /** Can be a group or a user */
88 public String getUserDisplayName(String dn) {
89 // FIXME: during initialisation phase, the system logs "admin" as user
90 // name rather than the corresponding dn
91 if ("admin".equals(dn))
92 return "System Administrator";
93 else
94 return UserAdminUtils.getUserDisplayName(getUserAdmin(), dn);
95 }
96
97 @Override
98 public String getUserMail(String dn) {
99 return UserAdminUtils.getUserMail(getUserAdmin(), dn);
100 }
101
102 /** Lists all roles of the given user */
103 @Override
104 public String[] getUserRoles(String dn) {
105 Authorization currAuth = getUserAdmin().getAuthorization(getUser(dn));
106 return currAuth.getRoles();
107 }
108
109 @Override
110 public boolean isUserInRole(String userDn, String roleDn) {
111 String[] roles = getUserRoles(userDn);
112 for (String role : roles) {
113 if (role.equalsIgnoreCase(roleDn))
114 return true;
115 }
116 return false;
117 }
118
119 private final String[] knownProps = { LdapAttrs.cn.name(), LdapAttrs.sn.name(), LdapAttrs.givenName.name(),
120 LdapAttrs.uid.name() };
121
122 public Set<User> listUsersInGroup(String groupDn, String filter) {
123 Group group = (Group) userAdmin.getRole(groupDn);
124 if (group == null)
125 throw new IllegalArgumentException("Group " + groupDn + " not found");
126 Set<User> users = new HashSet<User>();
127 addUsers(users, group, filter);
128 return users;
129 }
130
131 /** Recursively add users to list */
132 private void addUsers(Set<User> users, Group group, String filter) {
133 Role[] roles = group.getMembers();
134 for (Role role : roles) {
135 if (role.getType() == Role.GROUP) {
136 addUsers(users, (Group) role, filter);
137 } else if (role.getType() == Role.USER) {
138 if (match(role, filter))
139 users.add((User) role);
140 } else {
141 // ignore
142 }
143 }
144 }
145
146 public List<User> listGroups(String filter, boolean includeUsers, boolean includeSystemRoles) {
147 Role[] roles = null;
148 try {
149 roles = getUserAdmin().getRoles(filter);
150 } catch (InvalidSyntaxException e) {
151 throw new IllegalArgumentException("Unable to get roles with filter: " + filter, e);
152 }
153
154 List<User> users = new ArrayList<User>();
155 for (Role role : roles) {
156 if ((includeUsers && role.getType() == Role.USER || role.getType() == Role.GROUP) && !users.contains(role)
157 && (includeSystemRoles || !role.getName().toLowerCase().endsWith(CmsConstants.ROLES_BASEDN))) {
158 if (match(role, filter))
159 users.add((User) role);
160 }
161 }
162 return users;
163 }
164
165 private boolean match(Role role, String filter) {
166 boolean doFilter = filter != null && !"".equals(filter);
167 if (doFilter) {
168 for (String prop : knownProps) {
169 Object currProp = null;
170 try {
171 currProp = role.getProperties().get(prop);
172 } catch (Exception e) {
173 throw e;
174 }
175 if (currProp != null) {
176 String currPropStr = ((String) currProp).toLowerCase();
177 if (currPropStr.contains(filter.toLowerCase())) {
178 return true;
179 }
180 }
181 }
182 return false;
183 } else
184 return true;
185 }
186
187 @Override
188 public User getUserFromLocalId(String localId) {
189 User user = getUserAdmin().getUser(LdapAttrs.uid.name(), localId);
190 if (user == null)
191 user = getUserAdmin().getUser(LdapAttrs.cn.name(), localId);
192 return user;
193 }
194
195 @Override
196 public String buildDefaultDN(String localId, int type) {
197 return buildDistinguishedName(localId, getDefaultDomainName(), type);
198 }
199
200 @Override
201 public String getDefaultDomainName() {
202 Map<String, String> dns = getKnownBaseDns(true);
203 if (dns.size() == 1)
204 return dns.keySet().iterator().next();
205 else
206 throw new IllegalStateException("Current context contains " + dns.size() + " base dns: "
207 + dns.keySet().toString() + ". Unable to chose a default one.");
208 }
209
210 // public Map<String, String> getKnownBaseDns(boolean onlyWritable) {
211 // Map<String, String> dns = new HashMap<String, String>();
212 // String[] propertyKeys = 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 Map<String, String> getKnownBaseDns(boolean onlyWritable) {
232 Map<String, String> dns = new HashMap<String, String>();
233 for (UserDirectory userDirectory : userDirectories.keySet()) {
234 Boolean readOnly = userDirectory.isReadOnly();
235 String baseDn = userDirectory.getContext();
236
237 if (onlyWritable && readOnly)
238 continue;
239 if (baseDn.equalsIgnoreCase(CmsConstants.ROLES_BASEDN))
240 continue;
241 if (baseDn.equalsIgnoreCase(CmsConstants.TOKENS_BASEDN))
242 continue;
243 dns.put(baseDn, UserAdminConf.propertiesAsUri(userDirectories.get(userDirectory)).toString());
244
245 }
246 return dns;
247 }
248
249 public Set<UserDirectory> getUserDirectories() {
250 TreeSet<UserDirectory> res = new TreeSet<>((o1, o2) -> o1.getContext().compareTo(o2.getContext()));
251 res.addAll(userDirectories.keySet());
252 return res;
253 }
254
255 public String buildDistinguishedName(String localId, String baseDn, int type) {
256 Map<String, String> dns = getKnownBaseDns(true);
257 Dictionary<String, ?> props = UserAdminConf.uriAsProperties(dns.get(baseDn));
258 String dn = null;
259 if (Role.GROUP == type)
260 dn = LdapAttrs.cn.name() + "=" + localId + "," + UserAdminConf.groupBase.getValue(props) + "," + baseDn;
261 else if (Role.USER == type)
262 dn = LdapAttrs.uid.name() + "=" + localId + "," + UserAdminConf.userBase.getValue(props) + "," + baseDn;
263 else
264 throw new IllegalStateException("Unknown role type. " + "Cannot deduce dn for " + localId);
265 return dn;
266 }
267
268 @Override
269 public void changeOwnPassword(char[] oldPassword, char[] newPassword) {
270 String name = CurrentUser.getUsername();
271 LdapName dn;
272 try {
273 dn = new LdapName(name);
274 } catch (InvalidNameException e) {
275 throw new IllegalArgumentException("Invalid user dn " + name, e);
276 }
277 User user = (User) userAdmin.getRole(dn.toString());
278 if (!user.hasCredential(null, oldPassword))
279 throw new IllegalArgumentException("Invalid password");
280 if (Arrays.equals(newPassword, new char[0]))
281 throw new IllegalArgumentException("New password empty");
282 try {
283 userTransaction.begin();
284 user.getCredentials().put(null, newPassword);
285 userTransaction.commit();
286 } catch (Exception e) {
287 try {
288 userTransaction.rollback();
289 } catch (Exception e1) {
290 log.error("Could not roll back", e1);
291 }
292 if (e instanceof RuntimeException)
293 throw (RuntimeException) e;
294 else
295 throw new RuntimeException("Cannot change password", e);
296 }
297 }
298
299 public void resetPassword(String username, char[] newPassword) {
300 LdapName dn;
301 try {
302 dn = new LdapName(username);
303 } catch (InvalidNameException e) {
304 throw new IllegalArgumentException("Invalid user dn " + username, e);
305 }
306 User user = (User) userAdmin.getRole(dn.toString());
307 if (Arrays.equals(newPassword, new char[0]))
308 throw new IllegalArgumentException("New password empty");
309 try {
310 userTransaction.begin();
311 user.getCredentials().put(null, newPassword);
312 userTransaction.commit();
313 } catch (Exception e) {
314 try {
315 userTransaction.rollback();
316 } catch (Exception e1) {
317 log.error("Could not roll back", e1);
318 }
319 if (e instanceof RuntimeException)
320 throw (RuntimeException) e;
321 else
322 throw new RuntimeException("Cannot change password", e);
323 }
324 }
325
326 public String addSharedSecret(String email, int hours) {
327 User user = (User) userAdmin.getUser(LdapAttrs.mail.name(), email);
328 try {
329 userTransaction.begin();
330 String uuid = UUID.randomUUID().toString();
331 SharedSecret sharedSecret = new SharedSecret(hours, uuid);
332 user.getCredentials().put(SharedSecret.X_SHARED_SECRET, sharedSecret.toAuthPassword());
333 String tokenStr = sharedSecret.getAuthInfo() + '$' + sharedSecret.getAuthValue();
334 userTransaction.commit();
335 return tokenStr;
336 } catch (Exception e) {
337 try {
338 userTransaction.rollback();
339 } catch (Exception e1) {
340 log.error("Could not roll back", e1);
341 }
342 if (e instanceof RuntimeException)
343 throw (RuntimeException) e;
344 else
345 throw new RuntimeException("Cannot change password", e);
346 }
347 }
348
349 @Deprecated
350 public String addSharedSecret(String username, String authInfo, String authToken) {
351 try {
352 userTransaction.begin();
353 User user = (User) userAdmin.getRole(username);
354 SharedSecret sharedSecret = new SharedSecret(authInfo, authToken);
355 user.getCredentials().put(SharedSecret.X_SHARED_SECRET, sharedSecret.toAuthPassword());
356 String tokenStr = sharedSecret.getAuthInfo() + '$' + sharedSecret.getAuthValue();
357 userTransaction.commit();
358 return tokenStr;
359 } catch (Exception e1) {
360 try {
361 if (!userTransaction.isNoTransactionStatus())
362 userTransaction.rollback();
363 } catch (Exception e2) {
364 if (log.isTraceEnabled())
365 log.trace("Cannot rollback transaction", e2);
366 }
367 throw new RuntimeException("Cannot add shared secret", e1);
368 }
369 }
370
371 @Override
372 public void expireAuthToken(String token) {
373 try {
374 userTransaction.begin();
375 String dn = cn + "=" + token + "," + CmsConstants.TOKENS_BASEDN;
376 Group tokenGroup = (Group) userAdmin.getRole(dn);
377 String ldapDate = NamingUtils.instantToLdapDate(ZonedDateTime.now(ZoneOffset.UTC));
378 tokenGroup.getProperties().put(description.name(), ldapDate);
379 userTransaction.commit();
380 if (log.isDebugEnabled())
381 log.debug("Token " + token + " expired.");
382 } catch (Exception e1) {
383 try {
384 if (!userTransaction.isNoTransactionStatus())
385 userTransaction.rollback();
386 } catch (Exception e2) {
387 if (log.isTraceEnabled())
388 log.trace("Cannot rollback transaction", e2);
389 }
390 throw new RuntimeException("Cannot expire token", e1);
391 }
392 }
393
394 @Override
395 public void expireAuthTokens(Subject subject) {
396 Set<String> tokens = TokenUtils.tokensUsed(subject, CmsConstants.TOKENS_BASEDN);
397 for (String token : tokens)
398 expireAuthToken(token);
399 }
400
401 @Override
402 public void addAuthToken(String userDn, String token, Integer hours, String... roles) {
403 addAuthToken(userDn, token, ZonedDateTime.now().plusHours(hours), roles);
404 }
405
406 @Override
407 public void addAuthToken(String userDn, String token, ZonedDateTime expiryDate, String... roles) {
408 try {
409 userTransaction.begin();
410 User user = (User) userAdmin.getRole(userDn);
411 String tokenDn = cn + "=" + token + "," + CmsConstants.TOKENS_BASEDN;
412 Group tokenGroup = (Group) userAdmin.createRole(tokenDn, Role.GROUP);
413 if (roles != null)
414 for (String role : roles) {
415 Role r = userAdmin.getRole(role);
416 if (r != null)
417 tokenGroup.addMember(r);
418 else {
419 if (!role.equals(CmsConstants.ROLE_USER)) {
420 throw new IllegalStateException(
421 "Cannot add role " + role + " to token " + token + " for " + userDn);
422 }
423 }
424 }
425 tokenGroup.getProperties().put(owner.name(), user.getName());
426 if (expiryDate != null) {
427 String ldapDate = NamingUtils.instantToLdapDate(expiryDate);
428 tokenGroup.getProperties().put(description.name(), ldapDate);
429 }
430 userTransaction.commit();
431 } catch (Exception e1) {
432 try {
433 if (!userTransaction.isNoTransactionStatus())
434 userTransaction.rollback();
435 } catch (Exception e2) {
436 if (log.isTraceEnabled())
437 log.trace("Cannot rollback transaction", e2);
438 }
439 throw new RuntimeException("Cannot add token", e1);
440 }
441 }
442
443 @Override
444 public UserDirectory getDirectory(Role user) {
445 String name = user.getName();
446 NavigableMap<String, UserDirectory> possible = new TreeMap<>();
447 for (UserDirectory userDirectory : userDirectories.keySet()) {
448 if (name.endsWith(userDirectory.getContext())) {
449 possible.put(userDirectory.getContext(), userDirectory);
450 }
451 }
452 if (possible.size() == 0)
453 throw new IllegalStateException("No user directory found for user " + name);
454 return possible.lastEntry().getValue();
455 }
456
457 // public User createUserFromPerson(Node person) {
458 // String email = JcrUtils.get(person, LdapAttrs.mail.property());
459 // String dn = buildDefaultDN(email, Role.USER);
460 // User user;
461 // try {
462 // userTransaction.begin();
463 // user = (User) userAdmin.createRole(dn, Role.USER);
464 // Dictionary<String, Object> userProperties = user.getProperties();
465 // String name = JcrUtils.get(person, LdapAttrs.displayName.property());
466 // userProperties.put(LdapAttrs.cn.name(), name);
467 // userProperties.put(LdapAttrs.displayName.name(), name);
468 // String givenName = JcrUtils.get(person, LdapAttrs.givenName.property());
469 // String surname = JcrUtils.get(person, LdapAttrs.sn.property());
470 // userProperties.put(LdapAttrs.givenName.name(), givenName);
471 // userProperties.put(LdapAttrs.sn.name(), surname);
472 // userProperties.put(LdapAttrs.mail.name(), email.toLowerCase());
473 // userTransaction.commit();
474 // } catch (Exception e) {
475 // try {
476 // userTransaction.rollback();
477 // } catch (Exception e1) {
478 // log.error("Could not roll back", e1);
479 // }
480 // if (e instanceof RuntimeException)
481 // throw (RuntimeException) e;
482 // else
483 // throw new RuntimeException("Cannot create user", e);
484 // }
485 // return user;
486 // }
487
488 public UserAdmin getUserAdmin() {
489 return userAdmin;
490 }
491
492 // public UserTransaction getUserTransaction() {
493 // return userTransaction;
494 // }
495
496 /* DEPENDENCY INJECTION */
497 public void setUserAdmin(UserAdmin userAdmin) {
498 this.userAdmin = userAdmin;
499 // this.serviceProperties = serviceProperties;
500 }
501
502 public void setUserTransaction(WorkTransaction userTransaction) {
503 this.userTransaction = userTransaction;
504 }
505
506 public void addUserDirectory(UserDirectory userDirectory, Map<String, Object> properties) {
507 userDirectories.put(userDirectory, new Hashtable<>(properties));
508 }
509
510 public void removeUserDirectory(UserDirectory userDirectory, Map<String, Object> properties) {
511 userDirectories.remove(userDirectory);
512 }
513
514 }