/*
This file is part of Subsonic.
Subsonic 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.
Subsonic 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 Subsonic. If not, see .
Copyright 2009 (C) Sindre Mehus
*/
package net.sourceforge.subsonic.service;
import java.io.File;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.GrantedAuthorityImpl;
import org.acegisecurity.providers.dao.DaoAuthenticationProvider;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.UserDetailsService;
import org.acegisecurity.userdetails.UsernameNotFoundException;
import org.acegisecurity.wrapper.SecurityContextHolderAwareRequestWrapper;
import org.springframework.dao.DataAccessException;
import net.sf.ehcache.Ehcache;
import net.sourceforge.subsonic.Logger;
import net.sourceforge.subsonic.dao.UserDao;
import net.sourceforge.subsonic.domain.MusicFolder;
import net.sourceforge.subsonic.domain.User;
import net.sourceforge.subsonic.util.FileUtil;
/**
* 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 presented to the {@link DaoAuthenticationProvider}
* @return A fully populated user record (never null
)
* @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 {
User user = getUserByName(username);
if (user == null) {
throw new UsernameNotFoundException("User \"" + username + "\" was not found.");
}
String[] roles = userDao.getRolesForUser(username);
GrantedAuthority[] authorities = new GrantedAuthority[roles.length];
for (int i = 0; i < roles.length; i++) {
authorities[i] = new GrantedAuthorityImpl("ROLE_" + roles[i].toUpperCase());
}
// If user is LDAP authenticated, disable user. The proper authentication should in that case
// be done by SubsonicLdapBindAuthenticator.
boolean enabled = !user.isLdapAuthenticated();
return new org.acegisecurity.userdetails.User(username, user.getPassword(), enabled, true, true, true, authorities);
}
/**
* Returns the currently logged-in user for the given HTTP request.
*
* @param request The HTTP request.
* @return The logged-in user, or null
.
*/
public User getCurrentUser(HttpServletRequest request) {
String username = getCurrentUsername(request);
return username == null ? null : userDao.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 null
.
*/
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 null
if not found.
*/
public User getUserByName(String username) {
return userDao.getUserByName(username);
}
/**
* Returns the user with the given email address.
*
* @param email The email address.
* @return The user, or null
if not found.
*/
public User getUserByEmail(String email) {
return userDao.getUserByEmail(email);
}
/**
* Returns all users.
*
* @return Possibly empty array of all users.
*/
public List 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);
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 null
.
* @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 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;
}
/**
* 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 false
.
*
* @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;
}
}