1 package org
.argeo
.cms
.internal
.auth
;
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
;
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
;
16 import java
.util
.NavigableMap
;
17 import java
.util
.Objects
;
19 import java
.util
.TreeMap
;
20 import java
.util
.TreeSet
;
21 import java
.util
.UUID
;
23 import javax
.naming
.InvalidNameException
;
24 import javax
.naming
.ldap
.LdapName
;
25 import javax
.security
.auth
.Subject
;
27 import org
.argeo
.api
.acr
.NamespaceUtils
;
28 import org
.argeo
.api
.cms
.CmsConstants
;
29 import org
.argeo
.api
.cms
.CmsLog
;
30 import org
.argeo
.cms
.CmsUserManager
;
31 import org
.argeo
.cms
.auth
.CurrentUser
;
32 import org
.argeo
.cms
.auth
.SystemRole
;
33 import org
.argeo
.cms
.auth
.UserAdminUtils
;
34 import org
.argeo
.osgi
.useradmin
.AggregatingUserAdmin
;
35 import org
.argeo
.osgi
.useradmin
.TokenUtils
;
36 import org
.argeo
.osgi
.useradmin
.UserDirectory
;
37 import org
.argeo
.util
.directory
.DirectoryConf
;
38 import org
.argeo
.util
.directory
.HierarchyUnit
;
39 import org
.argeo
.util
.directory
.ldap
.LdapEntry
;
40 import org
.argeo
.util
.directory
.ldap
.SharedSecret
;
41 import org
.argeo
.util
.naming
.LdapAttrs
;
42 import org
.argeo
.util
.naming
.NamingUtils
;
43 import org
.argeo
.util
.transaction
.WorkTransaction
;
44 import org
.osgi
.framework
.InvalidSyntaxException
;
45 import org
.osgi
.service
.useradmin
.Authorization
;
46 import org
.osgi
.service
.useradmin
.Group
;
47 import org
.osgi
.service
.useradmin
.Role
;
48 import org
.osgi
.service
.useradmin
.User
;
49 import org
.osgi
.service
.useradmin
.UserAdmin
;
52 * Canonical implementation of the people {@link CmsUserManager}. Wraps
53 * interaction with users and groups.
55 * In a *READ-ONLY* mode. We want to be able to:
57 * <li>Retrieve my user and corresponding information (main info,
59 * <li>List all local groups (not the system roles)</li>
60 * <li>If sufficient rights: retrieve a given user and its information</li>
63 public class CmsUserManagerImpl
implements CmsUserManager
{
64 private final static CmsLog log
= CmsLog
.getLog(CmsUserManagerImpl
.class);
66 private UserAdmin userAdmin
;
67 // private Map<String, String> serviceProperties;
68 private WorkTransaction userTransaction
;
70 private final String
[] knownProps
= { LdapAttrs
.cn
.name(), LdapAttrs
.sn
.name(), LdapAttrs
.givenName
.name(),
71 LdapAttrs
.uid
.name() };
73 // private Map<UserDirectory, Hashtable<String, Object>> userDirectories = Collections
74 // .synchronizedMap(new LinkedHashMap<>());
76 private Set
<UserDirectory
> userDirectories
= new HashSet
<>();
79 log
.debug(() -> "CMS user manager available");
87 public String
getMyMail() {
88 return getUserMail(CurrentUser
.getUsername());
92 public Role
[] getRoles(String filter
) throws InvalidSyntaxException
{
93 return userAdmin
.getRoles(filter
);
96 // ALL USER: WARNING access to this will be later reduced
98 /** Retrieve a user given his dn, or <code>null</code> if it doesn't exist. */
99 public User
getUser(String dn
) {
100 return (User
) getUserAdmin().getRole(dn
);
103 /** Can be a group or a user */
104 public String
getUserDisplayName(String dn
) {
105 // FIXME: during initialisation phase, the system logs "admin" as user
106 // name rather than the corresponding dn
107 if ("admin".equals(dn
))
108 return "System Administrator";
110 return UserAdminUtils
.getUserDisplayName(getUserAdmin(), dn
);
114 public String
getUserMail(String dn
) {
115 return UserAdminUtils
.getUserMail(getUserAdmin(), dn
);
118 /** Lists all roles of the given user */
120 public String
[] getUserRoles(String dn
) {
121 Authorization currAuth
= getUserAdmin().getAuthorization(getUser(dn
));
122 return currAuth
.getRoles();
126 public boolean isUserInRole(String userDn
, String roleDn
) {
127 String
[] roles
= getUserRoles(userDn
);
128 for (String role
: roles
) {
129 if (role
.equalsIgnoreCase(roleDn
))
135 public Set
<User
> listUsersInGroup(String groupDn
, String filter
) {
136 Group group
= (Group
) userAdmin
.getRole(groupDn
);
138 throw new IllegalArgumentException("Group " + groupDn
+ " not found");
139 Set
<User
> users
= new HashSet
<User
>();
140 addUsers(users
, group
, filter
);
145 // public Set<User> listAccounts(HierarchyUnit hierarchyUnit, boolean deep) {
146 // if(!hierarchyUnit.isFunctional())
147 // throw new IllegalArgumentException("Hierarchy unit "+hierarchyUnit.getBase()+" is not functional");
148 // UserDirectory directory = (UserDirectory)hierarchyUnit.getDirectory();
149 // Set<User> res = new HashSet<>();
150 // for(HierarchyUnit technicalHu:hierarchyUnit.getDirectHierarchyUnits(false)) {
151 // if(technicalHu.isFunctional())
153 // for(Role role:directory.getHierarchyUnitRoles(technicalHu, null, false)) {
160 /** Recursively add users to list */
161 private void addUsers(Set
<User
> users
, Group group
, String filter
) {
162 Role
[] roles
= group
.getMembers();
163 for (Role role
: roles
) {
164 if (role
.getType() == Role
.GROUP
) {
165 addUsers(users
, (Group
) role
, filter
);
166 } else if (role
.getType() == Role
.USER
) {
167 if (match(role
, filter
))
168 users
.add((User
) role
);
175 public List
<User
> listGroups(String filter
, boolean includeUsers
, boolean includeSystemRoles
) {
178 roles
= getUserAdmin().getRoles(filter
);
179 } catch (InvalidSyntaxException e
) {
180 throw new IllegalArgumentException("Unable to get roles with filter: " + filter
, e
);
183 List
<User
> users
= new ArrayList
<User
>();
184 for (Role role
: roles
) {
185 if ((includeUsers
&& role
.getType() == Role
.USER
|| role
.getType() == Role
.GROUP
) && !users
.contains(role
)
186 && (includeSystemRoles
187 || !role
.getName().toLowerCase().endsWith(CmsConstants
.SYSTEM_ROLES_BASEDN
))) {
188 if (match(role
, filter
))
189 users
.add((User
) role
);
195 private boolean match(Role role
, String filter
) {
196 boolean doFilter
= filter
!= null && !"".equals(filter
);
198 for (String prop
: knownProps
) {
199 Object currProp
= null;
201 currProp
= role
.getProperties().get(prop
);
202 } catch (Exception e
) {
205 if (currProp
!= null) {
206 String currPropStr
= ((String
) currProp
).toLowerCase();
207 if (currPropStr
.contains(filter
.toLowerCase())) {
218 public User
getUserFromLocalId(String localId
) {
219 User user
= getUserAdmin().getUser(LdapAttrs
.uid
.name(), localId
);
221 user
= getUserAdmin().getUser(LdapAttrs
.cn
.name(), localId
);
226 public String
buildDefaultDN(String localId
, int type
) {
227 return buildDistinguishedName(localId
, getDefaultDomainName(), type
);
234 public User
createUser(String username
, Map
<String
, Object
> properties
, Map
<String
, Object
> credentials
) {
236 userTransaction
.begin();
237 User user
= (User
) userAdmin
.createRole(username
, Role
.USER
);
238 if (properties
!= null) {
239 for (String key
: properties
.keySet())
240 user
.getProperties().put(key
, properties
.get(key
));
242 if (credentials
!= null) {
243 for (String key
: credentials
.keySet())
244 user
.getCredentials().put(key
, credentials
.get(key
));
246 userTransaction
.commit();
248 } catch (Exception e
) {
250 userTransaction
.rollback();
251 } catch (Exception e1
) {
252 log
.error("Could not roll back", e1
);
254 if (e
instanceof RuntimeException
)
255 throw (RuntimeException
) e
;
257 throw new RuntimeException("Cannot create user " + username
, e
);
262 public Group
getOrCreateGroup(HierarchyUnit groups
, String commonName
) {
264 String dn
= LdapAttrs
.cn
.name() + "=" + commonName
+ "," + groups
.getBase();
265 Group group
= (Group
) getUserAdmin().getRole(dn
);
268 userTransaction
.begin();
269 group
= (Group
) userAdmin
.createRole(dn
, Role
.GROUP
);
270 userTransaction
.commit();
272 } catch (Exception e
) {
274 userTransaction
.rollback();
275 } catch (Exception e1
) {
276 log
.error("Could not roll back", e1
);
278 if (e
instanceof RuntimeException
)
279 throw (RuntimeException
) e
;
281 throw new RuntimeException("Cannot create group " + commonName
+ " in " + groups
, e
);
286 public Group
getOrCreateSystemRole(HierarchyUnit roles
, SystemRole systemRole
) {
288 String dn
= LdapAttrs
.cn
.name() + "=" + NamespaceUtils
.toPrefixedName(systemRole
.getName()) + ","
290 Group group
= (Group
) getUserAdmin().getRole(dn
);
293 userTransaction
.begin();
294 group
= (Group
) userAdmin
.createRole(dn
, Role
.GROUP
);
295 userTransaction
.commit();
297 } catch (Exception e
) {
299 userTransaction
.rollback();
300 } catch (Exception e1
) {
301 log
.error("Could not roll back", e1
);
303 if (e
instanceof RuntimeException
)
304 throw (RuntimeException
) e
;
306 throw new RuntimeException("Cannot create system role " + systemRole
+ " in " + roles
, e
);
311 public HierarchyUnit
getOrCreateHierarchyUnit(UserDirectory directory
, String path
) {
312 HierarchyUnit hi
= directory
.getHierarchyUnit(path
);
316 userTransaction
.begin();
317 HierarchyUnit hierarchyUnit
= directory
.createHierarchyUnit(path
);
318 userTransaction
.commit();
319 return hierarchyUnit
;
320 } catch (Exception e1
) {
322 if (!userTransaction
.isNoTransactionStatus())
323 userTransaction
.rollback();
324 } catch (Exception e2
) {
325 if (log
.isTraceEnabled())
326 log
.trace("Cannot rollback transaction", e2
);
328 throw new RuntimeException("Cannot create hierarchy unit " + path
+ " in directory " + directory
, e1
);
333 public void addObjectClasses(Role role
, Set
<String
> objectClasses
, Map
<String
, Object
> additionalProperties
) {
335 userTransaction
.begin();
336 LdapEntry
.addObjectClasses(role
.getProperties(), objectClasses
);
337 for (String key
: additionalProperties
.keySet()) {
338 role
.getProperties().put(key
, additionalProperties
.get(key
));
340 userTransaction
.commit();
341 } catch (Exception e1
) {
343 if (!userTransaction
.isNoTransactionStatus())
344 userTransaction
.rollback();
345 } catch (Exception e2
) {
346 if (log
.isTraceEnabled())
347 log
.trace("Cannot rollback transaction", e2
);
349 throw new RuntimeException("Cannot add object classes " + objectClasses
+ " to " + role
, e1
);
354 public void addObjectClasses(HierarchyUnit hierarchyUnit
, Set
<String
> objectClasses
,
355 Map
<String
, Object
> additionalProperties
) {
357 userTransaction
.begin();
358 LdapEntry
.addObjectClasses(hierarchyUnit
.getProperties(), objectClasses
);
359 for (String key
: additionalProperties
.keySet()) {
360 hierarchyUnit
.getProperties().put(key
, additionalProperties
.get(key
));
362 userTransaction
.commit();
363 } catch (Exception e1
) {
365 if (!userTransaction
.isNoTransactionStatus())
366 userTransaction
.rollback();
367 } catch (Exception e2
) {
368 if (log
.isTraceEnabled())
369 log
.trace("Cannot rollback transaction", e2
);
371 throw new RuntimeException("Cannot add object classes " + objectClasses
+ " to " + hierarchyUnit
, e1
);
376 public void edit(Runnable action
) {
377 Objects
.requireNonNull(action
);
379 userTransaction
.begin();
381 userTransaction
.commit();
382 } catch (Exception e1
) {
384 if (!userTransaction
.isNoTransactionStatus())
385 userTransaction
.rollback();
386 } catch (Exception e2
) {
387 if (log
.isTraceEnabled())
388 log
.trace("Cannot rollback transaction", e2
);
390 throw new RuntimeException("Cannot edit", e1
);
395 public void addMember(Group group
, Role role
) {
397 userTransaction
.begin();
398 group
.addMember(role
);
399 userTransaction
.commit();
400 } catch (Exception e1
) {
402 if (!userTransaction
.isNoTransactionStatus())
403 userTransaction
.rollback();
404 } catch (Exception e2
) {
405 if (log
.isTraceEnabled())
406 log
.trace("Cannot rollback transaction", e2
);
408 throw new RuntimeException("Cannot add object classes " + role
+ " to group " + group
, e1
);
413 public String
getDefaultDomainName() {
414 Map
<String
, String
> dns
= getKnownBaseDns(true);
416 return dns
.keySet().iterator().next();
418 throw new IllegalStateException("Current context contains " + dns
.size() + " base dns: "
419 + dns
.keySet().toString() + ". Unable to chose a default one.");
422 public Map
<String
, String
> getKnownBaseDns(boolean onlyWritable
) {
423 Map
<String
, String
> dns
= new HashMap
<String
, String
>();
424 for (UserDirectory userDirectory
: userDirectories
) {
425 Boolean readOnly
= userDirectory
.isReadOnly();
426 String baseDn
= userDirectory
.getBase();
428 if (onlyWritable
&& readOnly
)
430 if (baseDn
.equalsIgnoreCase(CmsConstants
.SYSTEM_ROLES_BASEDN
))
432 if (baseDn
.equalsIgnoreCase(CmsConstants
.TOKENS_BASEDN
))
434 dns
.put(baseDn
, DirectoryConf
.propertiesAsUri(userDirectory
.getProperties()).toString());
440 public Set
<UserDirectory
> getUserDirectories() {
441 TreeSet
<UserDirectory
> res
= new TreeSet
<>((o1
, o2
) -> o1
.getBase().compareTo(o2
.getBase()));
442 res
.addAll(userDirectories
);
446 public String
buildDistinguishedName(String localId
, String baseDn
, int type
) {
447 Map
<String
, String
> dns
= getKnownBaseDns(true);
448 Dictionary
<String
, ?
> props
= DirectoryConf
.uriAsProperties(dns
.get(baseDn
));
450 if (Role
.GROUP
== type
)
451 dn
= LdapAttrs
.cn
.name() + "=" + localId
+ "," + DirectoryConf
.groupBase
.getValue(props
) + "," + baseDn
;
452 else if (Role
.USER
== type
)
453 dn
= LdapAttrs
.uid
.name() + "=" + localId
+ "," + DirectoryConf
.userBase
.getValue(props
) + "," + baseDn
;
455 throw new IllegalStateException("Unknown role type. " + "Cannot deduce dn for " + localId
);
460 public void changeOwnPassword(char[] oldPassword
, char[] newPassword
) {
461 String name
= CurrentUser
.getUsername();
464 dn
= new LdapName(name
);
465 } catch (InvalidNameException e
) {
466 throw new IllegalArgumentException("Invalid user dn " + name
, e
);
468 User user
= (User
) userAdmin
.getRole(dn
.toString());
469 if (!user
.hasCredential(null, oldPassword
))
470 throw new IllegalArgumentException("Invalid password");
471 if (Arrays
.equals(newPassword
, new char[0]))
472 throw new IllegalArgumentException("New password empty");
474 userTransaction
.begin();
475 user
.getCredentials().put(null, newPassword
);
476 userTransaction
.commit();
477 } catch (Exception e
) {
479 userTransaction
.rollback();
480 } catch (Exception e1
) {
481 log
.error("Could not roll back", e1
);
483 if (e
instanceof RuntimeException
)
484 throw (RuntimeException
) e
;
486 throw new RuntimeException("Cannot change password", e
);
490 public void resetPassword(String username
, char[] newPassword
) {
493 dn
= new LdapName(username
);
494 } catch (InvalidNameException e
) {
495 throw new IllegalArgumentException("Invalid user dn " + username
, e
);
497 User user
= (User
) userAdmin
.getRole(dn
.toString());
498 if (Arrays
.equals(newPassword
, new char[0]))
499 throw new IllegalArgumentException("New password empty");
501 userTransaction
.begin();
502 user
.getCredentials().put(null, newPassword
);
503 userTransaction
.commit();
504 } catch (Exception e
) {
506 userTransaction
.rollback();
507 } catch (Exception e1
) {
508 log
.error("Could not roll back", e1
);
510 if (e
instanceof RuntimeException
)
511 throw (RuntimeException
) e
;
513 throw new RuntimeException("Cannot change password", e
);
517 public String
addSharedSecret(String email
, int hours
) {
518 User user
= (User
) userAdmin
.getUser(LdapAttrs
.mail
.name(), email
);
520 userTransaction
.begin();
521 String uuid
= UUID
.randomUUID().toString();
522 SharedSecret sharedSecret
= new SharedSecret(hours
, uuid
);
523 user
.getCredentials().put(SharedSecret
.X_SHARED_SECRET
, sharedSecret
.toAuthPassword());
524 String tokenStr
= sharedSecret
.getAuthInfo() + '$' + sharedSecret
.getAuthValue();
525 userTransaction
.commit();
527 } catch (Exception e
) {
529 userTransaction
.rollback();
530 } catch (Exception e1
) {
531 log
.error("Could not roll back", e1
);
533 if (e
instanceof RuntimeException
)
534 throw (RuntimeException
) e
;
536 throw new RuntimeException("Cannot change password", e
);
541 public String
addSharedSecret(String username
, String authInfo
, String authToken
) {
543 userTransaction
.begin();
544 User user
= (User
) userAdmin
.getRole(username
);
545 SharedSecret sharedSecret
= new SharedSecret(authInfo
, authToken
);
546 user
.getCredentials().put(SharedSecret
.X_SHARED_SECRET
, sharedSecret
.toAuthPassword());
547 String tokenStr
= sharedSecret
.getAuthInfo() + '$' + sharedSecret
.getAuthValue();
548 userTransaction
.commit();
550 } catch (Exception e1
) {
552 if (!userTransaction
.isNoTransactionStatus())
553 userTransaction
.rollback();
554 } catch (Exception e2
) {
555 if (log
.isTraceEnabled())
556 log
.trace("Cannot rollback transaction", e2
);
558 throw new RuntimeException("Cannot add shared secret", e1
);
563 public void expireAuthToken(String token
) {
565 userTransaction
.begin();
566 String dn
= cn
+ "=" + token
+ "," + CmsConstants
.TOKENS_BASEDN
;
567 Group tokenGroup
= (Group
) userAdmin
.getRole(dn
);
568 String ldapDate
= NamingUtils
.instantToLdapDate(ZonedDateTime
.now(ZoneOffset
.UTC
));
569 tokenGroup
.getProperties().put(description
.name(), ldapDate
);
570 userTransaction
.commit();
571 if (log
.isDebugEnabled())
572 log
.debug("Token " + token
+ " expired.");
573 } catch (Exception e1
) {
575 if (!userTransaction
.isNoTransactionStatus())
576 userTransaction
.rollback();
577 } catch (Exception e2
) {
578 if (log
.isTraceEnabled())
579 log
.trace("Cannot rollback transaction", e2
);
581 throw new RuntimeException("Cannot expire token", e1
);
586 public void expireAuthTokens(Subject subject
) {
587 Set
<String
> tokens
= TokenUtils
.tokensUsed(subject
, CmsConstants
.TOKENS_BASEDN
);
588 for (String token
: tokens
)
589 expireAuthToken(token
);
593 public void addAuthToken(String userDn
, String token
, Integer hours
, String
... roles
) {
594 addAuthToken(userDn
, token
, ZonedDateTime
.now().plusHours(hours
), roles
);
598 public void addAuthToken(String userDn
, String token
, ZonedDateTime expiryDate
, String
... roles
) {
600 userTransaction
.begin();
601 User user
= (User
) userAdmin
.getRole(userDn
);
602 String tokenDn
= cn
+ "=" + token
+ "," + CmsConstants
.TOKENS_BASEDN
;
603 Group tokenGroup
= (Group
) userAdmin
.createRole(tokenDn
, Role
.GROUP
);
605 for (String role
: roles
) {
606 Role r
= userAdmin
.getRole(role
);
608 tokenGroup
.addMember(r
);
610 if (!role
.equals(CmsConstants
.ROLE_USER
)) {
611 throw new IllegalStateException(
612 "Cannot add role " + role
+ " to token " + token
+ " for " + userDn
);
616 tokenGroup
.getProperties().put(owner
.name(), user
.getName());
617 if (expiryDate
!= null) {
618 String ldapDate
= NamingUtils
.instantToLdapDate(expiryDate
);
619 tokenGroup
.getProperties().put(description
.name(), ldapDate
);
621 userTransaction
.commit();
622 } catch (Exception e1
) {
624 if (!userTransaction
.isNoTransactionStatus())
625 userTransaction
.rollback();
626 } catch (Exception e2
) {
627 if (log
.isTraceEnabled())
628 log
.trace("Cannot rollback transaction", e2
);
630 throw new RuntimeException("Cannot add token", e1
);
635 public UserDirectory
getDirectory(Role user
) {
636 String name
= user
.getName();
637 NavigableMap
<String
, UserDirectory
> possible
= new TreeMap
<>();
638 for (UserDirectory userDirectory
: userDirectories
) {
639 if (name
.endsWith(userDirectory
.getBase())) {
640 possible
.put(userDirectory
.getBase(), userDirectory
);
643 if (possible
.size() == 0)
644 throw new IllegalStateException("No user directory found for user " + name
);
645 return possible
.lastEntry().getValue();
648 // public User createUserFromPerson(Node person) {
649 // String email = JcrUtils.get(person, LdapAttrs.mail.property());
650 // String dn = buildDefaultDN(email, Role.USER);
653 // userTransaction.begin();
654 // user = (User) userAdmin.createRole(dn, Role.USER);
655 // Dictionary<String, Object> userProperties = user.getProperties();
656 // String name = JcrUtils.get(person, LdapAttrs.displayName.property());
657 // userProperties.put(LdapAttrs.cn.name(), name);
658 // userProperties.put(LdapAttrs.displayName.name(), name);
659 // String givenName = JcrUtils.get(person, LdapAttrs.givenName.property());
660 // String surname = JcrUtils.get(person, LdapAttrs.sn.property());
661 // userProperties.put(LdapAttrs.givenName.name(), givenName);
662 // userProperties.put(LdapAttrs.sn.name(), surname);
663 // userProperties.put(LdapAttrs.mail.name(), email.toLowerCase());
664 // userTransaction.commit();
665 // } catch (Exception e) {
667 // userTransaction.rollback();
668 // } catch (Exception e1) {
669 // log.error("Could not roll back", e1);
671 // if (e instanceof RuntimeException)
672 // throw (RuntimeException) e;
674 // throw new RuntimeException("Cannot create user", e);
679 public UserAdmin
getUserAdmin() {
683 // public UserTransaction getUserTransaction() {
684 // return userTransaction;
687 /* DEPENDENCY INJECTION */
688 public void setUserAdmin(UserAdmin userAdmin
) {
689 this.userAdmin
= userAdmin
;
691 if (userAdmin
instanceof AggregatingUserAdmin
) {
692 userDirectories
= ((AggregatingUserAdmin
) userAdmin
).getUserDirectories();
694 throw new IllegalArgumentException("Only " + AggregatingUserAdmin
.class.getName() + " is supported.");
697 // this.serviceProperties = serviceProperties;
700 public void setUserTransaction(WorkTransaction userTransaction
) {
701 this.userTransaction
= userTransaction
;
704 // public void addUserDirectory(UserDirectory userDirectory, Map<String, Object> properties) {
705 // userDirectories.put(userDirectory, new Hashtable<>(properties));
708 // public void removeUserDirectory(UserDirectory userDirectory, Map<String, Object> properties) {
709 // userDirectories.remove(userDirectory);