/*
* 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.rest.data;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.apache.syncope.common.mod.MembershipMod;
import org.apache.syncope.common.mod.UserMod;
import org.apache.syncope.common.to.MembershipTO;
import org.apache.syncope.common.to.UserTO;
import org.apache.syncope.common.types.AttributableType;
import org.apache.syncope.common.types.CipherAlgorithm;
import org.apache.syncope.common.types.IntMappingType;
import org.apache.syncope.common.types.PasswordPolicySpec;
import org.apache.syncope.common.types.ResourceOperation;
import org.apache.syncope.common.types.ClientExceptionType;
import org.apache.syncope.common.util.BeanUtils;
import org.apache.syncope.common.SyncopeClientCompositeException;
import org.apache.syncope.common.SyncopeClientException;
import org.apache.syncope.common.mod.AttributeMod;
import org.apache.syncope.core.connid.ConnObjectUtil;
import org.apache.syncope.core.persistence.beans.AbstractAttr;
import org.apache.syncope.core.persistence.beans.AbstractDerAttr;
import org.apache.syncope.core.persistence.beans.AbstractMappingItem;
import org.apache.syncope.core.persistence.beans.AbstractVirAttr;
import org.apache.syncope.core.persistence.beans.ExternalResource;
import org.apache.syncope.core.persistence.beans.PasswordPolicy;
import org.apache.syncope.core.persistence.beans.membership.MAttr;
import org.apache.syncope.core.persistence.beans.membership.MDerAttr;
import org.apache.syncope.core.persistence.beans.membership.MVirAttr;
import org.apache.syncope.core.persistence.beans.membership.Membership;
import org.apache.syncope.core.persistence.beans.role.SyncopeRole;
import org.apache.syncope.core.persistence.beans.user.SyncopeUser;
import org.apache.syncope.core.persistence.dao.NotFoundException;
import org.apache.syncope.core.propagation.PropagationByResource;
import org.apache.syncope.core.rest.controller.UnauthorizedRoleException;
import org.apache.syncope.core.util.AttributableUtil;
import org.apache.syncope.core.util.EntitlementUtil;
import org.apache.syncope.core.util.Encryptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
@Transactional(rollbackFor = { Throwable.class })
public class UserDataBinder extends AbstractAttributableDataBinder {
private static final String[] IGNORE_USER_PROPERTIES = {
"memberships", "attrs", "derAttrs", "virAttrs", "resources"
};
@Autowired
private ConnObjectUtil connObjectUtil;
@Resource(name = "adminUser")
private String adminUser;
@Resource(name = "anonymousUser")
private String anonymousUser;
private final Encryptor encryptor = Encryptor.getInstance();
@Transactional(readOnly = true)
public SyncopeUser getUserFromId(final Long userId) {
if (userId == null) {
throw new NotFoundException("Null user id");
}
SyncopeUser user = userDAO.find(userId);
if (user == null) {
throw new NotFoundException("User " + userId);
}
// Allows anonymous (during self-registration) and self (during self-update) to read own SyncopeUser,
// otherwise goes thorugh security checks to see if needed role entitlements are owned
if (!EntitlementUtil.getAuthenticatedUsername().equals(anonymousUser)
&& !EntitlementUtil.getAuthenticatedUsername().equals(user.getUsername())) {
Set<Long> roleIds = user.getRoleIds();
Set<Long> adminRoleIds = EntitlementUtil.getRoleIds(EntitlementUtil.getOwnedEntitlementNames());
roleIds.removeAll(adminRoleIds);
if (!roleIds.isEmpty()) {
throw new UnauthorizedRoleException(roleIds);
}
}
return user;
}
@Transactional(readOnly = true)
public Membership getMembershipFromId(final Long membershipId) {
if (membershipId == null) {
throw new NotFoundException("Null membership id");
}
Membership membership = membershipDAO.find(membershipId);
if (membership == null) {
throw new NotFoundException("Membership " + membershipId);
}
return membership;
}
@Transactional(readOnly = true)
public Set<String> getResourceNamesForUserId(final Long userId) {
return getUserFromId(userId).getResourceNames();
}
@Transactional(readOnly = true)
public UserTO getAuthenticatedUserTO() {
final UserTO authUserTO;
final String authUsername = SecurityContextHolder.getContext().getAuthentication().getName();
if (anonymousUser.equals(authUsername)) {
authUserTO = new UserTO();
authUserTO.setId(-2);
authUserTO.setUsername(anonymousUser);
} else if (adminUser.equals(authUsername)) {
authUserTO = new UserTO();
authUserTO.setId(-1);
authUserTO.setUsername(adminUser);
} else {
SyncopeUser authUser = userDAO.find(authUsername);
authUserTO = getUserTO(authUser);
}
return authUserTO;
}
@Transactional(readOnly = true)
public boolean verifyPassword(final String username, final String password) {
return verifyPassword(getUserFromUsername(username), password);
}
@Transactional(readOnly = true)
public boolean verifyPassword(final SyncopeUser user, final String password) {
return encryptor.verify(password, user.getCipherAlgorithm(), user.getPassword());
}
@Transactional(readOnly = true)
public SyncopeUser getUserFromUsername(final String username) {
if (username == null) {
throw new NotFoundException("Null username");
}
SyncopeUser user = userDAO.find(username);
if (user == null) {
throw new NotFoundException("User " + username);
}
Set<Long> roleIds = user.getRoleIds();
Set<Long> adminRoleIds = EntitlementUtil.getRoleIds(EntitlementUtil.getOwnedEntitlementNames());
roleIds.removeAll(adminRoleIds);
if (!roleIds.isEmpty()) {
throw new UnauthorizedRoleException(roleIds);
}
return user;
}
/**
* Get predefined password cipher algorithm from SyncopeConf.
*
* @return cipher algorithm.
*/
private CipherAlgorithm getPredefinedCipherAlgoritm() {
final String algorithm = confDAO.find(
"password.cipher.algorithm", CipherAlgorithm.AES.name()).getValues().get(0).getStringValue();
try {
return CipherAlgorithm.valueOf(algorithm);
} catch (IllegalArgumentException e) {
throw new NotFoundException("Cipher algorithm " + algorithm);
}
}
private void setPassword(final SyncopeUser user, final String password,
final SyncopeClientCompositeException scce) {
int passwordHistorySize = 0;
PasswordPolicy policy = policyDAO.getGlobalPasswordPolicy();
if (policy != null && policy.getSpecification(PasswordPolicySpec.class) != null) {
passwordHistorySize = policy.getSpecification(PasswordPolicySpec.class).getHistoryLength();
}
try {
user.setPassword(password, getPredefinedCipherAlgoritm(), passwordHistorySize);
} catch (NotFoundException e) {
final SyncopeClientException invalidCiperAlgorithm =
SyncopeClientException.build(ClientExceptionType.NotFound);
invalidCiperAlgorithm.getElements().add(e.getMessage());
scce.addException(invalidCiperAlgorithm);
throw scce;
}
}
public void create(final SyncopeUser user, final UserTO userTO, final boolean storePassword) {
SyncopeClientCompositeException scce = SyncopeClientException.buildComposite();
// memberships
SyncopeRole role;
for (MembershipTO membershipTO : userTO.getMemberships()) {
role = roleDAO.find(membershipTO.getRoleId());
if (role == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Ignoring invalid role " + membershipTO.getRoleName());
}
} else {
Membership membership = null;
if (user.getId() != null) {
membership = user.getMembership(role.getId()) == null
? membershipDAO.find(user, role)
: user.getMembership(role.getId());
}
if (membership == null) {
membership = new Membership();
membership.setSyncopeRole(role);
membership.setSyncopeUser(user);
user.addMembership(membership);
}
fill(membership, membershipTO, AttributableUtil.getInstance(AttributableType.MEMBERSHIP), scce);
}
}
// attributes, derived attributes, virtual attributes and resources
fill(user, userTO, AttributableUtil.getInstance(AttributableType.USER), scce);
// set password
if (StringUtils.isBlank(userTO.getPassword()) || !storePassword) {
LOG.debug("Password was not provided or not required to be stored");
} else {
setPassword(user, userTO.getPassword(), scce);
}
// set username
user.setUsername(userTO.getUsername());
}
/**
* Update user, given UserMod.
*
* @param toBeUpdated user to be updated
* @param userMod bean containing update request
* @return updated user + propagation by resource
* @see PropagationByResource
*/
public PropagationByResource update(final SyncopeUser toBeUpdated, final UserMod userMod) {
// Re-merge any pending change from workflow tasks
SyncopeUser user = userDAO.save(toBeUpdated);
PropagationByResource propByRes = new PropagationByResource();
SyncopeClientCompositeException scce = SyncopeClientException.buildComposite();
Set<String> currentResources = user.getResourceNames();
// password
if (StringUtils.isNotBlank(userMod.getPassword())) {
setPassword(user, userMod.getPassword(), scce);
user.setChangePwdDate(new Date());
propByRes.addAll(ResourceOperation.UPDATE, currentResources);
}
// username
if (userMod.getUsername() != null && !userMod.getUsername().equals(user.getUsername())) {
String oldUsername = user.getUsername();
user.setUsername(userMod.getUsername());
propByRes.addAll(ResourceOperation.UPDATE, currentResources);
for (ExternalResource resource : user.getResources()) {
for (AbstractMappingItem mapItem : resource.getUmapping().getItems()) {
if (mapItem.isAccountid() && mapItem.getIntMappingType() == IntMappingType.Username) {
propByRes.addOldAccountId(resource.getName(), oldUsername);
}
}
}
}
// attributes, derived attributes, virtual attributes and resources
propByRes.merge(fill(user, userMod, AttributableUtil.getInstance(AttributableType.USER), scce));
// store the role ids of membership required to be added
Set<Long> membershipToBeAddedRoleIds = new HashSet<Long>();
for (MembershipMod membToBeAdded : userMod.getMembershipsToAdd()) {
membershipToBeAddedRoleIds.add(membToBeAdded.getRole());
}
final Set<String> toBeDeprovisioned = new HashSet<String>();
final Set<String> toBeProvisioned = new HashSet<String>();
// memberships to be removed
for (Long membershipId : userMod.getMembershipsToRemove()) {
LOG.debug("Membership to be removed: {}", membershipId);
Membership membership = membershipDAO.find(membershipId);
if (membership == null) {
LOG.debug("Invalid membership id specified to be removed: {}", membershipId);
} else {
if (!membershipToBeAddedRoleIds.contains(membership.getSyncopeRole().getId())) {
toBeDeprovisioned.addAll(membership.getSyncopeRole().getResourceNames());
}
// In order to make the removeMembership() below to work,
// we need to be sure to take exactly the same membership
// of the user object currently in memory (which has potentially
// some modifications compared to the one stored in the DB
membership = user.getMembership(membership.getSyncopeRole().getId());
if (membershipToBeAddedRoleIds.contains(membership.getSyncopeRole().getId())) {
Set<Long> attributeIds = new HashSet<Long>(membership.getAttrs().size());
for (AbstractAttr attribute : membership.getAttrs()) {
attributeIds.add(attribute.getId());
}
for (Long attributeId : attributeIds) {
attrDAO.delete(attributeId, MAttr.class);
}
attributeIds.clear();
// remove derived attributes
for (AbstractDerAttr derAttr : membership.getDerAttrs()) {
attributeIds.add(derAttr.getId());
}
for (Long derAttrId : attributeIds) {
derAttrDAO.delete(derAttrId, MDerAttr.class);
}
attributeIds.clear();
// remove virtual attributes
for (AbstractVirAttr virAttr : membership.getVirAttrs()) {
attributeIds.add(virAttr.getId());
}
for (Long virAttrId : attributeIds) {
virAttrDAO.delete(virAttrId, MVirAttr.class);
}
attributeIds.clear();
} else {
user.removeMembership(membership);
membershipDAO.delete(membershipId);
}
}
}
// memberships to be added
for (MembershipMod membershipMod : userMod.getMembershipsToAdd()) {
LOG.debug("Membership to be added: role({})", membershipMod.getRole());
SyncopeRole role = roleDAO.find(membershipMod.getRole());
if (role == null) {
LOG.debug("Ignoring invalid role {}", membershipMod.getRole());
} else {
Membership membership = user.getMembership(role.getId());
if (membership == null) {
membership = new Membership();
membership.setSyncopeRole(role);
membership.setSyncopeUser(user);
user.addMembership(membership);
toBeProvisioned.addAll(role.getResourceNames());
}
propByRes.merge(fill(membership, membershipMod,
AttributableUtil.getInstance(AttributableType.MEMBERSHIP), scce));
}
}
propByRes.addAll(ResourceOperation.DELETE, toBeDeprovisioned);
propByRes.addAll(ResourceOperation.UPDATE, toBeProvisioned);
/**
* In case of new memberships all the current resources have to be updated in order to propagate new role and
* membership attribute values.
*/
if (!toBeDeprovisioned.isEmpty() || !toBeProvisioned.isEmpty()) {
currentResources.removeAll(toBeDeprovisioned);
propByRes.addAll(ResourceOperation.UPDATE, currentResources);
}
return propByRes;
}
@Transactional(readOnly = true)
public UserTO getUserTO(final SyncopeUser user) {
UserTO userTO = new UserTO();
BeanUtils.copyProperties(user, userTO, IGNORE_USER_PROPERTIES);
connObjectUtil.retrieveVirAttrValues(user, AttributableUtil.getInstance(AttributableType.USER));
fillTO(userTO, user.getAttrs(), user.getDerAttrs(), user.getVirAttrs(), user.getResources());
MembershipTO membershipTO;
for (Membership membership : user.getMemberships()) {
membershipTO = new MembershipTO();
// set sys info
membershipTO.setCreator(membership.getCreator());
membershipTO.setCreationDate(membership.getCreationDate());
membershipTO.setLastModifier(membership.getLastModifier());
membershipTO.setLastChangeDate(membership.getLastChangeDate());
membershipTO.setId(membership.getId());
membershipTO.setRoleId(membership.getSyncopeRole().getId());
membershipTO.setRoleName(membership.getSyncopeRole().getName());
// SYNCOPE-458 retrieve also membership virtual attributes
connObjectUtil.retrieveVirAttrValues(membership, AttributableUtil.getInstance(AttributableType.MEMBERSHIP));
fillTO(membershipTO,
membership.getAttrs(), membership.getDerAttrs(), membership.getVirAttrs(),
Collections.<ExternalResource>emptyList());
userTO.getMemberships().add(membershipTO);
}
return userTO;
}
@Transactional(readOnly = true)
public UserTO getUserTO(final String username) {
return getUserTO(getUserFromUsername(username));
}
@Transactional(readOnly = true)
public UserTO getUserTO(final Long userId) {
return getUserTO(getUserFromId(userId));
}
/**
* SYNCOPE-459: build virtual attribute changes in case no other changes were made.
*
* @param id user id
* @param vAttrsToBeRemoved virtual attributes to be removed.
* @param vAttrsToBeUpdated virtual attributes to be updated.
* @return operations to be performed on external resources for virtual attributes changes
*/
public PropagationByResource fillVirtual(
final Long id, final Set<String> vAttrsToBeRemoved, final Set<AttributeMod> vAttrsToBeUpdated) {
return fillVirtual(
getUserFromId(id),
vAttrsToBeRemoved,
vAttrsToBeUpdated,
AttributableUtil.getInstance(AttributableType.USER));
}
/**
* SYNCOPE-501: build membership virtual attribute changes in case no other changes were made.
*
* @param userId user id
* @param roleId role id
* @param membershipId membership id
* @param vAttrsToBeRemoved virtual attributes to be removed.
* @param vAttrsToBeUpdated virtual attributes to be updated.
* @param isRemoval flag to check if fill is on removed or added membership
* @return operations to be performed on external resources for membership virtual attributes changes
*/
public PropagationByResource fillMembershipVirtual(
final Long userId, final Long roleId, final Long membershipId, final Set<String> vAttrsToBeRemoved,
final Set<AttributeMod> vAttrsToBeUpdated, final boolean isRemoval) {
final Membership membership = membershipId == null
? getUserFromId(userId).getMembership(roleId)
: getMembershipFromId(membershipId);
return membership == null ? new PropagationByResource() : isRemoval
? fillVirtual(
membership,
membership.getVirAttrs() == null
? Collections.<String>emptySet()
: getAttributeNames(membership.getVirAttrs()),
vAttrsToBeUpdated,
AttributableUtil.getInstance(AttributableType.MEMBERSHIP))
: fillVirtual(
membership,
vAttrsToBeRemoved,
vAttrsToBeUpdated,
AttributableUtil.getInstance(AttributableType.MEMBERSHIP));
}
private Set<String> getAttributeNames(final List<? extends AbstractVirAttr> virAttrs) {
final Set<String> virAttrNames = new HashSet<String>();
for (AbstractVirAttr attr : virAttrs) {
virAttrNames.add(attr.getSchema().getName());
}
return virAttrNames;
}
}