]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/internal/auth/CmsUserManagerImpl.java
a8a322356f45a52893a25af3c3c6c02be107d7d9
[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, or <code>null</code> if it doesn't exist. */
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 // @Override
144 // public Set<User> listAccounts(HierarchyUnit hierarchyUnit, boolean deep) {
145 // if(!hierarchyUnit.isFunctional())
146 // throw new IllegalArgumentException("Hierarchy unit "+hierarchyUnit.getBase()+" is not functional");
147 // UserDirectory directory = (UserDirectory)hierarchyUnit.getDirectory();
148 // Set<User> res = new HashSet<>();
149 // for(HierarchyUnit technicalHu:hierarchyUnit.getDirectHierarchyUnits(false)) {
150 // if(technicalHu.isFunctional())
151 // continue;
152 // for(Role role:directory.getHierarchyUnitRoles(technicalHu, null, false)) {
153 // if(role)
154 // }
155 // }
156 // return res;
157 // }
158
159 /** Recursively add users to list */
160 private void addUsers(Set<User> users, Group group, String filter) {
161 Role[] roles = group.getMembers();
162 for (Role role : roles) {
163 if (role.getType() == Role.GROUP) {
164 addUsers(users, (Group) role, filter);
165 } else if (role.getType() == Role.USER) {
166 if (match(role, filter))
167 users.add((User) role);
168 } else {
169 // ignore
170 }
171 }
172 }
173
174 public List<User> listGroups(String filter, boolean includeUsers, boolean includeSystemRoles) {
175 Role[] roles = null;
176 try {
177 roles = getUserAdmin().getRoles(filter);
178 } catch (InvalidSyntaxException e) {
179 throw new IllegalArgumentException("Unable to get roles with filter: " + filter, e);
180 }
181
182 List<User> users = new ArrayList<User>();
183 for (Role role : roles) {
184 if ((includeUsers && role.getType() == Role.USER || role.getType() == Role.GROUP) && !users.contains(role)
185 && (includeSystemRoles
186 || !role.getName().toLowerCase().endsWith(CmsConstants.SYSTEM_ROLES_BASEDN))) {
187 if (match(role, filter))
188 users.add((User) role);
189 }
190 }
191 return users;
192 }
193
194 private boolean match(Role role, String filter) {
195 boolean doFilter = filter != null && !"".equals(filter);
196 if (doFilter) {
197 for (String prop : knownProps) {
198 Object currProp = null;
199 try {
200 currProp = role.getProperties().get(prop);
201 } catch (Exception e) {
202 throw e;
203 }
204 if (currProp != null) {
205 String currPropStr = ((String) currProp).toLowerCase();
206 if (currPropStr.contains(filter.toLowerCase())) {
207 return true;
208 }
209 }
210 }
211 return false;
212 } else
213 return true;
214 }
215
216 @Override
217 public User getUserFromLocalId(String localId) {
218 User user = getUserAdmin().getUser(LdapAttrs.uid.name(), localId);
219 if (user == null)
220 user = getUserAdmin().getUser(LdapAttrs.cn.name(), localId);
221 return user;
222 }
223
224 @Override
225 public String buildDefaultDN(String localId, int type) {
226 return buildDistinguishedName(localId, getDefaultDomainName(), type);
227 }
228
229 /*
230 * EDITION
231 */
232 @Override
233 public User createUser(String username, Map<String, Object> properties, Map<String, Object> credentials) {
234 try {
235 userTransaction.begin();
236 User user = (User) userAdmin.createRole(username, Role.USER);
237 if (properties != null) {
238 for (String key : properties.keySet())
239 user.getProperties().put(key, properties.get(key));
240 }
241 if (credentials != null) {
242 for (String key : credentials.keySet())
243 user.getCredentials().put(key, credentials.get(key));
244 }
245 userTransaction.commit();
246 return user;
247 } catch (Exception e) {
248 try {
249 userTransaction.rollback();
250 } catch (Exception e1) {
251 log.error("Could not roll back", e1);
252 }
253 if (e instanceof RuntimeException)
254 throw (RuntimeException) e;
255 else
256 throw new RuntimeException("Cannot create user " + username, e);
257 }
258 }
259
260 @Override
261 public Group getOrCreateGroup(HierarchyUnit groups, String commonName) {
262 try {
263 String dn = LdapAttrs.cn.name() + "=" + commonName + "," + groups.getBase();
264 Group group = (Group) getUserAdmin().getRole(dn);
265 if (group != null)
266 return group;
267 userTransaction.begin();
268 group = (Group) userAdmin.createRole(dn, Role.GROUP);
269 userTransaction.commit();
270 return group;
271 } catch (Exception e) {
272 try {
273 userTransaction.rollback();
274 } catch (Exception e1) {
275 log.error("Could not roll back", e1);
276 }
277 if (e instanceof RuntimeException)
278 throw (RuntimeException) e;
279 else
280 throw new RuntimeException("Cannot create group " + commonName + " in " + groups, e);
281 }
282 }
283
284 @Override
285 public Group getOrCreateSystemRole(HierarchyUnit roles, SystemRole systemRole) {
286 try {
287 String dn = LdapAttrs.cn.name() + "=" + NamespaceUtils.toPrefixedName(systemRole.getName()) + ","
288 + roles.getBase();
289 Group group = (Group) getUserAdmin().getRole(dn);
290 if (group != null)
291 return group;
292 userTransaction.begin();
293 group = (Group) userAdmin.createRole(dn, Role.GROUP);
294 userTransaction.commit();
295 return group;
296 } catch (Exception e) {
297 try {
298 userTransaction.rollback();
299 } catch (Exception e1) {
300 log.error("Could not roll back", e1);
301 }
302 if (e instanceof RuntimeException)
303 throw (RuntimeException) e;
304 else
305 throw new RuntimeException("Cannot create system role " + systemRole + " in " + roles, e);
306 }
307 }
308
309 @Override
310 public HierarchyUnit getOrCreateHierarchyUnit(UserDirectory directory, String path) {
311 HierarchyUnit hi = directory.getHierarchyUnit(path);
312 if (hi != null)
313 return hi;
314 try {
315 userTransaction.begin();
316 HierarchyUnit hierarchyUnit = directory.createHierarchyUnit(path);
317 userTransaction.commit();
318 return hierarchyUnit;
319 } catch (Exception e1) {
320 try {
321 if (!userTransaction.isNoTransactionStatus())
322 userTransaction.rollback();
323 } catch (Exception e2) {
324 if (log.isTraceEnabled())
325 log.trace("Cannot rollback transaction", e2);
326 }
327 throw new RuntimeException("Cannot create hierarchy unit " + path + " in directory " + directory, e1);
328 }
329 }
330
331 @Override
332 public void addObjectClasses(Role role, Set<String> objectClasses, Map<String, Object> additionalProperties) {
333 try {
334 userTransaction.begin();
335 LdapEntry.addObjectClasses(role.getProperties(), objectClasses);
336 for (String key : additionalProperties.keySet()) {
337 role.getProperties().put(key, additionalProperties.get(key));
338 }
339 userTransaction.commit();
340 } catch (Exception e1) {
341 try {
342 if (!userTransaction.isNoTransactionStatus())
343 userTransaction.rollback();
344 } catch (Exception e2) {
345 if (log.isTraceEnabled())
346 log.trace("Cannot rollback transaction", e2);
347 }
348 throw new RuntimeException("Cannot add object classes " + objectClasses + " to " + role, e1);
349 }
350 }
351
352 @Override
353 public void addMember(Group group, Role role) {
354 try {
355 userTransaction.begin();
356 group.addMember(role);
357 userTransaction.commit();
358 } catch (Exception e1) {
359 try {
360 if (!userTransaction.isNoTransactionStatus())
361 userTransaction.rollback();
362 } catch (Exception e2) {
363 if (log.isTraceEnabled())
364 log.trace("Cannot rollback transaction", e2);
365 }
366 throw new RuntimeException("Cannot add object classes " + role + " to group " + group, e1);
367 }
368 }
369
370 @Override
371 public String getDefaultDomainName() {
372 Map<String, String> dns = getKnownBaseDns(true);
373 if (dns.size() == 1)
374 return dns.keySet().iterator().next();
375 else
376 throw new IllegalStateException("Current context contains " + dns.size() + " base dns: "
377 + dns.keySet().toString() + ". Unable to chose a default one.");
378 }
379
380 public Map<String, String> getKnownBaseDns(boolean onlyWritable) {
381 Map<String, String> dns = new HashMap<String, String>();
382 for (UserDirectory userDirectory : userDirectories) {
383 Boolean readOnly = userDirectory.isReadOnly();
384 String baseDn = userDirectory.getBase();
385
386 if (onlyWritable && readOnly)
387 continue;
388 if (baseDn.equalsIgnoreCase(CmsConstants.SYSTEM_ROLES_BASEDN))
389 continue;
390 if (baseDn.equalsIgnoreCase(CmsConstants.TOKENS_BASEDN))
391 continue;
392 dns.put(baseDn, DirectoryConf.propertiesAsUri(userDirectory.getProperties()).toString());
393
394 }
395 return dns;
396 }
397
398 public Set<UserDirectory> getUserDirectories() {
399 TreeSet<UserDirectory> res = new TreeSet<>((o1, o2) -> o1.getBase().compareTo(o2.getBase()));
400 res.addAll(userDirectories);
401 return res;
402 }
403
404 public String buildDistinguishedName(String localId, String baseDn, int type) {
405 Map<String, String> dns = getKnownBaseDns(true);
406 Dictionary<String, ?> props = DirectoryConf.uriAsProperties(dns.get(baseDn));
407 String dn = null;
408 if (Role.GROUP == type)
409 dn = LdapAttrs.cn.name() + "=" + localId + "," + DirectoryConf.groupBase.getValue(props) + "," + baseDn;
410 else if (Role.USER == type)
411 dn = LdapAttrs.uid.name() + "=" + localId + "," + DirectoryConf.userBase.getValue(props) + "," + baseDn;
412 else
413 throw new IllegalStateException("Unknown role type. " + "Cannot deduce dn for " + localId);
414 return dn;
415 }
416
417 @Override
418 public void changeOwnPassword(char[] oldPassword, char[] newPassword) {
419 String name = CurrentUser.getUsername();
420 LdapName dn;
421 try {
422 dn = new LdapName(name);
423 } catch (InvalidNameException e) {
424 throw new IllegalArgumentException("Invalid user dn " + name, e);
425 }
426 User user = (User) userAdmin.getRole(dn.toString());
427 if (!user.hasCredential(null, oldPassword))
428 throw new IllegalArgumentException("Invalid password");
429 if (Arrays.equals(newPassword, new char[0]))
430 throw new IllegalArgumentException("New password empty");
431 try {
432 userTransaction.begin();
433 user.getCredentials().put(null, newPassword);
434 userTransaction.commit();
435 } catch (Exception e) {
436 try {
437 userTransaction.rollback();
438 } catch (Exception e1) {
439 log.error("Could not roll back", e1);
440 }
441 if (e instanceof RuntimeException)
442 throw (RuntimeException) e;
443 else
444 throw new RuntimeException("Cannot change password", e);
445 }
446 }
447
448 public void resetPassword(String username, char[] newPassword) {
449 LdapName dn;
450 try {
451 dn = new LdapName(username);
452 } catch (InvalidNameException e) {
453 throw new IllegalArgumentException("Invalid user dn " + username, e);
454 }
455 User user = (User) userAdmin.getRole(dn.toString());
456 if (Arrays.equals(newPassword, new char[0]))
457 throw new IllegalArgumentException("New password empty");
458 try {
459 userTransaction.begin();
460 user.getCredentials().put(null, newPassword);
461 userTransaction.commit();
462 } catch (Exception e) {
463 try {
464 userTransaction.rollback();
465 } catch (Exception e1) {
466 log.error("Could not roll back", e1);
467 }
468 if (e instanceof RuntimeException)
469 throw (RuntimeException) e;
470 else
471 throw new RuntimeException("Cannot change password", e);
472 }
473 }
474
475 public String addSharedSecret(String email, int hours) {
476 User user = (User) userAdmin.getUser(LdapAttrs.mail.name(), email);
477 try {
478 userTransaction.begin();
479 String uuid = UUID.randomUUID().toString();
480 SharedSecret sharedSecret = new SharedSecret(hours, uuid);
481 user.getCredentials().put(SharedSecret.X_SHARED_SECRET, sharedSecret.toAuthPassword());
482 String tokenStr = sharedSecret.getAuthInfo() + '$' + sharedSecret.getAuthValue();
483 userTransaction.commit();
484 return tokenStr;
485 } catch (Exception e) {
486 try {
487 userTransaction.rollback();
488 } catch (Exception e1) {
489 log.error("Could not roll back", e1);
490 }
491 if (e instanceof RuntimeException)
492 throw (RuntimeException) e;
493 else
494 throw new RuntimeException("Cannot change password", e);
495 }
496 }
497
498 @Deprecated
499 public String addSharedSecret(String username, String authInfo, String authToken) {
500 try {
501 userTransaction.begin();
502 User user = (User) userAdmin.getRole(username);
503 SharedSecret sharedSecret = new SharedSecret(authInfo, authToken);
504 user.getCredentials().put(SharedSecret.X_SHARED_SECRET, sharedSecret.toAuthPassword());
505 String tokenStr = sharedSecret.getAuthInfo() + '$' + sharedSecret.getAuthValue();
506 userTransaction.commit();
507 return tokenStr;
508 } catch (Exception e1) {
509 try {
510 if (!userTransaction.isNoTransactionStatus())
511 userTransaction.rollback();
512 } catch (Exception e2) {
513 if (log.isTraceEnabled())
514 log.trace("Cannot rollback transaction", e2);
515 }
516 throw new RuntimeException("Cannot add shared secret", e1);
517 }
518 }
519
520 @Override
521 public void expireAuthToken(String token) {
522 try {
523 userTransaction.begin();
524 String dn = cn + "=" + token + "," + CmsConstants.TOKENS_BASEDN;
525 Group tokenGroup = (Group) userAdmin.getRole(dn);
526 String ldapDate = NamingUtils.instantToLdapDate(ZonedDateTime.now(ZoneOffset.UTC));
527 tokenGroup.getProperties().put(description.name(), ldapDate);
528 userTransaction.commit();
529 if (log.isDebugEnabled())
530 log.debug("Token " + token + " expired.");
531 } catch (Exception e1) {
532 try {
533 if (!userTransaction.isNoTransactionStatus())
534 userTransaction.rollback();
535 } catch (Exception e2) {
536 if (log.isTraceEnabled())
537 log.trace("Cannot rollback transaction", e2);
538 }
539 throw new RuntimeException("Cannot expire token", e1);
540 }
541 }
542
543 @Override
544 public void expireAuthTokens(Subject subject) {
545 Set<String> tokens = TokenUtils.tokensUsed(subject, CmsConstants.TOKENS_BASEDN);
546 for (String token : tokens)
547 expireAuthToken(token);
548 }
549
550 @Override
551 public void addAuthToken(String userDn, String token, Integer hours, String... roles) {
552 addAuthToken(userDn, token, ZonedDateTime.now().plusHours(hours), roles);
553 }
554
555 @Override
556 public void addAuthToken(String userDn, String token, ZonedDateTime expiryDate, String... roles) {
557 try {
558 userTransaction.begin();
559 User user = (User) userAdmin.getRole(userDn);
560 String tokenDn = cn + "=" + token + "," + CmsConstants.TOKENS_BASEDN;
561 Group tokenGroup = (Group) userAdmin.createRole(tokenDn, Role.GROUP);
562 if (roles != null)
563 for (String role : roles) {
564 Role r = userAdmin.getRole(role);
565 if (r != null)
566 tokenGroup.addMember(r);
567 else {
568 if (!role.equals(CmsConstants.ROLE_USER)) {
569 throw new IllegalStateException(
570 "Cannot add role " + role + " to token " + token + " for " + userDn);
571 }
572 }
573 }
574 tokenGroup.getProperties().put(owner.name(), user.getName());
575 if (expiryDate != null) {
576 String ldapDate = NamingUtils.instantToLdapDate(expiryDate);
577 tokenGroup.getProperties().put(description.name(), ldapDate);
578 }
579 userTransaction.commit();
580 } catch (Exception e1) {
581 try {
582 if (!userTransaction.isNoTransactionStatus())
583 userTransaction.rollback();
584 } catch (Exception e2) {
585 if (log.isTraceEnabled())
586 log.trace("Cannot rollback transaction", e2);
587 }
588 throw new RuntimeException("Cannot add token", e1);
589 }
590 }
591
592 @Override
593 public UserDirectory getDirectory(Role user) {
594 String name = user.getName();
595 NavigableMap<String, UserDirectory> possible = new TreeMap<>();
596 for (UserDirectory userDirectory : userDirectories) {
597 if (name.endsWith(userDirectory.getBase())) {
598 possible.put(userDirectory.getBase(), userDirectory);
599 }
600 }
601 if (possible.size() == 0)
602 throw new IllegalStateException("No user directory found for user " + name);
603 return possible.lastEntry().getValue();
604 }
605
606 // public User createUserFromPerson(Node person) {
607 // String email = JcrUtils.get(person, LdapAttrs.mail.property());
608 // String dn = buildDefaultDN(email, Role.USER);
609 // User user;
610 // try {
611 // userTransaction.begin();
612 // user = (User) userAdmin.createRole(dn, Role.USER);
613 // Dictionary<String, Object> userProperties = user.getProperties();
614 // String name = JcrUtils.get(person, LdapAttrs.displayName.property());
615 // userProperties.put(LdapAttrs.cn.name(), name);
616 // userProperties.put(LdapAttrs.displayName.name(), name);
617 // String givenName = JcrUtils.get(person, LdapAttrs.givenName.property());
618 // String surname = JcrUtils.get(person, LdapAttrs.sn.property());
619 // userProperties.put(LdapAttrs.givenName.name(), givenName);
620 // userProperties.put(LdapAttrs.sn.name(), surname);
621 // userProperties.put(LdapAttrs.mail.name(), email.toLowerCase());
622 // userTransaction.commit();
623 // } catch (Exception e) {
624 // try {
625 // userTransaction.rollback();
626 // } catch (Exception e1) {
627 // log.error("Could not roll back", e1);
628 // }
629 // if (e instanceof RuntimeException)
630 // throw (RuntimeException) e;
631 // else
632 // throw new RuntimeException("Cannot create user", e);
633 // }
634 // return user;
635 // }
636
637 public UserAdmin getUserAdmin() {
638 return userAdmin;
639 }
640
641 // public UserTransaction getUserTransaction() {
642 // return userTransaction;
643 // }
644
645 /* DEPENDENCY INJECTION */
646 public void setUserAdmin(UserAdmin userAdmin) {
647 this.userAdmin = userAdmin;
648
649 if (userAdmin instanceof AggregatingUserAdmin) {
650 userDirectories = ((AggregatingUserAdmin) userAdmin).getUserDirectories();
651 } else {
652 throw new IllegalArgumentException("Only " + AggregatingUserAdmin.class.getName() + " is supported.");
653 }
654
655 // this.serviceProperties = serviceProperties;
656 }
657
658 public void setUserTransaction(WorkTransaction userTransaction) {
659 this.userTransaction = userTransaction;
660 }
661
662 // public void addUserDirectory(UserDirectory userDirectory, Map<String, Object> properties) {
663 // userDirectories.put(userDirectory, new Hashtable<>(properties));
664 // }
665 //
666 // public void removeUserDirectory(UserDirectory userDirectory, Map<String, Object> properties) {
667 // userDirectories.remove(userDirectory);
668 // }
669
670 }