/* This file is part of Libresonic. Libresonic is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Libresonic 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 General Public License for more details. You should have received a copy of the GNU General Public License along with Libresonic. If not, see <http://www.gnu.org/licenses/>. Copyright 2016 (C) Libresonic Authors Based upon Subsonic, Copyright 2009 (C) Sindre Mehus */ package org.libresonic.player.service; import net.sf.ehcache.Ehcache; import org.libresonic.player.Logger; import org.libresonic.player.dao.UserDao; import org.libresonic.player.domain.MediaFile; import org.libresonic.player.domain.MusicFolder; import org.libresonic.player.domain.User; import org.libresonic.player.util.FileUtil; import org.springframework.dao.DataAccessException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestWrapper; import javax.servlet.http.HttpServletRequest; import java.io.File; import java.util.ArrayList; import java.util.List; /** * Provides security-related services for authentication and authorization. * * @author Sindre Mehus */ public class SecurityService implements UserDetailsService { private static final Logger LOG = Logger.getLogger(SecurityService.class); private UserDao userDao; private SettingsService settingsService; private Ehcache userCache; /** * Locates the user based on the username. * * @param username The username * @return A fully populated user record (never <code>null</code>) * @throws UsernameNotFoundException if the user could not be found or the user has no GrantedAuthority. * @throws DataAccessException If user could not be found for a repository-specific reason. */ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { return loadUserByUsername(username, true); } public UserDetails loadUserByUsername(String username, boolean caseSensitive) throws UsernameNotFoundException, DataAccessException { User user = getUserByName(username, caseSensitive); if (user == null) { throw new UsernameNotFoundException("User \"" + username + "\" was not found."); } List<GrantedAuthority> authorities = getGrantedAuthorities(username); return new org.springframework.security.core.userdetails.User(username, user.getPassword(), authorities); } public List<GrantedAuthority> getGrantedAuthorities(String username) { String[] roles = userDao.getRolesForUser(username); List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("IS_AUTHENTICATED_ANONYMOUSLY")); authorities.add(new SimpleGrantedAuthority("ROLE_USER")); for (int i = 0; i < roles.length; i++) { authorities.add(new SimpleGrantedAuthority("ROLE_" + roles[i].toUpperCase())); } return authorities; } /** * Returns the currently logged-in user for the given HTTP request. * * @param request The HTTP request. * @return The logged-in user, or <code>null</code>. */ public User getCurrentUser(HttpServletRequest request) { String username = getCurrentUsername(request); return username == null ? null : getUserByName(username); } /** * Returns the name of the currently logged-in user. * * @param request The HTTP request. * @return The name of the logged-in user, or <code>null</code>. */ public String getCurrentUsername(HttpServletRequest request) { return new SecurityContextHolderAwareRequestWrapper(request, null).getRemoteUser(); } /** * Returns the user with the given username. * * @param username The username used when logging in. * @return The user, or <code>null</code> if not found. */ public User getUserByName(String username) { return getUserByName(username, true); } /** * Returns the user with the given username * @param username * @param caseSensitive If false, will do a case insensitive search * @return */ public User getUserByName(String username, boolean caseSensitive) { return userDao.getUserByName(username, caseSensitive); } /** * Returns the user with the given email address. * * @param email The email address. * @return The user, or <code>null</code> if not found. */ public User getUserByEmail(String email) { return userDao.getUserByEmail(email); } /** * Returns all users. * * @return Possibly empty array of all users. */ public List<User> getAllUsers() { return userDao.getAllUsers(); } /** * Returns whether the given user has administrative rights. */ public boolean isAdmin(String username) { if (User.USERNAME_ADMIN.equals(username)) { return true; } User user = getUserByName(username); return user != null && user.isAdminRole(); } /** * Creates a new user. * * @param user The user to create. */ public void createUser(User user) { userDao.createUser(user); settingsService.setMusicFoldersForUser(user.getUsername(), MusicFolder.toIdList(settingsService.getAllMusicFolders())); LOG.info("Created user " + user.getUsername()); } /** * Deletes the user with the given username. * * @param username The username. */ public void deleteUser(String username) { userDao.deleteUser(username); LOG.info("Deleted user " + username); userCache.remove(username); } /** * Updates the given user. * * @param user The user to update. */ public void updateUser(User user) { userDao.updateUser(user); userCache.remove(user.getUsername()); } /** * Updates the byte counts for given user. * * @param user The user to update, may be <code>null</code>. * @param bytesStreamedDelta Increment bytes streamed count with this value. * @param bytesDownloadedDelta Increment bytes downloaded count with this value. * @param bytesUploadedDelta Increment bytes uploaded count with this value. */ public void updateUserByteCounts(User user, long bytesStreamedDelta, long bytesDownloadedDelta, long bytesUploadedDelta) { if (user == null) { return; } user.setBytesStreamed(user.getBytesStreamed() + bytesStreamedDelta); user.setBytesDownloaded(user.getBytesDownloaded() + bytesDownloadedDelta); user.setBytesUploaded(user.getBytesUploaded() + bytesUploadedDelta); userDao.updateUser(user); } /** * Returns whether the given file may be read. * * @return Whether the given file may be read. */ public boolean isReadAllowed(File file) { // Allowed to read from both music folder and podcast folder. return isInMusicFolder(file) || isInPodcastFolder(file); } /** * Returns whether the given file may be written, created or deleted. * * @return Whether the given file may be written, created or deleted. */ public boolean isWriteAllowed(File file) { // Only allowed to write podcasts or cover art. boolean isPodcast = isInPodcastFolder(file); boolean isCoverArt = isInMusicFolder(file) && file.getName().startsWith("cover."); return isPodcast || isCoverArt; } /** * Returns whether the given file may be uploaded. * * @return Whether the given file may be uploaded. */ public boolean isUploadAllowed(File file) { return isInMusicFolder(file) && !FileUtil.exists(file); } /** * Returns whether the given file is located in one of the music folders (or any of their sub-folders). * * @param file The file in question. * @return Whether the given file is located in one of the music folders. */ private boolean isInMusicFolder(File file) { return getMusicFolderForFile(file) != null; } private MusicFolder getMusicFolderForFile(File file) { List<MusicFolder> folders = settingsService.getAllMusicFolders(false, true); String path = file.getPath(); for (MusicFolder folder : folders) { if (isFileInFolder(path, folder.getPath().getPath())) { return folder; } } return null; } /** * Returns whether the given file is located in the Podcast folder (or any of its sub-folders). * * @param file The file in question. * @return Whether the given file is located in the Podcast folder. */ private boolean isInPodcastFolder(File file) { String podcastFolder = settingsService.getPodcastFolder(); return isFileInFolder(file.getPath(), podcastFolder); } public String getRootFolderForFile(File file) { MusicFolder folder = getMusicFolderForFile(file); if (folder != null) { return folder.getPath().getPath(); } if (isInPodcastFolder(file)) { return settingsService.getPodcastFolder(); } return null; } public boolean isFolderAccessAllowed(MediaFile file, String username) { if (isInPodcastFolder(file.getFile())) { return true; } for (MusicFolder musicFolder : settingsService.getMusicFoldersForUser(username)) { if (musicFolder.getPath().getPath().equals(file.getFolder())) { return true; } } return false; } /** * Returns whether the given file is located in the given folder (or any of its sub-folders). * If the given file contains the expression ".." (indicating a reference to the parent directory), * this method will return <code>false</code>. * * @param file The file in question. * @param folder The folder in question. * @return Whether the given file is located in the given folder. */ protected boolean isFileInFolder(String file, String folder) { // Deny access if file contains ".." surrounded by slashes (or end of line). if (file.matches(".*(/|\\\\)\\.\\.(/|\\\\|$).*")) { return false; } // Convert slashes. file = file.replace('\\', '/'); folder = folder.replace('\\', '/'); return file.toUpperCase().startsWith(folder.toUpperCase()); } public void setSettingsService(SettingsService settingsService) { this.settingsService = settingsService; } public void setUserDao(UserDao userDao) { this.userDao = userDao; } public void setUserCache(Ehcache userCache) { this.userCache = userCache; } }