/* * This is eMonocot, a global online biodiversity information resource. * * Copyright © 2011–2015 The Board of Trustees of the Royal Botanic Gardens, Kew and The University of Oxford * * eMonocot is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * eMonocot is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * The complete text of the GNU Affero General Public License is in the source repository as the file * ‘COPYING’. It is also available from <http://www.gnu.org/licenses/>. */ package org.emonocot.service.impl; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.UUID; import org.apache.tika.Tika; import org.apache.tika.metadata.Metadata; import org.apache.tika.parser.AutoDetectParser; import org.emonocot.api.UserService; import org.emonocot.model.SecuredObject; import org.emonocot.model.auth.Group; import org.emonocot.model.auth.User; import org.emonocot.persistence.dao.AclService; import org.emonocot.persistence.dao.GroupDao; import org.emonocot.persistence.dao.UserDao; import org.hibernate.NonUniqueResultException; import org.im4java.core.ConvertCmd; import org.im4java.core.IM4JavaException; import org.im4java.core.IMOperation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.FileSystemResource; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.orm.ObjectRetrievalFailureException; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.acls.domain.ObjectIdentityImpl; import org.springframework.security.acls.domain.PrincipalSid; import org.springframework.security.acls.model.AccessControlEntry; import org.springframework.security.acls.model.Acl; import org.springframework.security.acls.model.MutableAcl; import org.springframework.security.acls.model.NotFoundException; import org.springframework.security.acls.model.ObjectIdentity; import org.springframework.security.acls.model.Permission; import org.springframework.security.acls.model.Sid; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.ReflectionSaltSource; import org.springframework.security.authentication.dao.SaltSource; import org.springframework.security.authentication.encoding.Md5PasswordEncoder; import org.springframework.security.authentication.encoding.PasswordEncoder; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserCache; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.cache.NullUserCache; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; import org.springframework.web.multipart.MultipartFile; /** * * @author ben * */ @Service public class UserServiceImpl extends SearchableServiceImpl<User, UserDao> implements UserService { private final Double THUMBNAIL_DIMENSION = 100D; private static Logger logger = LoggerFactory.getLogger(UserServiceImpl.class); private GroupDao groupDao; private AclService aclService; private SaltSource saltSource; private PasswordEncoder passwordEncoder; private AuthenticationManager authenticationManager; private UserCache userCache; private String searchPath; private FileSystemResource temporaryFolder; private FileSystemResource userProfilesFolder; private Tika tika = new Tika(); public UserServiceImpl() { saltSource = new ReflectionSaltSource(); ((ReflectionSaltSource) saltSource).setUserPropertyToUse("getUsername"); passwordEncoder = new Md5PasswordEncoder(); userCache = new NullUserCache(); } /** * * @param aclService * Set the acl service */ @Autowired(required = false) public final void setAclService(final AclService aclService) { this.aclService = aclService; } /** * * @param userCache * Set the user cache */ @Autowired(required = false) public final void setUserCache(final UserCache userCache) { Assert.notNull(userCache, "userCache cannot be null"); this.userCache = userCache; } /** * * @param passwordEncoder Set the password encoder */ @Autowired(required = false) public final void setPasswordEncoder(final PasswordEncoder passwordEncoder) { this.passwordEncoder = passwordEncoder; } /** * * @param saltSource Set the salt source */ @Autowired(required = false) public final void setSaltSource(final SaltSource saltSource) { this.saltSource = saltSource; } /** * * @param authenticationManager Set the authentication manager */ @Autowired(required = false) public final void setAuthenticationManager(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } /** * * @param userDao Set the user dao */ @Autowired public final void setUserDao(final UserDao userDao) { this.dao = userDao; } /** * * @param groupDao Set the group dao */ @Autowired public final void setGroupDao(final GroupDao groupDao) { this.groupDao = groupDao; } /** * * @param currentAuth Set the current authentication * @param newPassword Set the new password * @return return the new authentication */ @Transactional(readOnly = false) protected final Authentication createNewAuthentication( final Authentication currentAuth, final String newPassword) { UserDetails user = loadUserByUsername(currentAuth.getName()); UsernamePasswordAuthenticationToken newAuthentication = new UsernamePasswordAuthenticationToken( user, user.getPassword(), user.getAuthorities()); newAuthentication.setDetails(currentAuth.getDetails()); return newAuthentication; } /** * @param oldPassword Set the old password * @param newPassword Set the new password */ @Transactional(readOnly = false) public final void changePassword(final String oldPassword, final String newPassword) { Assert.hasText(oldPassword); Assert.hasText(newPassword); Authentication authentication = SecurityContextHolder.getContext() .getAuthentication(); if (authentication != null && authentication.getPrincipal() != null && authentication.getPrincipal() instanceof User) { User user = (User) authentication.getPrincipal(); authenticationManager .authenticate(new UsernamePasswordAuthenticationToken(user .getUsername(), oldPassword)); Object salt = this.saltSource.getSalt(user); String password = passwordEncoder.encodePassword(newPassword, salt); ((User) user).setPassword(password); dao.update((User) user); SecurityContextHolder.getContext().setAuthentication( createNewAuthentication(authentication, newPassword)); userCache.removeUserFromCache(user.getUsername()); } else { throw new AccessDeniedException( "Can't change password as no Authentication object found in context for current user."); } } /** * * @param username Set the username * @param newPassword Set the new password */ @Transactional(readOnly = false) public final void changePasswordForUser(final String username, final String newPassword) { Assert.hasText(username); Assert.hasText(newPassword); try { User user = dao.find(username); if (user == null) { throw new UsernameNotFoundException(username); } Object salt = this.saltSource.getSalt(user); String password = passwordEncoder.encodePassword(newPassword, salt); ((User) user).setPassword(password); dao.update((User) user); userCache.removeUserFromCache(user.getUsername()); } catch (NonUniqueResultException nure) { throw new IncorrectResultSizeDataAccessException( "More than one user found with name '" + username + "'", 1); } } /** * @param user Set the user details */ @Transactional(readOnly = false) public final void createUser(final UserDetails user) { Assert.isInstanceOf(User.class, user); String rawPassword = user.getPassword(); if (rawPassword != null) { Object salt = this.saltSource.getSalt(user); String password = passwordEncoder.encodePassword(rawPassword, salt); ((User) user).setPassword(password); } dao.save((User) user); } @Transactional(readOnly = false) public String createNonce(String username) { User user = dao.find(username); if (user == null) { throw new UsernameNotFoundException(username); } String nonce = UUID.randomUUID().toString(); Object salt = this.saltSource.getSalt(user); String hash = passwordEncoder.encodePassword(nonce, salt); user.setNonce(hash); dao.update(user); return nonce; } @Transactional(readOnly = false) public boolean verifyNonce(String username, String nonce) { User user = dao.find(username); if (user == null) { throw new UsernameNotFoundException(username); } Object salt = this.saltSource.getSalt(user); String hash = passwordEncoder.encodePassword(nonce, salt); boolean verified = user.getNonce() == null ? false : user.getNonce().equals(hash); user.setNonce(null); dao.update(user); return verified; } /** * @param username The username of the user to delete */ @PreAuthorize("hasRole('PERMISSION_ADMINISTRATE') or hasRole('PERMISSION_DELETE_USER')") @Transactional(readOnly = false) public final void deleteUser(final String username) { Assert.hasLength(username); User user = dao.find(username); for(Group group : user.getGroups()) { removeUserFromGroup(username, group.getName()); } if (user != null) { dao.delete(username); } userCache.removeUserFromCache(username); } /** * @param user Set the user to update */ @Transactional(readOnly = false) public final void updateUser(final UserDetails user) { Assert.isInstanceOf(User.class, user); dao.update((User) user); userCache.removeUserFromCache(user.getUsername()); } /** * @param username The username of the user to test for * @return true if the user exists, false otherwise */ @Transactional(readOnly = true) public final boolean userExists(final String username) { Assert.hasText(username); User user = dao.find(username); return user != null; } /** * DO NOT CALL THIS METHOD IN LONG RUNNING SESSIONS OR CONVERSATIONS A * THROWN UsernameNotFoundException WILL RENDER THE CONVERSATION UNUSABLE. * * @param username * Set the username * @return the userdetails of the user */ @Transactional(readOnly = true) public final UserDetails loadUserByUsername(final String username) { try { Assert.hasText(username); } catch (IllegalArgumentException iae) { throw new UsernameNotFoundException(username, iae); } try { User user = dao.load(username); userCache.putUserInCache(user); return user; } catch (ObjectRetrievalFailureException orfe) { throw new UsernameNotFoundException(username, orfe); } catch (NonUniqueResultException nure) { throw new IncorrectResultSizeDataAccessException( "More than one user found with name '" + username + "'", 1); } } /** * @param groupName * Set the group name * @param authority * Set the granted authority */ @PreAuthorize("hasRole('PERMISSION_ADMINISTRATE')") @Transactional(readOnly = false) public final void addGroupAuthority(final String groupName, final GrantedAuthority authority) { Assert.hasText(groupName); Assert.notNull(authority); Group group = groupDao.find(groupName); if (group.getGrantedAuthorities().add(authority)) { groupDao.update(group); } } /** * @param username Set the username * @param groupName Set the group name */ @PreAuthorize("hasRole('PERMISSION_ADMINISTRATE')") @Transactional(readOnly = false) public final void addUserToGroup(final String username, final String groupName) { Assert.hasText(username); Assert.hasText(groupName); Group group = groupDao.find(groupName); User user = dao.find(username); if (group.addMember(user)) { groupDao.update(group); userCache.removeUserFromCache(user.getUsername()); } } /** * @param groupName Set the group name * @param authorities Set the authorities granted to the group */ @PreAuthorize("hasRole('PERMISSION_ADMINISTRATE')") @Transactional(readOnly = false) public final void createGroup(final String groupName, final List<GrantedAuthority> authorities) { Assert.hasText(groupName); Assert.notNull(authorities); Group group = new Group(); group.setName(groupName); for (GrantedAuthority authority : authorities) { group.getGrantedAuthorities().add(authority); } groupDao.save(group); } /** * @param groupName The name of the group to delete */ @PreAuthorize("hasRole('PERMISSION_ADMINISTRATE') or hasRole('PERMISSION_DELETE_GROUP')") @Transactional(readOnly = false) public final void deleteGroup(final String groupName) { Assert.hasText(groupName); Group group = groupDao.find(groupName); for(User user : group.getMembers()) { removeUserFromGroup(user.getUsername(), groupName); } for(Object[] objs : listAces(groupName)) { Object object = objs[0]; Acl acl = (Acl)objs[1]; aclService.deleteAcl(acl.getObjectIdentity(), true); } groupDao.delete(groupName); } /** * @return a list of all of the groups */ @Transactional(readOnly = true) public final List<String> findAllGroups() { return groupDao.listNames(null, null); } /** * @param groupName * The name of the group for which the authorities should be * found * @return the authorities granted to the group */ @Transactional(readOnly = true) public final List<GrantedAuthority> findGroupAuthorities( final String groupName) { Assert.hasText(groupName); Group group = groupDao.find(groupName); return new ArrayList<GrantedAuthority>(group.getGrantedAuthorities()); } /** * @param groupName * the name of the group for which the users should be found * @return the list of usernames belonging to that group */ @Transactional(readOnly = true) public final List<String> findUsersInGroup(final String groupName) { Assert.hasText(groupName); Group group = groupDao.find(groupName); List<String> users = groupDao.listMembers(group, null, null); return users; } /** * @param groupName * The name of the group for which the authority should be * removed * @param authority * The authority to remove */ @PreAuthorize("hasRole('PERMISSION_ADMINISTRATE')") @Transactional(readOnly = false) public final void removeGroupAuthority(final String groupName, final GrantedAuthority authority) { Assert.hasText(groupName); Assert.notNull(authority); Group group = groupDao.find(groupName); if (group.getGrantedAuthorities().remove(authority)) { groupDao.update(group); } } /** * @param username * Set the name of the user to remove from the group * @param groupName * Set the name of the group from which the user should be * removed */ @PreAuthorize("hasRole('PERMISSION_ADMINISTRATE')") @Transactional(readOnly = false) public final void removeUserFromGroup(final String username, final String groupName) { Assert.hasText(username); Assert.hasText(groupName); Group group = groupDao.find(groupName); User user = dao.find(username); if (group.removeMember(user)) { groupDao.update(group); userCache.removeUserFromCache(user.getUsername()); } } /** * @param oldName Set the old name of the group * @param newName Set the new name of the group */ @PreAuthorize("hasRole('PERMISSION_ADMINISTRATE')") @Transactional(readOnly = false) public final void renameGroup(final String oldName, final String newName) { Assert.hasText(oldName); Assert.hasText(newName); Group group = groupDao.find(oldName); group.setName(newName); groupDao.update(group); } /** * @param user Set the user to update */ @Transactional(readOnly = false) public final void update(final User user) { updateUser(user); } /** * * @param group Set the group to save */ @PreAuthorize("hasRole('PERMISSION_ADMINISTRATE')") @Transactional(readOnly = false) public final void saveGroup(final Group group) { groupDao.save(group); } /** * * @param groupName Set the name of the group * @param groupType Set the type of group (ignored) * @param parentGroupId Set the parent group (ignored) * @return the name of the group */ public final String createGroup(final String groupName, final String groupType, final String parentGroupId) { this.createGroup(groupName, new ArrayList<GrantedAuthority>()); return groupName; } /** * * @param username Set the name of the user * @param groupName Set the name of the group * @param role Set the role of the user in the group (ignored) */ public final void createMembership(final String username, final String groupName, final String role) { this.addUserToGroup(username, groupName); } /** * * @param username Set the username * @param givenName Set the given name * @param familyName Set the family name * @param businessEmail Set the email address * @return the username of the created user */ public final String createUser(final String username, final String givenName, final String familyName, final String businessEmail) { User user = new User(); user.setUsername(username); user.setEmailAddress(businessEmail); user.setApiKey(UUID.randomUUID().toString()); this.createUser(user); return username; } /** * * @param username Set the name of the user to remove from the group * @param groupName Set the name of the group * @param role Set the role of the user in that group (ignored) */ public final void deleteMembership(final String username, final String groupName, final String role) { this.removeUserFromGroup(username, groupName); } /** * * @param identifier Set the name of the group to find * @return the group or null if the group does not exist */ public final Group findGroup(final String identifier) { return groupDao.find(identifier); } /** * @param object Set the secured object * @param recipient Set the recipient principal * @param permission Set the type of permission * @param clazz Set the class of object * */ @PreAuthorize("hasRole('PERMISSION_ADMINISTRATE')") @Transactional(readOnly = false) public void addPermission(final SecuredObject object, final String recipient, final Permission permission, final Class<? extends SecuredObject> clazz) { MutableAcl acl; ObjectIdentity oid = new ObjectIdentityImpl(clazz.getCanonicalName(), object.getId()); try { acl = (MutableAcl) aclService.readAclById(oid); } catch (NotFoundException nfe) { acl = aclService.createAcl(oid); } acl.insertAce(acl.getEntries().size(), permission, new PrincipalSid( recipient), true); aclService.updateAcl(acl); if (logger.isDebugEnabled()) { logger.debug("Added permission " + permission + " for Sid " + recipient + " securedObject " + object); } } /** * @param object Set the secured object * @param recipient Set the recipient principal * @param permission Set the type of permission * @param clazz Set the class of object * */ @PreAuthorize("hasRole('PERMISSION_ADMINISTRATE')") @Transactional(readOnly = false) public void deletePermission(final SecuredObject object, final String recipient, final Permission permission, final Class<? extends SecuredObject> clazz) { ObjectIdentity oid = new ObjectIdentityImpl(clazz.getCanonicalName(), object.getId()); MutableAcl acl = (MutableAcl) aclService.readAclById(oid); Sid sid = new PrincipalSid(recipient); // Remove all permissions associated with this particular recipient // (string equality used to keep things simple) List<AccessControlEntry> entries = acl.getEntries(); for (int i = 0; i < entries.size(); i++) { if (entries.get(i).getSid().equals(sid) && entries.get(i).getPermission().equals(permission)) { acl.deleteAce(i); } } aclService.updateAcl(acl); if (logger.isDebugEnabled()) { logger.debug("Deleted securedObject " + object + " ACL permissions for recipient " + recipient); } } /** * * @param recipient Set the principal * @return a list of access control entries */ @Transactional public final List<Object[]> listAces(final String recipient) { return aclService.listAces(new PrincipalSid(recipient)); } @Override public String makeProfileThumbnail(MultipartFile file, String oldProfileImage) throws IOException, InterruptedException, IM4JavaException { if(file != null && !file.isEmpty()) { String tmpFileExtension = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf('.')); String tmpFileName = UUID.randomUUID().toString() + tmpFileExtension; File tmpFile = new File(temporaryFolder.getFile(),tmpFileName); file.transferTo(tmpFile); Metadata metadata = new Metadata(); AutoDetectParser parser = new AutoDetectParser(); FileInputStream fileInputStream = new FileInputStream(tmpFile); String mimeType = tika.detect(fileInputStream); String fileExtension = null; switch(mimeType) { case "image/jpeg": fileExtension = ".jpg"; break; case "image/png": fileExtension = ".png"; break; case "image/gif": fileExtension = ".gif"; break; default: throw new UnsupportedOperationException(mimeType); } String imageFileName = UUID.randomUUID().toString() + fileExtension; File imageFile = new File(userProfilesFolder.getFile(), imageFileName); ConvertCmd convert = new ConvertCmd(); if (searchPath != null) { convert.setSearchPath(searchPath); } IMOperation operation = new IMOperation(); operation.addImage(tmpFile.getAbsolutePath()); operation.resize(THUMBNAIL_DIMENSION.intValue(), THUMBNAIL_DIMENSION.intValue(), "^"); operation.gravity("center"); operation.extent(THUMBNAIL_DIMENSION.intValue(), THUMBNAIL_DIMENSION.intValue()); operation.addImage(imageFile.getAbsolutePath()); convert.run(operation); tmpFile.delete(); if(oldProfileImage != null) { File oldFile = new File(userProfilesFolder.getFile(),oldProfileImage); oldFile.delete(); } return imageFileName; } else { return null; } } public void setTemporaryFolder(FileSystemResource temporaryFolder) { this.temporaryFolder = temporaryFolder; } public void setUserProfilesFolder(FileSystemResource userProfilesFolder) { this.userProfilesFolder = userProfilesFolder; } @Override @Transactional public UserDetails getUserByApiKey(String apiKey) { return this.dao.getUserByApiKey(apiKey); } }