UserServiceImpl.java

/*
 * Copyright 2018 Global Crop Diversity Trust
 *
 * Licensed 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.genesys.server.service.impl;

import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.Period;
import java.time.temporal.ChronoField;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;

import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;

import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.validator.routines.EmailValidator;
import org.genesys.blocks.security.NoUserFoundException;
import org.genesys.blocks.security.NotUniqueUserException;
import org.genesys.blocks.security.SecurityContextUtil;
import org.genesys.blocks.security.UserException;
import org.genesys.blocks.security.model.BasicUser.AccountType;
import org.genesys.blocks.security.service.BasicUserService;
import org.genesys.blocks.security.service.CustomAclService;
import org.genesys.blocks.security.service.PasswordPolicy.PasswordPolicyException;
import org.genesys.blocks.security.service.impl.BasicUserServiceImpl;
import org.genesys.server.exception.NotFoundElement;
import org.genesys.server.model.Partner;
import org.genesys.server.model.QPartner;
import org.genesys.server.model.UserRole;
import org.genesys.server.model.impl.QUser;
import org.genesys.server.model.impl.User;
import org.genesys.server.persistence.PartnerRepository;
import org.genesys.server.persistence.UserRepository;
import org.genesys.server.service.ContentService;
import org.genesys.server.service.EMailService;
import org.genesys.server.service.UserService;
import org.genesys.server.service.filter.UserFilter;
import org.genesys.spring.TransactionHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.CacheManager;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.querydsl.core.BooleanBuilder;

import lombok.extern.slf4j.Slf4j;

@Service(value = "userService")
@Transactional(readOnly = true)
@Slf4j
public class UserServiceImpl extends BasicUserServiceImpl<UserRole, User> implements UserService {

	/// A non-password used for system and Google accounts
	private static final String THIS_IS_NOT_A_PASSWORD = "THIS-IS-NOT-A-PASSWORD";

	@Autowired
	private UserRepository userRepository;

	private final List<UserRole> availableRoles = ListUtils.unmodifiableList(Arrays.asList(UserRole.values()));

	private EmailValidator emailValidator = EmailValidator.getInstance();

	@Autowired
	private CustomAclService aclService;

	@Autowired
	private PartnerRepository partnerRepository;

	@Autowired
	private ContentService contentService;
	
	@Autowired
	private EMailService eMailService;

	@Value("${frontend.url}")
	private String frontendUrl;
	
	@Value("${base.url}")
	private String apiUrl;

	@Autowired
	private EntityManager entityManager;

	/** The cache manager. */
	@Autowired(required = false)
	private CacheManager cacheManager;

	@Override
	public void afterPropertiesSet() throws Exception {
		super.afterPropertiesSet();
		try {
			asAdmin(() -> {
				// Ensure system-wide user roles are defined
				for (UserRole systemRole : UserRole.values()) {
					log.warn("Ensuring SID for role {}", systemRole.getAuthority());
					aclService.ensureAuthoritySid(systemRole.getAuthority());
				}
				return true;
			});
		} catch (Throwable e) {
			log.warn("Could not add system-wide roles: {}", e.getMessage());
		}
	}

	private <T> T asAdmin(Callable<T> callable) throws Exception {
		UserDetails administrator = loadUserByUsername(BasicUserService.SYSTEM_ADMIN);
		List<GrantedAuthority> authorities = Lists.newArrayList(UserRole.ADMINISTRATOR);
		authorities.addAll(administrator.getAuthorities());
		Authentication authentication = new UsernamePasswordAuthenticationToken(administrator, null, authorities);
		return TransactionHelper.asUser(authentication, callable);
	}

	@Override
	public List<UserRole> listAvailableRoles() {
		return availableRoles;
	}

	@Override
	protected JpaRepository<User, Long> getUserRepository() {
		return userRepository;
	}

	@Override
	protected User createSystemAdministrator(String username) throws UserException {
		final User admin = createUser(username, "System Administrator", null, AccountType.SYSTEM);
		setRoles(admin, Sets.newHashSet(UserRole.ADMINISTRATOR));
		return admin;
	}

	@Override
	public List<UserRole> getDefaultUserRoles() {
		return List.of(UserRole.USER, UserRole.EVERYONE);
	}
	
	@Override
	@Transactional
	public User createUser(String email, String fullName, String password, AccountType accountType) throws UserException {
		log.info("Creating user email={} fullName={}", email, fullName);
		final User user = new User();
		user.setEmail(email);
		user.setFullName(fullName);
		user.setAccountType(accountType);
		super.setPassword(user, password);
		return deepLoad(userRepository.save(user));
	}

	@Override
	@Transactional
	@Cacheable(cacheNames = { "userDetails" }, key = "#username", unless = "#result == null")
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		log.warn("Loading user details {}", username);
		final User user = getUserByEmail(username);

		if (user == null) {
			log.debug("User does not exist: {}", username);
			throw new UsernameNotFoundException(username);
		}

		final boolean enabled = user.isEnabled();
		final boolean accountNonLocked = !user.isAccountLocked();

		if (!accountNonLocked) {
			log.warn("Account is locked for user={} until={}", user.getEmail(), user.getLockedUntil());
		}
		if (!enabled) {
			log.warn("Account is disabled for user={}", user.getEmail());
		}

		if (user.getLockedUntil() != null && accountNonLocked) {
			log.warn("Account can be unlocked for user={}", user.getEmail());
			user.setLockedUntil(null);
			userRepository.save(user);
		}

		/*
		 * Order of authorities is extremely important in ACL!
		 * First the dynamic runtime authorities are added, then all the default authorities. 
		 */
		var runtimeAuthorities = new LinkedHashSet<GrantedAuthority>(20);
		runtimeAuthorities.addAll(getDynamicAuthorities(user));
		runtimeAuthorities.addAll(user.getRoles());
		getDefaultUserRoles().forEach(runtimeAuthorities::remove); // Remove them so they are added to the end
		runtimeAuthorities.addAll(getDefaultUserRoles());

		user.setRuntimeAuthorities(new ArrayList<>(runtimeAuthorities));

		return user;
	}

	@Override
	// FIXME Re-enable this
	// @PreAuthorize("hasRole('ADMINISTRATOR')")
	public Page<User> listUsers(Pageable pageable) {
		return userRepository.findAll(pageable);
	}


	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') || principal.id == #user.id")
	public User updateUser(User user, String email, String fullName) throws UserException {

		if (!emailValidator.isValid(email)) {
			log.warn("Invalid email provided: {}", email);
			throw new UserException("Invalid email provided: " + email);
		}

		evictFromCache(user);
		return super.updateUser(user, email, fullName);
	}

	protected User updateUser(User user) throws UserException {
		try {
			evictFromCache(user);
			return userRepository.save(user);
		} catch (final DataIntegrityViolationException e) {
			throw new NotUniqueUserException(e, user.getEmail());
		} catch (final EmptyResultDataAccessException e) {
			throw new NoUserFoundException(e, user.getId());
		} catch (final RuntimeException e) {
			throw new UserException(e);
		}
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	public void setAccountActive(UUID uuid, boolean enabled) throws UserException {
		final User user = userRepository.findByUuid(uuid.toString());
		if (user == null) {
			throw new NoUserFoundException("Record not found by UUID=" + uuid);
		}
		if (!enabled && user.getRoles().contains(UserRole.ADMINISTRATOR)) {
			throw new SecurityException("Can't disable ADMINISTRATOR accounts");
		}
		user.setActive(enabled);
		updateUser(user);
		log.warn("User account for user={} enabled={}", user.getEmail(), enabled);
	}

	/**
	 * Checks if the passwords are equal.
	 *
	 * @param rawPassword password entered by user
	 * @param encodedPassword current encoded user's password
	 * @return true if passwords are equal
	 */
	@Override
	public boolean checkPasswordsMatch(String rawPassword, String encodedPassword) {
		return passwordEncoder.matches(rawPassword, encodedPassword);
	}

	@Override
	public User getMe() {
		final User user = userRepository.findById(SecurityContextUtil.getCurrentUser().getId()).orElse(null);

		if (user != null) {
			user.getRoles().size();
		}

		return user;
	}

	@Override
	public User getUserByEmail(String email) {
		final User user = userRepository.findByEmail(email);
		return deepLoad(user);
	}
	
	/**
	 * This allows us to add dynamic authorities, e.g. membership in a Partner.
	 */
	@Override
	protected Set<GrantedAuthority> getDynamicAuthorities(User user) {

		// Has role on Partner
		final HashSet<Long> partnersIds = new HashSet<>();
		partnersIds.addAll(aclService.listObjectIdentityIdsForSid(Partner.class, user, BasePermission.WRITE));
		partnersIds.addAll(aclService.listObjectIdentityIdsForSid(Partner.class, user, BasePermission.CREATE));

		final Set<GrantedAuthority> newAuthorities = new HashSet<>(partnersIds.size());
		partnerRepository.findAll(QPartner.partner.id.in(partnersIds)).forEach(partner -> {
			newAuthorities.add(new SimpleGrantedAuthority(partner.getAuthorityName()));
			log.debug("Adding partner SID {}", partner.getAuthorityName());
		});

		return newAuthorities;
	}

	@Override
	public User getUser(UUID uuid) {
		final User user = userRepository.findByUuid(uuid.toString());
		return deepLoad(user);
	}

	@Override
	public User getUser(UUID uuid, Integer version) throws NoUserFoundException, ConcurrencyFailureException {
		final User user = getUser(uuid);

		if (user == null) {
			throw new NoUserFoundException("Record not found by uuid=" + uuid);
		}

		if (version != null && !user.getVersion().equals(version)) {
			log.warn("Record versions don't match anymore");
			throw new ConcurrencyFailureException("Object version changed to " + user.getVersion() + ", you provided " + version);
		}
		return user;
	}

	@Override
	public boolean exists(String username) {
		return userRepository.findByEmail(username) != null;
	}

	@Override
	@Transactional
	public void userEmailValidated(UUID uuid) {
		log.info("Validating user email for uuid={}", uuid);

		final User user = userRepository.findByUuid(uuid.toString());
		Set<UserRole> userRoles = user.getRoles();
		if (userRoles == null) {
			log.debug("User roles are null, creating role set");
			user.setRoles(userRoles = new HashSet<UserRole>());
		}
		// Since it's a set, we can just add
		userRoles.add(UserRole.VALIDATEDUSER);

		try {
			updateUser(user);
			addRoleToCurrentUser(user, UserRole.VALIDATEDUSER.getName());
			log.info("Ensured VALIDATEDUSER role for user {}", user);
		} catch (final UserException e) {

		}
	}

	@Override
	@Transactional
	public void addVettedUserRole(UUID uuid) {
		final User user = userRepository.findByUuid(uuid.toString());
		final Set<UserRole> userRoles = user.getRoles();
		userRoles.add(UserRole.VETTEDUSER);

		try {
			updateUser(user);
			addRoleToCurrentUser(user, UserRole.VETTEDUSER.getName());
			log.info("Add role VETTEDUSER for user {}", user);
		} catch (final UserException e) {
			log.error(e.getMessage(), e);
		}

	}

	private void addRoleToCurrentUser(User user, String role) {
		final Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

		if (principal instanceof User) {
			if (!((User) principal).getId().equals(user.getId())) {
				log.warn("Not adding role, user != principal");
				return;
			}
		} else {
			log.warn("Not adding role to current principal: {}", principal);
			return;
		}

		final List<GrantedAuthority> authorities = new ArrayList<>(SecurityContextHolder.getContext().getAuthentication().getAuthorities());
		for (final GrantedAuthority authority : authorities) {
			if (authority.getAuthority().equals(role)) {
				return;
			}
		}
		final SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role);
		authorities.add(0, simpleGrantedAuthority); // Insert authority to top of the list

		final Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal, authorities);
		SecurityContextHolder.getContext().setAuthentication(authentication);
	}

	@Override
	public List<User> autocompleteUser(String email) {
		if (StringUtils.isBlank(email) || email.length() < 4)
			return Collections.emptyList();
		return userRepository.autocompleteByEmail(email + "%", PageRequest.of(0, 10, Sort.by("email")));
	}
	
	@Override
	@PreAuthorize("isAuthenticated()")
	public List<User> autocompleteUser(final String email, final int limit) {
		if (StringUtils.isBlank(email) || email.length() < 1) {
			return Collections.emptyList();
		}
		return userRepository.autocomplete(email, PageRequest.of(0, Integer.min(100, limit), Sort.by("email")));
	}


	@Override
	@Transactional
	public User setAccountType(User user, AccountType accountType) {
		User u = userRepository.findById(user.getId()).orElseThrow(() -> new EntityNotFoundException("User not found."));
		u.setAccountType(accountType);

		if (accountType != AccountType.LOCAL) {
			user.setPassword(THIS_IS_NOT_A_PASSWORD);
		}

		evictFromCache(u);
		return userRepository.save(u);
	}

	/**
	 * Set FTP application password for the user.
	 */
	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') || (hasRole('VETTEDUSER') && principal.id == #user.id)")
	public void setFtpPassword(final User user, final String ftpPassword) throws PasswordPolicyException {
		assureGoodPassword(ftpPassword);
		user.setFtpPassword(passwordEncoder.encode(ftpPassword));
		evictFromCache(user);
		userRepository.save(user);
	}

	@Override
	@Transactional
	@PreAuthorize("isFullyAuthenticated()")
	public void disableMyAccount() throws UserException {
		User currentUser = SecurityContextUtil.getMe();

		User u = userRepository.findById(currentUser.getId()).orElseThrow(() -> new EntityNotFoundException("User not found."));
	
		if (u.hasRole(UserRole.ADMINISTRATOR.getName())) {
			throw new UserException("Refusing to disable active administrator account");
		}
		
		Calendar expires = Calendar.getInstance();
		expires.add(Calendar.MONTH, 1);
		u.setAccountExpires(expires.toInstant());
		u.setActive(false);
		// u.setAccountType(AccountType.DELETED);

		evictFromCache(u);
		userRepository.save(u);
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	public User extendAccount(User user) {
		Calendar expires = Calendar.getInstance();
		expires.add(Calendar.MONTH, 3);

		if (user.getAccountExpires() == null || user.getAccountExpires().isBefore(expires.toInstant())) {
			user.setAccountExpires(expires.toInstant());
			user.setActive(true);
			evictFromCache(user);
			return deepLoad(userRepository.save(user));
		}
		
		return deepLoad(user);
	}

	/* (non-Javadoc)
	 * @see org.genesys.server.service.UserService#archiveUser(org.genesys.server.model.impl.User)
	 */
	@Override
	@Transactional
	public User archiveUser(User user) throws UserException {
		user = userRepository.findById(user.getId()).orElseThrow(NoUserFoundException::new);
		
		if (user.hasRole(UserRole.ADMINISTRATOR.getName())) {
			throw new UserException("Refusing to disable active administrator account");
		}
		
		log.warn("Archiving user {}", user.getEmail());
		Instant now = Instant.now();
		user.setAccountExpires(now);
		user.setActive(false);
		user.setAccountType(AccountType.LOCAL);
//		 user.setAccountType(AccountType.DELETED);
		user.setEmail("deleted@" + now.getEpochSecond() + now.get(ChronoField.MILLI_OF_SECOND));
		user.setPassword(THIS_IS_NOT_A_PASSWORD);
		user.setFtpPassword(null);
		user.setFullName("USER ACCOUNT DELETED");
		user.setShortName("deleted" + now.getEpochSecond() + now.get(ChronoField.MILLI_OF_SECOND));
		user.setPasswordExpires(now);
		user.getRoles().clear();

		updateUser(user);

		log.warn("Removing ACL entries for {}", user.getEmail());
		aclEntryRepository.deleteAll(user.getAclEntries());
		return user;
	}

	/* (non-Javadoc)
	 * @see org.genesys.server.service.UserService#list(org.genesys.server.service.filter.UserFilter, org.springframework.data.domain.Pageable)
	 */
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	@Override
	public Page<User> list(UserFilter filter, Pageable page) {
		Page<User> res = userRepository.findAll(new BooleanBuilder().and(filter.buildPredicate()), page);
		return new PageImpl<>(res.getContent(), page, res.getTotalElements());
	}

	@PreAuthorize("hasRole('ADMINISTRATOR')")
	@Override
	public void sendEmail(Set<UUID> userUuids, String template, String subject) {
		var uuids = userUuids.stream().map(UUID::toString).collect(Collectors.toSet());
		var users = userRepository.findAll(QUser.user.uuid.in(uuids));

		Map<String, Object> root = new HashMap<>();
		root.put("frontendUrl", frontendUrl);
		root.put("apiUrl", apiUrl);
		for (User user : users) {
			root.put("user", user);
			var messageBody = contentService.processTemplate(template, root);
			eMailService.sendMail(subject, messageBody, user.getEmail());
		}
	}

	@Override
	public User loadOrRegisterUser(OidcUser oidcUser, String provider) {
		String email = oidcUser.getEmail();

		if (StringUtils.isBlank(email)) {
			throw new UsernameNotFoundException("Provided email is blank");
		}

		User user = getUserByEmail(email);

		if (user != null) {
			if (user.getAccountType() == AccountType.LOCAL) {
				// Change to OAuth
				try {
					var updatedUser = user;
					updatedUser.setAccountType(AccountType.valueOf(provider.toUpperCase()));
					var password = "INV" + passwordEncoder.encode(RandomStringUtils.random(32, true, true));
					updatedUser.setPassword(password);
					user = asAdmin(() -> TransactionHelper.executeInTransaction(false, () -> updateUser(updatedUser)));

				} catch (Exception e) {
					log.warn("Error upgrading local account for {} to {}: {}", email, provider.toUpperCase(), e.getMessage(), e);
					throw new AuthenticationServiceException("Could not upgrade to OAuth login", e);
				}

			} else if (!StringUtils.equalsIgnoreCase(user.getAccountType().name(), provider)) {
				throw new AuthenticationServiceException("You must authenticate with ".concat(user.getAccountType().name()));
			}

		} else {
			try {
				user = asAdmin(() -> TransactionHelper.executeInTransaction(false, () -> {
					var password = "INV" + passwordEncoder.encode(RandomStringUtils.random(32, true, true));
					return createUser(email, oidcUser.getFullName(), password, AccountType.valueOf(provider.toUpperCase()));
				}));
			} catch (Exception e) {
				log.warn("Error creating a local account for {}: {}", email, e.getMessage(), e);
				throw new AuthenticationServiceException("Could not register user", e);
			}
		}

		deepLoad(user);
		/*
		 * Order of authorities is extremely important in ACL!
		 * First the dynamic runtime authorities are added, then all the default authorities. 
		 */
		var runtimeAuthorities = new LinkedHashSet<GrantedAuthority>(20);
		runtimeAuthorities.addAll(getDynamicAuthorities(user));
		runtimeAuthorities.addAll(user.getRoles());
		runtimeAuthorities.removeAll(getDefaultUserRoles()); // Remove them so they are added to the end
		runtimeAuthorities.addAll(getDefaultUserRoles());

		user.setRuntimeAuthorities(new ArrayList<>(runtimeAuthorities));

		entityManager.detach(user);

		return user;
	}

	@Override
	@Transactional
	public void updateLastLogin(String userName) throws NoUserFoundException {
		User u = getUserByEmail(userName);
		if (u == null) {
			throw new NoUserFoundException("No such user.");
		}

		log.warn("Updating last login timestamp for {}", userName);
		// Set account to expire 1 year after last login
		userRepository.updateLastLogin(u, Instant.now(), OffsetDateTime.now().plus(Period.ofYears(1)).toInstant());
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	public void setAccountLock(long userId, boolean locked) throws NoUserFoundException {
		evictFromCache(getUser(userId));
		super.setAccountLock(userId, locked);
	}

	@Override
	@Transactional
	public void setAccountLockLocal(long userId, boolean locked) throws NoUserFoundException {
		evictFromCache(getUser(userId));
		super.setAccountLockLocal(userId, locked);
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') || principal.id == #user.id")
	public User changePassword(User user, String password) throws PasswordPolicyException {
		evictFromCache(user);
		return super.changePassword(user, password);
	}

	@Override
	@Transactional
	public User setRoles(User user, Set<UserRole> newRoles) {
		evictFromCache(user);
		return super.setRoles(user, newRoles);
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	public void deleteUser(User user) {
		evictFromCache(user);
		super.deleteUser(user);
	}

	@Override
	@Transactional
	public User updateUserPreferences(UserPreferences preferences) throws UserException {
		var user = getMe();
		if (user == null) {
			throw new NoUserFoundException();
		}
		if (preferences.language != null) {
			user.setLanguage(preferences.language);
		}
		if (preferences.timezone != null) {
			user.setTimezone(preferences.timezone);
		}
		user = userRepository.saveAndFlush(user);
		evictFromCache(user);
		entityManager.detach(user);
		return user;
	}

	@Override
	@Transactional
	public User updateUserPreference(String setting, boolean value) throws UserException {
		var user = getMe();
		if (user == null) {
			throw new NoUserFoundException();
		}
		Instant instant = Instant.now();
		if ("aiOptin".equals(setting)) {
			user.setAiOptin(value);
			user.setAiDate(instant);
		} else if ("newsletterOptin".equals(setting)) {
			user.setNewsletterOptin(value);
			user.setNewsletterDate(instant);
		} else {
			throw new NotFoundElement("Setting \"" + setting + "\" not found.");
		}
		user = userRepository.saveAndFlush(user);
		evictFromCache(user);
		entityManager.detach(user);
		return user;
	}

	private void evictFromCache(final User user) {
		if (user != null && user.getUsername() != null && cacheManager != null) {
			var cache = cacheManager.getCache("userDetails");
			if (cache != null) cache.evict(user.getUsername());
		}
	}
}