]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/internal/auth/CmsUserManagerImpl.java
Support writing file as XML
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / internal / auth / 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.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.NavigableMap;
17 import java.util.Set;
18 import java.util.TreeMap;
19 import java.util.TreeSet;
20 import java.util.UUID;
21
22 import javax.naming.InvalidNameException;
23 import javax.naming.ldap.LdapName;
24 import javax.security.auth.Subject;
25
26 import org.argeo.api.acr.NamespaceUtils;
27 import org.argeo.api.cms.CmsConstants;
28 import org.argeo.api.cms.CmsLog;
29 import org.argeo.cms.CmsUserManager;
30 import org.argeo.cms.auth.CurrentUser;
31 import org.argeo.cms.auth.SystemRole;
32 import org.argeo.cms.auth.UserAdminUtils;
33 import org.argeo.osgi.useradmin.AggregatingUserAdmin;
34 import org.argeo.osgi.useradmin.TokenUtils;
35 import org.argeo.osgi.useradmin.UserDirectory;
36 import org.argeo.util.directory.DirectoryConf;
37 import org.argeo.util.directory.HierarchyUnit;
38 import org.argeo.util.directory.ldap.LdapEntry;
39 import org.argeo.util.directory.ldap.SharedSecret;
40 import org.argeo.util.naming.LdapAttrs;
41 import org.argeo.util.naming.NamingUtils;
42 import org.argeo.util.transaction.WorkTransaction;
43 import org.osgi.framework.InvalidSyntaxException;
44 import org.osgi.service.useradmin.Authorization;
45 import org.osgi.service.useradmin.Group;
46 import org.osgi.service.useradmin.Role;
47 import org.osgi.service.useradmin.User;
48 import org.osgi.service.useradmin.UserAdmin;
49
50 /**
51 * Canonical implementation of the people {@link CmsUserManager}. Wraps
52 * interaction with users and groups.
53 *
54 * In a *READ-ONLY* mode. We want to be able to:
55 * <ul>
56 * <li>Retrieve my user and corresponding information (main info,
57 * groups...)</li>
58 * <li>List all local groups (not the system roles)</li>
59 * <li>If sufficient rights: retrieve a given user and its information</li>
60 * </ul>
61 */
62 public class CmsUserManagerImpl implements CmsUserManager {
63 private final static CmsLog log = CmsLog.getLog(CmsUserManagerImpl.class);
64
65 private UserAdmin userAdmin;
66 // private Map<String, String> serviceProperties;
67 private WorkTransaction userTransaction;
68
69 private final String[] knownProps = { LdapAttrs.cn.name(), LdapAttrs.sn.name(), LdapAttrs.givenName.name(),
70 LdapAttrs.uid.name() };
71
72 // private Map<UserDirectory, Hashtable<String, Object>> userDirectories = Collections
73 // .synchronizedMap(new LinkedHashMap<>());
74
75 private Set<UserDirectory> userDirectories = new HashSet<>();
76
77 public void start() {
78 log.debug(() -> "CMS user manager available");
79 }
80
81 public void stop() {
82
83 }
84
85 @Override
86 public String getMyMail() {
87 return getUserMail(CurrentUser.getUsername());
88 }
89
90 @Override
91 public Role[] getRoles(String filter) throws InvalidSyntaxException {
92 return userAdmin.getRoles(filter);
93 }
94
95 // ALL USER: WARNING access to this will be later reduced
96
97 /** Retrieve a user given his dn */
98 public User getUser(String dn) {
99 return (User) getUserAdmin().getRole(dn);
100 }
101
102 /** Can be a group or a user */
103 public String getUserDisplayName(String dn) {
104 // FIXME: during initialisation phase, the system logs "admin" as user
105 // name rather than the corresponding dn
106 if ("admin".equals(dn))
107 return "System Administrator";
108 else
109 return UserAdminUtils.getUserDisplayName(getUserAdmin(), dn);
110 }
111
112 @Override
113 public String getUserMail(String dn) {
114 return UserAdminUtils.getUserMail(getUserAdmin(), dn);
115 }
116
117 /** Lists all roles of the given user */
118 @Override
119 public String[] getUserRoles(String dn) {
120 Authorization currAuth = getUserAdmin().getAuthorization(getUser(dn));
121 return currAuth.getRoles();
122 }
123
124 @Override
125 public boolean isUserInRole(String userDn, String roleDn) {
126 String[] roles = getUserRoles(userDn);
127 for (String role : roles) {
128 if (role.equalsIgnoreCase(roleDn))
129 return true;
130 }
131 return false;
132 }
133
134 public Set<User> listUsersInGroup(String groupDn, String filter) {
135 Group group = (Group) userAdmin.getRole(groupDn);
136 if (group == null)
137 throw new IllegalArgumentException("Group " + groupDn + " not found");
138 Set<User> users = new HashSet<User>();
139 addUsers(users, group, filter);
140 return users;
141 }
142
143 /** Recursively add users to list */
144 private void addUsers(Set<User> users, Group group, String filter) {
145 Role[] roles = group.getMembers();
146 for (Role role : roles) {
147 if (role.getType() == Role.GROUP) {
148 addUsers(users, (Group) role, filter);
149 } else if (role.getType() == Role.USER) {
150 if (match(role, filter))
151 users.add((User) role);
152 } else {
153 // ignore
154 }
155 }
156 }
157
158 public List<User> listGroups(String filter, boolean includeUsers, boolean includeSystemRoles) {
159 Role[] roles = null;
160 try {
161 roles = getUserAdmin().getRoles(filter);
162 } catch (InvalidSyntaxException e) {
163 throw new IllegalArgumentException("Unable to get roles with filter: " + filter, e);
164 }
165
166 List<User> users = new ArrayList<User>();
167 for (Role role : roles) {
168 if ((includeUsers && role.getType() == Role.USER || role.getType() == Role.GROUP) && !users.contains(role)
169 && (includeSystemRoles || !role.getName().toLowerCase().endsWith(CmsConstants.SYSTEM_ROLES_BASEDN))) {
170 if (match(role, filter))
171 users.add((User) role);
172 }
173 }
174 return users;
175 }
176
177 private boolean match(Role role, String filter) {
178 boolean doFilter = filter != null && !"".equals(filter);
179 if (doFilter) {
180 for (String prop : knownProps) {
181 Object currProp = null;
182 try {
183 currProp = role.getProperties().get(prop);
184 } catch (Exception e) {
185 throw e;
186 }
187 if (currProp != null) {
188 String currPropStr = ((String) currProp).toLowerCase();
189 if (currPropStr.contains(filter.toLowerCase())) {
190 return true;
191 }
192 }
193 }
194 return false;
195 } else
196 return true;
197 }
198
199 @Override
200 public User getUserFromLocalId(String localId) {
201 User user = getUserAdmin().getUser(LdapAttrs.uid.name(), localId);
202 if (user == null)
203 user = getUserAdmin().getUser(LdapAttrs.cn.name(), localId);
204 return user;
205 }
206
207 @Override
208 public String buildDefaultDN(String localId, int type) {
209 return buildDistinguishedName(localId, getDefaultDomainName(), type);
210 }
211
212 /*
213 * EDITION
214 */
215 @Override
216 public User createUser(String username, Map<String, Object> properties, Map<String, Object> credentials) {
217 try {
218 userTransaction.begin();
219 User user = (User) userAdmin.createRole(username, Role.USER);
220 if (properties != null) {
221 for (String key : properties.keySet())
222 user.getProperties().put(key, properties.get(key));
223 }
224 if (credentials != null) {
225 for (String key : credentials.keySet())
226 user.getCredentials().put(key, credentials.get(key));
227 }
228 userTransaction.commit();
229 return user;
230 } catch (Exception e) {
231 try {
232 userTransaction.rollback();
233 } catch (Exception e1) {
234 log.error("Could not roll back", e1);
235 }
236 if (e instanceof RuntimeException)
237 throw (RuntimeException) e;
238 else
239 throw new RuntimeException("Cannot create user " + username, e);
240 }
241 }
242
243 @Override
244 public Group getOrCreateGroup(HierarchyUnit groups, String commonName) {
245 try {
246 String dn = LdapAttrs.cn.name() + "=" + commonName + "," + groups.getBase();
247 Group group = (Group) getUserAdmin().getRole(dn);
248 if (group != null)
249 return group;
250 userTransaction.begin();
251 group = (Group) userAdmin.createRole(dn, Role.GROUP);
252 userTransaction.commit();
253 return group;
254 } catch (Exception e) {
255 try {
256 userTransaction.rollback();
257 } catch (Exception e1) {
258 log.error("Could not roll back", e1);
259 }
260 if (e instanceof RuntimeException)
261 throw (RuntimeException) e;
262 else
263 throw new RuntimeException("Cannot create group " + commonName + " in " + groups, e);
264 }
265 }
266
267 @Override
268 public Group getOrCreateSystemRole(HierarchyUnit roles, SystemRole systemRole) {
269 try {
270 String dn = LdapAttrs.cn.name() + "=" + NamespaceUtils.toPrefixedName(systemRole.getName()) + ","
271 + roles.getBase();
272 Group group = (Group) getUserAdmin().getRole(dn);
273 if (group != null)
274 return group;
275 userTransaction.begin();
276 group = (Group) userAdmin.createRole(dn, Role.GROUP);
277 userTransaction.commit();
278 return group;
279 } catch (Exception e) {
280 try {
281 userTransaction.rollback();
282 } catch (Exception e1) {
283 log.error("Could not roll back", e1);
284 }
285 if (e instanceof RuntimeException)
286 throw (RuntimeException) e;
287 else
288 throw new RuntimeException("Cannot create system role " + systemRole + " in " + roles, e);
289 }
290 }
291
292 @Override
293 public HierarchyUnit getOrCreateHierarchyUnit(UserDirectory directory, String path) {
294 HierarchyUnit hi = directory.getHierarchyUnit(path);
295 if (hi != null)
296 return hi;
297 try {
298 userTransaction.begin();
299 HierarchyUnit hierarchyUnit = directory.createHierarchyUnit(path);
300 userTransaction.commit();
301 return hierarchyUnit;
302 } catch (Exception e1) {
303 try {
304 if (!userTransaction.isNoTransactionStatus())
305 userTransaction.rollback();
306 } catch (Exception e2) {
307 if (log.isTraceEnabled())
308 log.trace("Cannot rollback transaction", e2);
309 }
310 throw new RuntimeException("Cannot create hierarchy unit " + path + " in directory " + directory, e1);
311 }
312 }
313
314 @Override
315 public void addObjectClasses(Role role, Set<String> objectClasses, Map<String, Object> additionalProperties) {
316 try {
317 userTransaction.begin();
318 LdapEntry.addObjectClasses(role.getProperties(), objectClasses);
319 for (String key : additionalProperties.keySet()) {
320 role.getProperties().put(key, additionalProperties.get(key));
321 }
322 userTransaction.commit();
323 } catch (Exception e1) {
324 try {
325 if (!userTransaction.isNoTransactionStatus())
326 userTransaction.rollback();
327 } catch (Exception e2) {
328 if (log.isTraceEnabled())
329 log.trace("Cannot rollback transaction", e2);
330 }
331 throw new RuntimeException("Cannot add object classes " + objectClasses + " to " + role, e1);
332 }
333 }
334
335 @Override
336 public void addMember(Group group, Role role) {
337 try {
338 userTransaction.begin();
339 group.addMember(role);
340 userTransaction.commit();
341 } catch (Exception e1) {
342 try {
343 if (!userTransaction.isNoTransactionStatus())
344 userTransaction.rollback();
345 } catch (Exception e2) {
346 if (log.isTraceEnabled())
347 log.trace("Cannot rollback transaction", e2);
348 }
349 throw new RuntimeException("Cannot add object classes " + role + " to group " + group, e1);
350 }
351 }
352
353 @Override
354 public String getDefaultDomainName() {
355 Map<String, String> dns = getKnownBaseDns(true);
356 if (dns.size() == 1)
357 return dns.keySet().iterator().next();
358 else
359 throw new IllegalStateException("Current context contains " + dns.size() + " base dns: "
360 + dns.keySet().toString() + ". Unable to chose a default one.");
361 }
362
363 public Map<String, String> getKnownBaseDns(boolean onlyWritable) {
364 Map<String, String> dns = new HashMap<String, String>();
365 for (UserDirectory userDirectory : userDirectories) {
366 Boolean readOnly = userDirectory.isReadOnly();
367 String baseDn = userDirectory.getBase();
368
369 if (onlyWritable && readOnly)
370 continue;
371 if (baseDn.equalsIgnoreCase(CmsConstants.SYSTEM_ROLES_BASEDN))
372 continue;
373 if (baseDn.equalsIgnoreCase(CmsConstants.TOKENS_BASEDN))
374 continue;
375 dns.put(baseDn, DirectoryConf.propertiesAsUri(userDirectory.getProperties()).toString());
376
377 }
378 return dns;
379 }
380
381 public Set<UserDirectory> getUserDirectories() {
382 TreeSet<UserDirectory> res = new TreeSet<>((o1, o2) -> o1.getBase().compareTo(o2.getBase()));
383 res.addAll(userDirectories);
384 return res;
385 }
386
387 public String buildDistinguishedName(String localId, String baseDn, int type) {
388 Map<String, String> dns = getKnownBaseDns(true);
389 Dictionary<String, ?> props = DirectoryConf.uriAsProperties(dns.get(baseDn));
390 String dn = null;
391 if (Role.GROUP == type)
392 dn = LdapAttrs.cn.name() + "=" + localId + "," + DirectoryConf.groupBase.getValue(props) + "," + baseDn;
393 else if (Role.USER == type)
394 dn = LdapAttrs.uid.name() + "=" + localId + "," + DirectoryConf.userBase.getValue(props) + "," + baseDn;
395 else
396 throw new IllegalStateException("Unknown role type. " + "Cannot deduce dn for " + localId);
397 return dn;
398 }
399
400 @Override
401 public void changeOwnPassword(char[] oldPassword, char[] newPassword) {
402 String name = CurrentUser.getUsername();
403 LdapName dn;
404 try {
405 dn = new LdapName(name);
406 } catch (InvalidNameException e) {
407 throw new IllegalArgumentException("Invalid user dn " + name, e);
408 }
409 User user = (User) userAdmin.getRole(dn.toString());
410 if (!user.hasCredential(null, oldPassword))
411 throw new IllegalArgumentException("Invalid password");
412 if (Arrays.equals(newPassword, new char[0]))
413 throw new IllegalArgumentException("New password empty");
414 try {
415 userTransaction.begin();
416 user.getCredentials().put(null, newPassword);
417 userTransaction.commit();
418 } catch (Exception e) {
419 try {
420 userTransaction.rollback();
421 } catch (Exception e1) {
422 log.error("Could not roll back", e1);
423 }
424 if (e instanceof RuntimeException)
425 throw (RuntimeException) e;
426 else
427 throw new RuntimeException("Cannot change password", e);
428 }
429 }
430
431 public void resetPassword(String username, char[] newPassword) {
432 LdapName dn;
433 try {
434 dn = new LdapName(username);
435 } catch (InvalidNameException e) {
436 throw new IllegalArgumentException("Invalid user dn " + username, e);
437 }
438 User user = (User) userAdmin.getRole(dn.toString());
439 if (Arrays.equals(newPassword, new char[0]))
440 throw new IllegalArgumentException("New password empty");
441 try {
442 userTransaction.begin();
443 user.getCredentials().put(null, newPassword);
444 userTransaction.commit();
445 } catch (Exception e) {
446 try {
447 userTransaction.rollback();
448 } catch (Exception e1) {
449 log.error("Could not roll back", e1);
450 }
451 if (e instanceof RuntimeException)
452 throw (RuntimeException) e;
453 else
454 throw new RuntimeException("Cannot change password", e);
455 }
456 }
457
458 public String addSharedSecret(String email, int hours) {
459 User user = (User) userAdmin.getUser(LdapAttrs.mail.name(), email);
460 try {
461 userTransaction.begin();
462 String uuid = UUID.randomUUID().toString();
463 SharedSecret sharedSecret = new SharedSecret(hours, uuid);
464 user.getCredentials().put(SharedSecret.X_SHARED_SECRET, sharedSecret.toAuthPassword());
465 String tokenStr = sharedSecret.getAuthInfo() + '$' + sharedSecret.getAuthValue();
466 userTransaction.commit();
467 return tokenStr;
468 } catch (Exception e) {
469 try {
470 userTransaction.rollback();
471 } catch (Exception e1) {
472 log.error("Could not roll back", e1);
473 }
474 if (e instanceof RuntimeException)
475 throw (RuntimeException) e;
476 else
477 throw new RuntimeException("Cannot change password", e);
478 }
479 }
480
481 @Deprecated
482 public String addSharedSecret(String username, String authInfo, String authToken) {
483 try {
484 userTransaction.begin();
485 User user = (User) userAdmin.getRole(username);
486 SharedSecret sharedSecret = new SharedSecret(authInfo, authToken);
487 user.getCredentials().put(SharedSecret.X_SHARED_SECRET, sharedSecret.toAuthPassword());
488 String tokenStr = sharedSecret.getAuthInfo() + '$' + sharedSecret.getAuthValue();
489 userTransaction.commit();
490 return tokenStr;
491 } catch (Exception e1) {
492 try {
493 if (!userTransaction.isNoTransactionStatus())
494 userTransaction.rollback();
495 } catch (Exception e2) {
496 if (log.isTraceEnabled())
497 log.trace("Cannot rollback transaction", e2);
498 }
499 throw new RuntimeException("Cannot add shared secret", e1);
500 }
501 }
502
503 @Override
504 public void expireAuthToken(String token) {
505 try {
506 userTransaction.begin();
507 String dn = cn + "=" + token + "," + CmsConstants.TOKENS_BASEDN;
508 Group tokenGroup = (Group) userAdmin.getRole(dn);
509 String ldapDate = NamingUtils.instantToLdapDate(ZonedDateTime.now(ZoneOffset.UTC));
510 tokenGroup.getProperties().put(description.name(), ldapDate);
511 userTransaction.commit();
512 if (log.isDebugEnabled())
513 log.debug("Token " + token + " expired.");
514 } catch (Exception e1) {
515 try {
516 if (!userTransaction.isNoTransactionStatus())
517 userTransaction.rollback();
518 } catch (Exception e2) {
519 if (log.isTraceEnabled())
520 log.trace("Cannot rollback transaction", e2);
521 }
522 throw new RuntimeException("Cannot expire token", e1);
523 }
524 }
525
526 @Override
527 public void expireAuthTokens(Subject subject) {
528 Set<String> tokens = TokenUtils.tokensUsed(subject, CmsConstants.TOKENS_BASEDN);
529 for (String token : tokens)
530 expireAuthToken(token);
531 }
532
533 @Override
534 public void addAuthToken(String userDn, String token, Integer hours, String... roles) {
535 addAuthToken(userDn, token, ZonedDateTime.now().plusHours(hours), roles);
536 }
537
538 @Override
539 public void addAuthToken(String userDn, String token, ZonedDateTime expiryDate, String... roles) {
540 try {
541 userTransaction.begin();
542 User user = (User) userAdmin.getRole(userDn);
543 String tokenDn = cn + "=" + token + "," + CmsConstants.TOKENS_BASEDN;
544 Group tokenGroup = (Group) userAdmin.createRole(tokenDn, Role.GROUP);
545 if (roles != null)
546 for (String role : roles) {
547 Role r = userAdmin.getRole(role);
548 if (r != null)
549 tokenGroup.addMember(r);
550 else {
551 if (!role.equals(CmsConstants.ROLE_USER)) {
552 throw new IllegalStateException(
553 "Cannot add role " + role + " to token " + token + " for " + userDn);
554 }
555 }
556 }
557 tokenGroup.getProperties().put(owner.name(), user.getName());
558 if (expiryDate != null) {
559 String ldapDate = NamingUtils.instantToLdapDate(expiryDate);
560 tokenGroup.getProperties().put(description.name(), ldapDate);
561 }
562 userTransaction.commit();
563 } catch (Exception e1) {
564 try {
565 if (!userTransaction.isNoTransactionStatus())
566 userTransaction.rollback();
567 } catch (Exception e2) {
568 if (log.isTraceEnabled())
569 log.trace("Cannot rollback transaction", e2);
570 }
571 throw new RuntimeException("Cannot add token", e1);
572 }
573 }
574
575 @Override
576 public UserDirectory getDirectory(Role user) {
577 String name = user.getName();
578 NavigableMap<String, UserDirectory> possible = new TreeMap<>();
579 for (UserDirectory userDirectory : userDirectories) {
580 if (name.endsWith(userDirectory.getBase())) {
581 possible.put(userDirectory.getBase(), userDirectory);
582 }
583 }
584 if (possible.size() == 0)
585 throw new IllegalStateException("No user directory found for user " + name);
586 return possible.lastEntry().getValue();
587 }
588
589 // public User createUserFromPerson(Node person) {
590 // String email = JcrUtils.get(person, LdapAttrs.mail.property());
591 // String dn = buildDefaultDN(email, Role.USER);
592 // User user;
593 // try {
594 // userTransaction.begin();
595 // user = (User) userAdmin.createRole(dn, Role.USER);
596 // Dictionary<String, Object> userProperties = user.getProperties();
597 // String name = JcrUtils.get(person, LdapAttrs.displayName.property());
598 // userProperties.put(LdapAttrs.cn.name(), name);
599 // userProperties.put(LdapAttrs.displayName.name(), name);
600 // String givenName = JcrUtils.get(person, LdapAttrs.givenName.property());
601 // String surname = JcrUtils.get(person, LdapAttrs.sn.property());
602 // userProperties.put(LdapAttrs.givenName.name(), givenName);
603 // userProperties.put(LdapAttrs.sn.name(), surname);
604 // userProperties.put(LdapAttrs.mail.name(), email.toLowerCase());
605 // userTransaction.commit();
606 // } catch (Exception e) {
607 // try {
608 // userTransaction.rollback();
609 // } catch (Exception e1) {
610 // log.error("Could not roll back", e1);
611 // }
612 // if (e instanceof RuntimeException)
613 // throw (RuntimeException) e;
614 // else
615 // throw new RuntimeException("Cannot create user", e);
616 // }
617 // return user;
618 // }
619
620 public UserAdmin getUserAdmin() {
621 return userAdmin;
622 }
623
624 // public UserTransaction getUserTransaction() {
625 // return userTransaction;
626 // }
627
628 /* DEPENDENCY INJECTION */
629 public void setUserAdmin(UserAdmin userAdmin) {
630 this.userAdmin = userAdmin;
631
632 if (userAdmin instanceof AggregatingUserAdmin) {
633 userDirectories = ((AggregatingUserAdmin) userAdmin).getUserDirectories();
634 } else {
635 throw new IllegalArgumentException("Only " + AggregatingUserAdmin.class.getName() + " is supported.");
636 }
637
638 // this.serviceProperties = serviceProperties;
639 }
640
641 public void setUserTransaction(WorkTransaction userTransaction) {
642 this.userTransaction = userTransaction;
643 }
644
645 // public void addUserDirectory(UserDirectory userDirectory, Map<String, Object> properties) {
646 // userDirectories.put(userDirectory, new Hashtable<>(properties));
647 // }
648 //
649 // public void removeUserDirectory(UserDirectory userDirectory, Map<String, Object> properties) {
650 // userDirectories.remove(userDirectory);
651 // }
652
653 }