View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.syncope.core.logic;
20  
21  import java.lang.reflect.Method;
22  import java.util.Collection;
23  import java.util.Comparator;
24  import java.util.List;
25  import java.util.Objects;
26  import java.util.Optional;
27  import java.util.Set;
28  import java.util.stream.Collectors;
29  import org.apache.commons.lang3.ArrayUtils;
30  import org.apache.commons.lang3.StringUtils;
31  import org.apache.commons.lang3.tuple.Pair;
32  import org.apache.commons.lang3.tuple.Triple;
33  import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
34  import org.apache.syncope.common.lib.SyncopeClientException;
35  import org.apache.syncope.common.lib.request.BooleanReplacePatchItem;
36  import org.apache.syncope.common.lib.request.MembershipUR;
37  import org.apache.syncope.common.lib.request.PasswordPatch;
38  import org.apache.syncope.common.lib.request.StatusR;
39  import org.apache.syncope.common.lib.request.StringPatchItem;
40  import org.apache.syncope.common.lib.request.UserCR;
41  import org.apache.syncope.common.lib.request.UserUR;
42  import org.apache.syncope.common.lib.to.MembershipTO;
43  import org.apache.syncope.common.lib.to.PropagationStatus;
44  import org.apache.syncope.common.lib.to.ProvisioningResult;
45  import org.apache.syncope.common.lib.to.UserTO;
46  import org.apache.syncope.common.lib.types.AnyTypeKind;
47  import org.apache.syncope.common.lib.types.ClientExceptionType;
48  import org.apache.syncope.common.lib.types.EntityViolationType;
49  import org.apache.syncope.common.lib.types.IdRepoEntitlement;
50  import org.apache.syncope.common.lib.types.PatchOperation;
51  import org.apache.syncope.common.rest.api.beans.ComplianceQuery;
52  import org.apache.syncope.core.logic.api.LogicActions;
53  import org.apache.syncope.core.persistence.api.attrvalue.validation.InvalidEntityException;
54  import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO;
55  import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
56  import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
57  import org.apache.syncope.core.persistence.api.dao.DelegationDAO;
58  import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
59  import org.apache.syncope.core.persistence.api.dao.GroupDAO;
60  import org.apache.syncope.core.persistence.api.dao.NotFoundException;
61  import org.apache.syncope.core.persistence.api.dao.RealmDAO;
62  import org.apache.syncope.core.persistence.api.dao.UserDAO;
63  import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
64  import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
65  import org.apache.syncope.core.persistence.api.entity.AccessToken;
66  import org.apache.syncope.core.persistence.api.entity.Entity;
67  import org.apache.syncope.core.persistence.api.entity.ExternalResource;
68  import org.apache.syncope.core.persistence.api.entity.Realm;
69  import org.apache.syncope.core.persistence.api.entity.group.Group;
70  import org.apache.syncope.core.persistence.api.entity.policy.AccountPolicy;
71  import org.apache.syncope.core.persistence.api.entity.policy.PasswordPolicy;
72  import org.apache.syncope.core.persistence.api.entity.user.User;
73  import org.apache.syncope.core.provisioning.api.UserProvisioningManager;
74  import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
75  import org.apache.syncope.core.provisioning.api.rules.RuleEnforcer;
76  import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
77  import org.apache.syncope.core.provisioning.api.utils.RealmUtils;
78  import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
79  import org.apache.syncope.core.spring.policy.AccountPolicyException;
80  import org.apache.syncope.core.spring.policy.PasswordPolicyException;
81  import org.apache.syncope.core.spring.security.AuthContextUtils;
82  import org.apache.syncope.core.spring.security.Encryptor;
83  import org.springframework.security.access.prepost.PreAuthorize;
84  import org.springframework.transaction.annotation.Transactional;
85  
86  /**
87   * Note that this controller does not extend {@link AbstractTransactionalLogic}, hence does not provide any
88   * Spring's Transactional logic at class level.
89   */
90  public class UserLogic extends AbstractAnyLogic<UserTO, UserCR, UserUR> {
91  
92      protected final UserDAO userDAO;
93  
94      protected final GroupDAO groupDAO;
95  
96      protected final AnySearchDAO searchDAO;
97  
98      protected final ExternalResourceDAO resourceDAO;
99  
100     protected final AccessTokenDAO accessTokenDAO;
101 
102     protected final DelegationDAO delegationDAO;
103 
104     protected final ConfParamOps confParamOps;
105 
106     protected final UserDataBinder binder;
107 
108     protected final UserProvisioningManager provisioningManager;
109 
110     protected final SyncopeLogic syncopeLogic;
111 
112     protected final RuleEnforcer ruleEnforcer;
113 
114     public UserLogic(
115             final RealmDAO realmDAO,
116             final AnyTypeDAO anyTypeDAO,
117             final TemplateUtils templateUtils,
118             final UserDAO userDAO,
119             final GroupDAO groupDAO,
120             final AnySearchDAO searchDAO,
121             final ExternalResourceDAO resourceDAO,
122             final AccessTokenDAO accessTokenDAO,
123             final DelegationDAO delegationDAO,
124             final ConfParamOps confParamOps,
125             final UserDataBinder binder,
126             final UserProvisioningManager provisioningManager,
127             final SyncopeLogic syncopeLogic,
128             final RuleEnforcer ruleEnforcer) {
129 
130         super(realmDAO, anyTypeDAO, templateUtils);
131 
132         this.userDAO = userDAO;
133         this.groupDAO = groupDAO;
134         this.searchDAO = searchDAO;
135         this.resourceDAO = resourceDAO;
136         this.accessTokenDAO = accessTokenDAO;
137         this.delegationDAO = delegationDAO;
138         this.confParamOps = confParamOps;
139         this.binder = binder;
140         this.provisioningManager = provisioningManager;
141         this.syncopeLogic = syncopeLogic;
142         this.ruleEnforcer = ruleEnforcer;
143     }
144 
145     @PreAuthorize("isAuthenticated() and not(hasRole('" + IdRepoEntitlement.MUST_CHANGE_PASSWORD + "'))")
146     @Transactional(readOnly = true)
147     public Triple<String, String, UserTO> selfRead() {
148         UserTO authenticatedUser = binder.getAuthenticatedUserTO();
149 
150         return Triple.of(
151                 POJOHelper.serialize(AuthContextUtils.getAuthorizations()),
152                 POJOHelper.serialize(delegationDAO.findValidDelegating(authenticatedUser.getKey())), authenticatedUser);
153     }
154 
155     @PreAuthorize("hasRole('" + IdRepoEntitlement.USER_READ + "')")
156     @Transactional(readOnly = true)
157     @Override
158     public UserTO read(final String key) {
159         return binder.getUserTO(key);
160     }
161 
162     @PreAuthorize("hasRole('" + IdRepoEntitlement.USER_SEARCH + "')")
163     @Transactional(readOnly = true)
164     @Override
165     public Pair<Integer, List<UserTO>> search(
166             final SearchCond searchCond,
167             final int page, final int size, final List<OrderByClause> orderBy,
168             final String realm,
169             final boolean recursive,
170             final boolean details) {
171 
172         Realm base = Optional.ofNullable(realmDAO.findByFullPath(realm)).
173                 orElseThrow(() -> new NotFoundException("Realm " + realm));
174 
175         Set<String> authRealms = RealmUtils.getEffective(
176                 AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_SEARCH), realm);
177 
178         SearchCond effectiveCond = searchCond == null ? userDAO.getAllMatchingCond() : searchCond;
179 
180         int count = searchDAO.count(base, recursive, authRealms, effectiveCond, AnyTypeKind.USER);
181 
182         List<User> matching = searchDAO.search(
183                 base, recursive, authRealms, effectiveCond, page, size, orderBy, AnyTypeKind.USER);
184         List<UserTO> result = matching.stream().
185                 map(user -> binder.getUserTO(user, details)).
186                 collect(Collectors.toList());
187 
188         return Pair.of(count, result);
189     }
190 
191     @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
192     public ProvisioningResult<UserTO> selfCreate(final UserCR createReq, final boolean nullPriorityAsync) {
193         return doCreate(createReq, true, nullPriorityAsync);
194     }
195 
196     @PreAuthorize("hasRole('" + IdRepoEntitlement.USER_CREATE + "')")
197     public ProvisioningResult<UserTO> create(final UserCR createReq, final boolean nullPriorityAsync) {
198         return doCreate(createReq, false, nullPriorityAsync);
199     }
200 
201     protected ProvisioningResult<UserTO> doCreate(
202             final UserCR userCR,
203             final boolean self,
204             final boolean nullPriorityAsync) {
205 
206         Pair<UserCR, List<LogicActions>> before = beforeCreate(userCR);
207 
208         if (before.getLeft().getRealm() == null) {
209             throw SyncopeClientException.build(ClientExceptionType.InvalidRealm);
210         }
211 
212         if (!self) {
213             Set<String> authRealms = RealmUtils.getEffective(
214                     AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_CREATE),
215                     before.getLeft().getRealm());
216             userDAO.securityChecks(
217                     authRealms,
218                     null,
219                     before.getLeft().getRealm(),
220                     before.getLeft().getMemberships().stream().filter(Objects::nonNull).
221                             map(MembershipTO::getGroupKey).filter(Objects::nonNull).
222                             collect(Collectors.toSet()));
223         }
224 
225         Pair<String, List<PropagationStatus>> created = provisioningManager.create(
226                 before.getLeft(), nullPriorityAsync, AuthContextUtils.getUsername(), REST_CONTEXT);
227 
228         return afterCreate(binder.getUserTO(created.getKey()), created.getRight(), before.getRight());
229     }
230 
231     @PreAuthorize("isAuthenticated() "
232             + "and not(hasRole('" + IdRepoEntitlement.ANONYMOUS + "')) "
233             + "and not(hasRole('" + IdRepoEntitlement.MUST_CHANGE_PASSWORD + "'))")
234     public ProvisioningResult<UserTO> selfUpdate(final UserUR userUR, final boolean nullPriorityAsync) {
235         UserTO userTO = binder.getAuthenticatedUserTO();
236         userUR.setKey(userTO.getKey());
237         ProvisioningResult<UserTO> updated = doUpdate(userUR, true, nullPriorityAsync);
238 
239         // Ensures that, if the self update above moves the user into a status from which no authentication
240         // is possible, the existing Access Token is clean up to avoid issues with future authentications
241         List<String> authStatuses = List.of(confParamOps.get(AuthContextUtils.getDomain(),
242                 "authentication.statuses", new String[] {}, String[].class));
243         if (!authStatuses.contains(updated.getEntity().getStatus())) {
244             Optional.ofNullable(accessTokenDAO.findByOwner(updated.getEntity().getUsername())).
245                     map(AccessToken::getKey).ifPresent(accessTokenDAO::delete);
246         }
247 
248         return updated;
249     }
250 
251     @PreAuthorize("hasRole('" + IdRepoEntitlement.USER_UPDATE + "')")
252     @Override
253     public ProvisioningResult<UserTO> update(final UserUR userUR, final boolean nullPriorityAsync) {
254         return doUpdate(userUR, false, nullPriorityAsync);
255     }
256 
257     protected Set<String> groups(final UserTO userTO) {
258         return userTO.getMemberships().stream().filter(Objects::nonNull).
259                 map(MembershipTO::getGroupKey).filter(Objects::nonNull).
260                 collect(Collectors.toSet());
261     }
262 
263     protected ProvisioningResult<UserTO> doUpdate(
264             final UserUR userUR, final boolean self, final boolean nullPriorityAsync) {
265 
266         UserTO userTO = binder.getUserTO(userUR.getKey());
267         Pair<UserUR, List<LogicActions>> before = beforeUpdate(userUR, userTO.getRealm());
268 
269         Set<String> authRealms = RealmUtils.getEffective(
270                 AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_UPDATE),
271                 userTO.getRealm());
272         if (!self) {
273             Set<String> groups = groups(userTO);
274             groups.removeAll(userUR.getMemberships().stream().filter(Objects::nonNull).
275                     filter(m -> m.getOperation() == PatchOperation.DELETE).
276                     map(MembershipUR::getGroup).filter(Objects::nonNull).
277                     collect(Collectors.toSet()));
278 
279             userDAO.securityChecks(
280                     authRealms,
281                     before.getLeft().getKey(),
282                     userTO.getRealm(),
283                     groups);
284         }
285 
286         Pair<UserUR, List<PropagationStatus>> after = provisioningManager.update(
287                 before.getLeft(), nullPriorityAsync, AuthContextUtils.getUsername(), REST_CONTEXT);
288 
289         ProvisioningResult<UserTO> result = afterUpdate(
290                 binder.getUserTO(after.getLeft().getKey()),
291                 after.getRight(),
292                 before.getRight());
293 
294         return result;
295     }
296 
297     protected Pair<String, List<PropagationStatus>> setStatusOnWfAdapter(
298             final StatusR statusR, final boolean nullPriorityAsync) {
299 
300         Pair<String, List<PropagationStatus>> updated;
301 
302         switch (statusR.getType()) {
303             case SUSPEND:
304                 updated = provisioningManager.suspend(
305                         statusR, nullPriorityAsync, AuthContextUtils.getUsername(), REST_CONTEXT);
306                 break;
307 
308             case REACTIVATE:
309                 updated = provisioningManager.reactivate(
310                         statusR, nullPriorityAsync, AuthContextUtils.getUsername(), REST_CONTEXT);
311                 break;
312 
313             case ACTIVATE:
314             default:
315                 updated = provisioningManager.activate(
316                         statusR, nullPriorityAsync, AuthContextUtils.getUsername(), REST_CONTEXT);
317                 break;
318 
319         }
320 
321         return updated;
322     }
323 
324     @PreAuthorize("hasRole('" + IdRepoEntitlement.USER_UPDATE + "')")
325     public ProvisioningResult<UserTO> status(final StatusR statusR, final boolean nullPriorityAsync) {
326         // security checks
327         UserTO toUpdate = binder.getUserTO(statusR.getKey());
328 
329         Set<String> authRealms = RealmUtils.getEffective(
330                 AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_UPDATE),
331                 toUpdate.getRealm());
332         userDAO.securityChecks(
333                 authRealms,
334                 toUpdate.getKey(),
335                 toUpdate.getRealm(),
336                 groups(toUpdate));
337 
338         // ensures the actual user key is effectively on the request - as the binder.getUserTO(statusR.getKey())
339         // call above works with username as well
340         statusR.setKey(toUpdate.getKey());
341         Pair<String, List<PropagationStatus>> updated = setStatusOnWfAdapter(statusR, nullPriorityAsync);
342 
343         return afterUpdate(
344                 binder.getUserTO(updated.getKey()),
345                 updated.getRight(),
346                 List.of());
347     }
348 
349     @PreAuthorize("isAuthenticated() and not(hasRole('" + IdRepoEntitlement.MUST_CHANGE_PASSWORD + "'))")
350     public ProvisioningResult<UserTO> selfStatus(final StatusR statusR, final boolean nullPriorityAsync) {
351         statusR.setKey(userDAO.findKey(AuthContextUtils.getUsername()));
352         Pair<String, List<PropagationStatus>> updated = setStatusOnWfAdapter(statusR, nullPriorityAsync);
353 
354         return afterUpdate(
355                 binder.getUserTO(updated.getKey()),
356                 updated.getRight(),
357                 List.of());
358     }
359 
360     @PreAuthorize("hasRole('" + IdRepoEntitlement.MUST_CHANGE_PASSWORD + "')")
361     public ProvisioningResult<UserTO> mustChangePassword(
362             final PasswordPatch password, final boolean nullPriorityAsync) {
363 
364         UserTO userTO = binder.getAuthenticatedUserTO();
365 
366         password.setOnSyncope(true);
367         password.getResources().clear();
368         password.getResources().addAll(userDAO.findAllResourceKeys(userTO.getKey()));
369 
370         UserUR userUR = new UserUR.Builder(userTO.getKey()).
371                 password(password).
372                 mustChangePassword(new BooleanReplacePatchItem.Builder().value(false).build()).
373                 build();
374         ProvisioningResult<UserTO> result = selfUpdate(userUR, nullPriorityAsync);
375 
376         Optional.ofNullable(accessTokenDAO.findByOwner(result.getEntity().getUsername())).
377                 map(AccessToken::getKey).ifPresent(accessTokenDAO::delete);
378 
379         return result;
380     }
381 
382     @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
383     @Transactional(readOnly = true)
384     public void compliance(final ComplianceQuery query) {
385         SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.RESTValidation);
386 
387         if (query.isEmpty()) {
388             sce.getElements().add("Nothing to check");
389             throw sce;
390         }
391 
392         Realm realm = null;
393         if (StringUtils.isNotBlank(query.getRealm())) {
394             realm = Optional.ofNullable(realmDAO.findByFullPath(query.getRealm())).
395                     orElseThrow(() -> new NotFoundException("Realm " + query.getRealm()));
396         }
397         Set<ExternalResource> resources = query.getResources().stream().
398                 map(resourceDAO::find).filter(Objects::nonNull).collect(Collectors.toSet());
399         if (realm == null && resources.isEmpty()) {
400             sce.getElements().add("Nothing to check");
401             throw sce;
402         }
403 
404         if (StringUtils.isNotBlank(query.getUsername())) {
405             List<AccountPolicy> accountPolicies = ruleEnforcer.getAccountPolicies(realm, resources);
406             try {
407                 if (accountPolicies.isEmpty()) {
408                     if (!Entity.ID_PATTERN.matcher(query.getUsername()).matches()) {
409                         throw new AccountPolicyException("Character(s) not allowed: " + query.getUsername());
410                     }
411                 } else {
412                     for (AccountPolicy policy : accountPolicies) {
413                         ruleEnforcer.getAccountRules(policy).forEach(rule -> rule.enforce(query.getUsername()));
414                     }
415                 }
416             } catch (AccountPolicyException e) {
417                 throw new InvalidEntityException(User.class, EntityViolationType.InvalidUsername, e.getMessage());
418             }
419         }
420 
421         if (StringUtils.isNotBlank(query.getPassword())) {
422             try {
423                 for (PasswordPolicy policy : ruleEnforcer.getPasswordPolicies(realm, resources)) {
424                     ruleEnforcer.getPasswordRules(policy).
425                             forEach(rule -> rule.enforce(query.getUsername(), query.getPassword()));
426                 }
427             } catch (PasswordPolicyException e) {
428                 throw new InvalidEntityException(User.class, EntityViolationType.InvalidPassword, e.getMessage());
429             }
430         }
431     }
432 
433     @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
434     @Transactional
435     public void requestPasswordReset(final String username, final String securityAnswer) {
436         User user = Optional.ofNullable(userDAO.findByUsername(username)).
437                 orElseThrow(() -> new NotFoundException("User " + username));
438 
439         if (syncopeLogic.isPwdResetRequiringSecurityQuestions()
440                 && (securityAnswer == null || !Encryptor.getInstance().
441                         verify(securityAnswer, user.getCipherAlgorithm(), user.getSecurityAnswer()))) {
442 
443             throw SyncopeClientException.build(ClientExceptionType.InvalidSecurityAnswer);
444         }
445 
446         provisioningManager.requestPasswordReset(user.getKey(), AuthContextUtils.getUsername(), REST_CONTEXT);
447     }
448 
449     @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
450     @Transactional
451     public void confirmPasswordReset(final String token, final String password) {
452         User user = Optional.ofNullable(userDAO.findByToken(token)).
453                 orElseThrow(() -> new NotFoundException("User with token " + token));
454 
455         provisioningManager.confirmPasswordReset(
456                 user.getKey(), token, password, AuthContextUtils.getUsername(), REST_CONTEXT);
457     }
458 
459     @PreAuthorize("isAuthenticated() "
460             + "and not(hasRole('" + IdRepoEntitlement.ANONYMOUS + "')) "
461             + "and not(hasRole('" + IdRepoEntitlement.MUST_CHANGE_PASSWORD + "'))")
462     public ProvisioningResult<UserTO> selfDelete(final boolean nullPriorityAsync) {
463         return doDelete(binder.getAuthenticatedUserTO(), true, nullPriorityAsync);
464     }
465 
466     @PreAuthorize("hasRole('" + IdRepoEntitlement.USER_DELETE + "')")
467     @Override
468     public ProvisioningResult<UserTO> delete(final String key, final boolean nullPriorityAsync) {
469         return doDelete(binder.getUserTO(key), false, nullPriorityAsync);
470     }
471 
472     protected ProvisioningResult<UserTO> doDelete(
473             final UserTO userTO, final boolean self, final boolean nullPriorityAsync) {
474 
475         Pair<UserTO, List<LogicActions>> before = beforeDelete(userTO);
476 
477         if (!self) {
478             Set<String> authRealms = RealmUtils.getEffective(
479                     AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_DELETE),
480                     before.getLeft().getRealm());
481             userDAO.securityChecks(
482                     authRealms,
483                     before.getLeft().getKey(),
484                     before.getLeft().getRealm(),
485                     groups(before.getLeft()));
486         }
487 
488         List<Group> ownedGroups = groupDAO.findOwnedByUser(before.getLeft().getKey());
489         if (!ownedGroups.isEmpty()) {
490             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.GroupOwnership);
491             sce.getElements().addAll(ownedGroups.stream().
492                     map(group -> group.getKey() + ' ' + group.getName()).collect(Collectors.toList()));
493             throw sce;
494         }
495 
496         List<PropagationStatus> statuses = provisioningManager.delete(
497                 before.getLeft().getKey(), nullPriorityAsync, AuthContextUtils.getUsername(), REST_CONTEXT);
498 
499         UserTO deletedTO;
500         if (userDAO.find(before.getLeft().getKey()) == null) {
501             deletedTO = new UserTO();
502             deletedTO.setKey(before.getLeft().getKey());
503         } else {
504             deletedTO = binder.getUserTO(before.getLeft().getKey());
505         }
506 
507         return afterDelete(deletedTO, statuses, before.getRight());
508     }
509 
510     protected void updateChecks(final String key) {
511         UserTO userTO = binder.getUserTO(key);
512 
513         Set<String> authRealms = RealmUtils.getEffective(
514                 AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_UPDATE),
515                 userTO.getRealm());
516         userDAO.securityChecks(
517                 authRealms,
518                 userTO.getKey(),
519                 userTO.getRealm(),
520                 userTO.getMemberships().stream().
521                         map(MembershipTO::getGroupKey).
522                         collect(Collectors.toSet()));
523     }
524 
525     @PreAuthorize("hasRole('" + IdRepoEntitlement.USER_UPDATE + "')")
526     @Override
527     public UserTO unlink(final String key, final Collection<String> resources) {
528         updateChecks(key);
529 
530         UserUR req = new UserUR.Builder(key).
531                 resources(resources.stream().
532                         map(r -> new StringPatchItem.Builder().operation(PatchOperation.DELETE).value(r).build()).
533                         collect(Collectors.toList())).
534                 build();
535 
536         return binder.getUserTO(provisioningManager.unlink(req, AuthContextUtils.getUsername(), REST_CONTEXT));
537     }
538 
539     @PreAuthorize("hasRole('" + IdRepoEntitlement.USER_UPDATE + "')")
540     @Override
541     public UserTO link(final String key, final Collection<String> resources) {
542         updateChecks(key);
543 
544         UserUR req = new UserUR.Builder(key).
545                 resources(resources.stream().
546                         map(r -> new StringPatchItem.Builder().operation(PatchOperation.ADD_REPLACE).value(r).build()).
547                         collect(Collectors.toList())).
548                 build();
549 
550         return binder.getUserTO(provisioningManager.link(req, AuthContextUtils.getUsername(), REST_CONTEXT));
551     }
552 
553     @PreAuthorize("hasRole('" + IdRepoEntitlement.USER_UPDATE + "')")
554     @Override
555     public ProvisioningResult<UserTO> unassign(
556             final String key, final Collection<String> resources, final boolean nullPriorityAsync) {
557 
558         updateChecks(key);
559 
560         UserUR req = new UserUR.Builder(key).
561                 resources(resources.stream().
562                         map(r -> new StringPatchItem.Builder().operation(PatchOperation.DELETE).value(r).build()).
563                         collect(Collectors.toList())).
564                 build();
565 
566         return update(req, nullPriorityAsync);
567     }
568 
569     @PreAuthorize("hasRole('" + IdRepoEntitlement.USER_UPDATE + "')")
570     @Override
571     public ProvisioningResult<UserTO> assign(
572             final String key,
573             final Collection<String> resources,
574             final boolean changepwd,
575             final String password,
576             final boolean nullPriorityAsync) {
577 
578         updateChecks(key);
579 
580         UserUR req = new UserUR.Builder(key).
581                 resources(resources.stream().
582                         map(r -> new StringPatchItem.Builder().operation(PatchOperation.ADD_REPLACE).value(r).build()).
583                         collect(Collectors.toList())).
584                 build();
585 
586         if (changepwd) {
587             req.setPassword(new PasswordPatch.Builder().
588                     value(password).onSyncope(false).resources(resources).build());
589         }
590 
591         return update(req, nullPriorityAsync);
592     }
593 
594     @PreAuthorize("hasRole('" + IdRepoEntitlement.USER_UPDATE + "')")
595     @Override
596     public ProvisioningResult<UserTO> deprovision(
597             final String key,
598             final List<String> resources,
599             final boolean nullPriorityAsync) {
600 
601         updateChecks(key);
602 
603         List<PropagationStatus> statuses = provisioningManager.deprovision(
604                 key, resources, nullPriorityAsync, AuthContextUtils.getUsername());
605 
606         ProvisioningResult<UserTO> result = new ProvisioningResult<>();
607         result.setEntity(binder.getUserTO(key));
608         result.getPropagationStatuses().addAll(statuses);
609         result.getPropagationStatuses().sort(Comparator.comparing(item -> resources.indexOf(item.getResource())));
610         return result;
611     }
612 
613     @PreAuthorize("hasRole('" + IdRepoEntitlement.USER_UPDATE + "')")
614     @Override
615     public ProvisioningResult<UserTO> provision(
616             final String key,
617             final List<String> resources,
618             final boolean changePwd,
619             final String password,
620             final boolean nullPriorityAsync) {
621 
622         updateChecks(key);
623 
624         List<PropagationStatus> statuses = provisioningManager.provision(
625                 key, changePwd, password, resources, nullPriorityAsync, AuthContextUtils.getUsername());
626 
627         ProvisioningResult<UserTO> result = new ProvisioningResult<>();
628         result.setEntity(binder.getUserTO(key));
629         result.getPropagationStatuses().addAll(statuses);
630         result.getPropagationStatuses().sort(Comparator.comparing(item -> resources.indexOf(item.getResource())));
631         return result;
632     }
633 
634     @Override
635     protected UserTO resolveReference(final Method method, final Object... args) throws UnresolvedReferenceException {
636         String key = null;
637 
638         if ("requestPasswordReset".equals(method.getName())) {
639             key = userDAO.findKey((String) args[0]);
640         } else if (!"confirmPasswordReset".equals(method.getName()) && ArrayUtils.isNotEmpty(args)) {
641             for (int i = 0; key == null && i < args.length; i++) {
642                 if (args[i] instanceof String) {
643                     key = (String) args[i];
644                 } else if (args[i] instanceof UserTO) {
645                     key = ((UserTO) args[i]).getKey();
646                 } else if (args[i] instanceof UserUR) {
647                     key = ((UserUR) args[i]).getKey();
648                 } else if (args[i] instanceof StatusR) {
649                     key = ((StatusR) args[i]).getKey();
650                 }
651             }
652         }
653 
654         if (key != null) {
655             try {
656                 return binder.getUserTO(key);
657             } catch (Throwable ignore) {
658                 LOG.debug("Unresolved reference", ignore);
659                 throw new UnresolvedReferenceException(ignore);
660             }
661         }
662 
663         throw new UnresolvedReferenceException();
664     }
665 }