blob: 8ce3ab292e3e5314c0bec43f72d8fa7a6a432789 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.syncope.core.logic;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
import org.apache.syncope.common.lib.SyncopeClientException;
import org.apache.syncope.common.lib.request.BooleanReplacePatchItem;
import org.apache.syncope.common.lib.request.MembershipUR;
import org.apache.syncope.common.lib.request.PasswordPatch;
import org.apache.syncope.common.lib.request.StatusR;
import org.apache.syncope.common.lib.request.StringPatchItem;
import org.apache.syncope.common.lib.request.UserCR;
import org.apache.syncope.common.lib.request.UserUR;
import org.apache.syncope.common.lib.to.MembershipTO;
import org.apache.syncope.common.lib.to.PropagationStatus;
import org.apache.syncope.common.lib.to.ProvisioningResult;
import org.apache.syncope.common.lib.to.UserTO;
import org.apache.syncope.common.lib.types.AnyTypeKind;
import org.apache.syncope.common.lib.types.ClientExceptionType;
import org.apache.syncope.common.lib.types.PatchOperation;
import org.apache.syncope.common.lib.types.IdRepoEntitlement;
import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO;
import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
import org.apache.syncope.core.persistence.api.dao.DelegationDAO;
import org.apache.syncope.core.persistence.api.dao.GroupDAO;
import org.apache.syncope.core.persistence.api.dao.NotFoundException;
import org.apache.syncope.core.persistence.api.dao.UserDAO;
import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
import org.apache.syncope.core.persistence.api.entity.group.Group;
import org.apache.syncope.core.persistence.api.entity.user.User;
import org.apache.syncope.core.provisioning.api.LogicActions;
import org.apache.syncope.core.provisioning.api.UserProvisioningManager;
import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
import org.apache.syncope.core.provisioning.api.utils.RealmUtils;
import org.apache.syncope.core.spring.security.AuthContextUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* Note that this controller does not extend {@link AbstractTransactionalLogic}, hence does not provide any
* Spring's Transactional logic at class level.
*/
@Component
public class UserLogic extends AbstractAnyLogic<UserTO, UserCR, UserUR> {
@Autowired
protected UserDAO userDAO;
@Autowired
protected GroupDAO groupDAO;
@Autowired
protected AnySearchDAO searchDAO;
@Autowired
protected AccessTokenDAO accessTokenDAO;
@Autowired
protected DelegationDAO delegationDAO;
@Autowired
protected ConfParamOps confParamOps;
@Autowired
protected UserDataBinder binder;
@Autowired
protected UserProvisioningManager provisioningManager;
@Autowired
protected SyncopeLogic syncopeLogic;
@PreAuthorize("isAuthenticated() and not(hasRole('" + IdRepoEntitlement.MUST_CHANGE_PASSWORD + "'))")
@Transactional(readOnly = true)
public Triple<String, String, UserTO> selfRead() {
UserTO authenticatedUser = binder.getAuthenticatedUserTO();
return Triple.of(
POJOHelper.serialize(AuthContextUtils.getAuthorizations()),
POJOHelper.serialize(delegationDAO.findValidDelegating(authenticatedUser.getKey())),
binder.returnUserTO(authenticatedUser));
}
@PreAuthorize("hasRole('" + IdRepoEntitlement.USER_READ + "')")
@Transactional(readOnly = true)
@Override
public UserTO read(final String key) {
return binder.returnUserTO(binder.getUserTO(key));
}
@PreAuthorize("hasRole('" + IdRepoEntitlement.USER_SEARCH + "')")
@Transactional(readOnly = true)
@Override
public Pair<Integer, List<UserTO>> search(
final SearchCond searchCond,
final int page, final int size, final List<OrderByClause> orderBy,
final String realm,
final boolean details) {
Set<String> authRealms = RealmUtils.getEffective(
AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_SEARCH), realm);
SearchCond effectiveCond = searchCond == null ? userDAO.getAllMatchingCond() : searchCond;
int count = searchDAO.count(authRealms, effectiveCond, AnyTypeKind.USER);
List<User> matching = searchDAO.search(authRealms, effectiveCond, page, size, orderBy, AnyTypeKind.USER);
List<UserTO> result = matching.stream().
map(user -> binder.returnUserTO(binder.getUserTO(user, details))).
collect(Collectors.toList());
return Pair.of(count, result);
}
@PreAuthorize("isAnonymous() or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
public ProvisioningResult<UserTO> selfCreate(final UserCR createReq, final boolean nullPriorityAsync) {
return doCreate(createReq, true, nullPriorityAsync);
}
@PreAuthorize("hasRole('" + IdRepoEntitlement.USER_CREATE + "')")
public ProvisioningResult<UserTO> create(final UserCR createReq, final boolean nullPriorityAsync) {
return doCreate(createReq, false, nullPriorityAsync);
}
protected ProvisioningResult<UserTO> doCreate(
final UserCR userCR,
final boolean self,
final boolean nullPriorityAsync) {
Pair<UserCR, List<LogicActions>> before = beforeCreate(userCR);
if (before.getLeft().getRealm() == null) {
throw SyncopeClientException.build(ClientExceptionType.InvalidRealm);
}
if (!self) {
Set<String> authRealms = RealmUtils.getEffective(
AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_CREATE),
before.getLeft().getRealm());
userDAO.securityChecks(
authRealms,
null,
before.getLeft().getRealm(),
before.getLeft().getMemberships().stream().filter(Objects::nonNull).
map(MembershipTO::getGroupKey).filter(Objects::nonNull).
collect(Collectors.toSet()));
}
Pair<String, List<PropagationStatus>> created = provisioningManager.create(
before.getLeft(), nullPriorityAsync, AuthContextUtils.getUsername(), REST_CONTEXT);
return afterCreate(
binder.returnUserTO(binder.getUserTO(created.getKey())), created.getRight(), before.getRight());
}
@PreAuthorize("isAuthenticated() "
+ "and not(hasRole('" + IdRepoEntitlement.ANONYMOUS + "')) "
+ "and not(hasRole('" + IdRepoEntitlement.MUST_CHANGE_PASSWORD + "'))")
public ProvisioningResult<UserTO> selfUpdate(final UserUR userUR, final boolean nullPriorityAsync) {
UserTO userTO = binder.getAuthenticatedUserTO();
userUR.setKey(userTO.getKey());
ProvisioningResult<UserTO> updated = doUpdate(userUR, true, nullPriorityAsync);
// Ensures that, if the self update above moves the user into a status from which no authentication
// is possible, the existing Access Token is clean up to avoid issues with future authentications
List<String> authStatuses = List.of(confParamOps.get(AuthContextUtils.getDomain(),
"authentication.statuses", new String[] {}, String[].class));
if (!authStatuses.contains(updated.getEntity().getStatus())) {
String accessToken = accessTokenDAO.findByOwner(updated.getEntity().getUsername()).getKey();
if (accessToken != null) {
accessTokenDAO.delete(accessToken);
}
}
return updated;
}
@PreAuthorize("hasRole('" + IdRepoEntitlement.USER_UPDATE + "')")
@Override
public ProvisioningResult<UserTO> update(final UserUR userUR, final boolean nullPriorityAsync) {
return doUpdate(userUR, false, nullPriorityAsync);
}
protected Set<String> groups(final UserTO userTO) {
return userTO.getMemberships().stream().filter(Objects::nonNull).
map(MembershipTO::getGroupKey).filter(Objects::nonNull).
collect(Collectors.toSet());
}
protected ProvisioningResult<UserTO> doUpdate(
final UserUR userUR, final boolean self, final boolean nullPriorityAsync) {
UserTO userTO = binder.getUserTO(userUR.getKey());
Pair<UserUR, List<LogicActions>> before = beforeUpdate(userUR, userTO.getRealm());
Set<String> authRealms = RealmUtils.getEffective(
AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_UPDATE),
userTO.getRealm());
if (!self) {
Set<String> groups = groups(userTO);
groups.removeAll(userUR.getMemberships().stream().filter(Objects::nonNull).
filter(m -> m.getOperation() == PatchOperation.DELETE).
map(MembershipUR::getGroup).filter(Objects::nonNull).
collect(Collectors.toSet()));
userDAO.securityChecks(
authRealms,
before.getLeft().getKey(),
userTO.getRealm(),
groups);
}
Pair<UserUR, List<PropagationStatus>> after = provisioningManager.update(
before.getLeft(), nullPriorityAsync, AuthContextUtils.getUsername(), REST_CONTEXT);
ProvisioningResult<UserTO> result = afterUpdate(
binder.returnUserTO(binder.getUserTO(after.getLeft().getKey())),
after.getRight(),
before.getRight());
return result;
}
protected Pair<String, List<PropagationStatus>> setStatusOnWfAdapter(
final StatusR statusR, final boolean nullPriorityAsync) {
Pair<String, List<PropagationStatus>> updated;
switch (statusR.getType()) {
case SUSPEND:
updated = provisioningManager.suspend(
statusR, nullPriorityAsync, AuthContextUtils.getUsername(), REST_CONTEXT);
break;
case REACTIVATE:
updated = provisioningManager.reactivate(
statusR, nullPriorityAsync, AuthContextUtils.getUsername(), REST_CONTEXT);
break;
case ACTIVATE:
default:
updated = provisioningManager.activate(
statusR, nullPriorityAsync, AuthContextUtils.getUsername(), REST_CONTEXT);
break;
}
return updated;
}
@PreAuthorize("hasRole('" + IdRepoEntitlement.USER_UPDATE + "')")
public ProvisioningResult<UserTO> status(final StatusR statusR, final boolean nullPriorityAsync) {
// security checks
UserTO toUpdate = binder.getUserTO(statusR.getKey());
Set<String> authRealms = RealmUtils.getEffective(
AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_UPDATE),
toUpdate.getRealm());
userDAO.securityChecks(
authRealms,
toUpdate.getKey(),
toUpdate.getRealm(),
groups(toUpdate));
// ensures the actual user key is effectively on the request - as the binder.getUserTO(statusR.getKey())
// call above works with username as well
statusR.setKey(toUpdate.getKey());
Pair<String, List<PropagationStatus>> updated = setStatusOnWfAdapter(statusR, nullPriorityAsync);
return afterUpdate(
binder.returnUserTO(binder.getUserTO(updated.getKey())),
updated.getRight(),
List.of());
}
@PreAuthorize("isAuthenticated() and not(hasRole('" + IdRepoEntitlement.MUST_CHANGE_PASSWORD + "'))")
public ProvisioningResult<UserTO> selfStatus(final StatusR statusR, final boolean nullPriorityAsync) {
statusR.setKey(userDAO.findKey(AuthContextUtils.getUsername()));
Pair<String, List<PropagationStatus>> updated = setStatusOnWfAdapter(statusR, nullPriorityAsync);
return afterUpdate(
binder.returnUserTO(binder.getUserTO(updated.getKey())),
updated.getRight(),
List.of());
}
@PreAuthorize("hasRole('" + IdRepoEntitlement.MUST_CHANGE_PASSWORD + "')")
public ProvisioningResult<UserTO> mustChangePassword(final String password, final boolean nullPriorityAsync) {
UserUR userUR = new UserUR();
userUR.setPassword(new PasswordPatch.Builder().value(password).build());
userUR.setMustChangePassword(new BooleanReplacePatchItem.Builder().value(false).build());
return selfUpdate(userUR, nullPriorityAsync);
}
@PreAuthorize("isAnonymous() or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
@Transactional
public void requestPasswordReset(final String username, final String securityAnswer) {
if (username == null) {
throw new NotFoundException("Null username");
}
User user = userDAO.findByUsername(username);
if (user == null) {
throw new NotFoundException("User " + username);
}
if (syncopeLogic.isPwdResetRequiringSecurityQuestions()
&& (securityAnswer == null || !securityAnswer.equals(user.getSecurityAnswer()))) {
throw SyncopeClientException.build(ClientExceptionType.InvalidSecurityAnswer);
}
provisioningManager.requestPasswordReset(user.getKey(), AuthContextUtils.getUsername(), REST_CONTEXT);
}
@PreAuthorize("isAnonymous() or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
@Transactional
public void confirmPasswordReset(final String token, final String password) {
User user = userDAO.findByToken(token);
if (user == null) {
throw new NotFoundException("User with token " + token);
}
provisioningManager.confirmPasswordReset(
user.getKey(), token, password, AuthContextUtils.getUsername(), REST_CONTEXT);
}
@PreAuthorize("isAuthenticated() "
+ "and not(hasRole('" + IdRepoEntitlement.ANONYMOUS + "')) "
+ "and not(hasRole('" + IdRepoEntitlement.MUST_CHANGE_PASSWORD + "'))")
public ProvisioningResult<UserTO> selfDelete(final boolean nullPriorityAsync) {
return doDelete(binder.getAuthenticatedUserTO(), true, nullPriorityAsync);
}
@PreAuthorize("hasRole('" + IdRepoEntitlement.USER_DELETE + "')")
@Override
public ProvisioningResult<UserTO> delete(final String key, final boolean nullPriorityAsync) {
return doDelete(binder.getUserTO(key), false, nullPriorityAsync);
}
protected ProvisioningResult<UserTO> doDelete(
final UserTO userTO, final boolean self, final boolean nullPriorityAsync) {
Pair<UserTO, List<LogicActions>> before = beforeDelete(userTO);
if (!self) {
Set<String> authRealms = RealmUtils.getEffective(
AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_DELETE),
before.getLeft().getRealm());
userDAO.securityChecks(
authRealms,
before.getLeft().getKey(),
before.getLeft().getRealm(),
groups(before.getLeft()));
}
List<Group> ownedGroups = groupDAO.findOwnedByUser(before.getLeft().getKey());
if (!ownedGroups.isEmpty()) {
SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.GroupOwnership);
sce.getElements().addAll(ownedGroups.stream().
map(group -> group.getKey() + ' ' + group.getName()).collect(Collectors.toList()));
throw sce;
}
List<PropagationStatus> statuses = provisioningManager.delete(
before.getLeft().getKey(), nullPriorityAsync, AuthContextUtils.getUsername(), REST_CONTEXT);
UserTO deletedTO;
if (userDAO.find(before.getLeft().getKey()) == null) {
deletedTO = new UserTO();
deletedTO.setKey(before.getLeft().getKey());
} else {
deletedTO = binder.getUserTO(before.getLeft().getKey());
}
return afterDelete(binder.returnUserTO(deletedTO), statuses, before.getRight());
}
protected void updateChecks(final String key) {
User user = userDAO.authFind(key);
Set<String> authRealms = RealmUtils.getEffective(
AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_UPDATE),
user.getRealm().getFullPath());
userDAO.securityChecks(
authRealms,
user.getKey(),
user.getRealm().getFullPath(),
user.getMemberships().stream().
map(m -> m.getRightEnd().getKey()).
collect(Collectors.toSet()));
}
@PreAuthorize("hasRole('" + IdRepoEntitlement.USER_UPDATE + "')")
@Override
public UserTO unlink(final String key, final Collection<String> resources) {
updateChecks(key);
UserUR req = new UserUR();
req.setKey(key);
req.getResources().addAll(resources.stream().
map(r -> new StringPatchItem.Builder().operation(PatchOperation.DELETE).value(r).build()).
collect(Collectors.toList()));
return binder.returnUserTO(binder.getUserTO(
provisioningManager.unlink(req, AuthContextUtils.getUsername(), REST_CONTEXT)));
}
@PreAuthorize("hasRole('" + IdRepoEntitlement.USER_UPDATE + "')")
@Override
public UserTO link(final String key, final Collection<String> resources) {
updateChecks(key);
UserUR req = new UserUR();
req.setKey(key);
req.getResources().addAll(resources.stream().
map(r -> new StringPatchItem.Builder().operation(PatchOperation.ADD_REPLACE).value(r).build()).
collect(Collectors.toList()));
return binder.returnUserTO(binder.getUserTO(
provisioningManager.link(req, AuthContextUtils.getUsername(), REST_CONTEXT)));
}
@PreAuthorize("hasRole('" + IdRepoEntitlement.USER_UPDATE + "')")
@Override
public ProvisioningResult<UserTO> unassign(
final String key, final Collection<String> resources, final boolean nullPriorityAsync) {
updateChecks(key);
UserUR req = new UserUR();
req.setKey(key);
req.getResources().addAll(resources.stream().
map(r -> new StringPatchItem.Builder().operation(PatchOperation.DELETE).value(r).build()).
collect(Collectors.toList()));
return update(req, nullPriorityAsync);
}
@PreAuthorize("hasRole('" + IdRepoEntitlement.USER_UPDATE + "')")
@Override
public ProvisioningResult<UserTO> assign(
final String key,
final Collection<String> resources,
final boolean changepwd,
final String password,
final boolean nullPriorityAsync) {
updateChecks(key);
UserUR req = new UserUR();
req.setKey(key);
req.getResources().addAll(resources.stream().
map(r -> new StringPatchItem.Builder().operation(PatchOperation.ADD_REPLACE).value(r).build()).
collect(Collectors.toList()));
if (changepwd) {
req.setPassword(new PasswordPatch.Builder().
value(password).onSyncope(false).resources(resources).build());
}
return update(req, nullPriorityAsync);
}
@PreAuthorize("hasRole('" + IdRepoEntitlement.USER_UPDATE + "')")
@Override
public ProvisioningResult<UserTO> deprovision(
final String key, final Collection<String> resources, final boolean nullPriorityAsync) {
updateChecks(key);
List<PropagationStatus> statuses = provisioningManager.deprovision(
key, resources, nullPriorityAsync, AuthContextUtils.getUsername(), REST_CONTEXT);
ProvisioningResult<UserTO> result = new ProvisioningResult<>();
result.setEntity(binder.returnUserTO(binder.getUserTO(key)));
result.getPropagationStatuses().addAll(statuses);
return result;
}
@PreAuthorize("hasRole('" + IdRepoEntitlement.USER_UPDATE + "')")
@Override
public ProvisioningResult<UserTO> provision(
final String key,
final Collection<String> resources,
final boolean changePwd,
final String password,
final boolean nullPriorityAsync) {
updateChecks(key);
List<PropagationStatus> statuses = provisioningManager.provision(
key, changePwd, password, resources, nullPriorityAsync, AuthContextUtils.getUsername(), REST_CONTEXT);
ProvisioningResult<UserTO> result = new ProvisioningResult<>();
result.setEntity(binder.returnUserTO(binder.getUserTO(key)));
result.getPropagationStatuses().addAll(statuses);
return result;
}
@Override
protected UserTO resolveReference(final Method method, final Object... args) throws UnresolvedReferenceException {
String key = null;
if ("requestPasswordReset".equals(method.getName())) {
key = userDAO.findKey((String) args[0]);
} else if (!"confirmPasswordReset".equals(method.getName()) && ArrayUtils.isNotEmpty(args)) {
for (int i = 0; key == null && i < args.length; i++) {
if (args[i] instanceof String) {
key = (String) args[i];
} else if (args[i] instanceof UserTO) {
key = ((UserTO) args[i]).getKey();
} else if (args[i] instanceof UserUR) {
key = ((UserUR) args[i]).getKey();
} else if (args[i] instanceof StatusR) {
key = ((StatusR) args[i]).getKey();
}
}
}
if (key != null) {
try {
return binder.getUserTO(key);
} catch (Throwable ignore) {
LOG.debug("Unresolved reference", ignore);
throw new UnresolvedReferenceException(ignore);
}
}
throw new UnresolvedReferenceException();
}
}