diff --git a/libresonic-main/pom.xml b/libresonic-main/pom.xml index 23325441..3b747114 100644 --- a/libresonic-main/pom.xml +++ b/libresonic-main/pom.xml @@ -69,6 +69,11 @@ org.springframework.security spring-security-ldap + + org.springframework.ldap + spring-ldap-core + 2.0.2.RELEASE + org.springframework.security spring-security-taglibs diff --git a/libresonic-main/src/main/java/org/libresonic/player/controller/AdvancedSettingsController.java b/libresonic-main/src/main/java/org/libresonic/player/controller/AdvancedSettingsController.java index 774e7632..3f9845ee 100644 --- a/libresonic-main/src/main/java/org/libresonic/player/controller/AdvancedSettingsController.java +++ b/libresonic-main/src/main/java/org/libresonic/player/controller/AdvancedSettingsController.java @@ -98,6 +98,8 @@ public class AdvancedSettingsController { settingsService.setSmtpPassword(command.getSmtpPassword()); } + settingsService.save(); + return "redirect:advancedSettings.view"; } diff --git a/libresonic-main/src/main/java/org/libresonic/player/dao/UserDao.java b/libresonic-main/src/main/java/org/libresonic/player/dao/UserDao.java index cb08ff5d..36309a7e 100644 --- a/libresonic-main/src/main/java/org/libresonic/player/dao/UserDao.java +++ b/libresonic-main/src/main/java/org/libresonic/player/dao/UserDao.java @@ -69,11 +69,23 @@ public class UserDao extends AbstractDao { * Returns the user with the given username. * * @param username The username used when logging in. + * @param caseSensitive * @return The user, or null if not found. */ - public User getUserByName(String username) { - String sql = "select " + USER_COLUMNS + " from " + getUserTable() + " where username=?"; - User user = queryOne(sql, userRowMapper, username); + public User getUserByName(String username, boolean caseSensitive) { + String sql; + if(caseSensitive) { + sql = "select " + USER_COLUMNS + " from " + getUserTable() + " where username=?"; + } else { + sql = "select " + USER_COLUMNS + " from " + getUserTable() + " where UPPER(username)=UPPER(?)"; + } + List users = query(sql, userRowMapper, username); + User user = null; + if(users.size() == 1) { + user = users.iterator().next(); + } else if (users.size() > 1) { + throw new RuntimeException("Too many matching users"); + } if(user != null) { readRoles(user); } diff --git a/libresonic-main/src/main/java/org/libresonic/player/security/LibresonicUserDetailsContextMapper.java b/libresonic-main/src/main/java/org/libresonic/player/security/LibresonicUserDetailsContextMapper.java new file mode 100644 index 00000000..09d3d344 --- /dev/null +++ b/libresonic-main/src/main/java/org/libresonic/player/security/LibresonicUserDetailsContextMapper.java @@ -0,0 +1,133 @@ +/* + * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * + * 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.libresonic.player.security; + +import org.libresonic.player.Logger; +import org.libresonic.player.domain.User; +import org.libresonic.player.service.SecurityService; +import org.libresonic.player.service.SettingsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.ldap.ppolicy.PasswordPolicyControl; +import org.springframework.security.ldap.ppolicy.PasswordPolicyResponseControl; +import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; +import org.springframework.security.ldap.userdetails.UserDetailsContextMapper; +import org.springframework.stereotype.Component; + +import java.util.Collection; + +@Component +public class LibresonicUserDetailsContextMapper implements UserDetailsContextMapper { + // ~ Instance fields + // ================================================================================================ + + private final Logger logger = Logger.getLogger(LibresonicUserDetailsContextMapper.class); + private String passwordAttributeName = "userPassword"; + + @Autowired + SecurityService securityService; + + @Autowired + SettingsService settingsService; + + // ~ Methods + // ======================================================================================================== + + public UserDetails mapUserFromContext(DirContextOperations ctx, String username, + Collection authorities) { + String dn = ctx.getNameInNamespace(); + + logger.debug("Mapping user details from context with DN: " + dn); + + // User must be defined in Libresonic, unless auto-shadowing is enabled. + User user = securityService.getUserByName(username, false); + if (user == null && !settingsService.isLdapAutoShadowing()) { + throw new BadCredentialsException("User does not exist."); + } + + if (user == null) { + User newUser = new User(username, "", null, true, 0L, 0L, 0L); + newUser.setStreamRole(true); + newUser.setSettingsRole(true); + securityService.createUser(newUser); + logger.info("Created local user '" + username + "' for DN " + dn); + user = securityService.getUserByName(username, false); + } + + // LDAP authentication must be enabled for the given user. + if (!user.isLdapAuthenticated()) { + throw new BadCredentialsException("LDAP authentication disabled for user."); + } + + LdapUserDetailsImpl.Essence essence = new LdapUserDetailsImpl.Essence(); + essence.setDn(dn); + + Object passwordValue = ctx.getObjectAttribute(passwordAttributeName); + + if (passwordValue != null) { + essence.setPassword(mapPassword(passwordValue)); + } + + essence.setUsername(user.getUsername()); + + // Add the supplied authorities + for (GrantedAuthority authority : securityService.getGrantedAuthorities(user.getUsername())) { + essence.addAuthority(authority); + } + + // Check for PPolicy data + + PasswordPolicyResponseControl ppolicy = (PasswordPolicyResponseControl) ctx + .getObjectAttribute(PasswordPolicyControl.OID); + + if (ppolicy != null) { + essence.setTimeBeforeExpiration(ppolicy.getTimeBeforeExpiration()); + essence.setGraceLoginsRemaining(ppolicy.getGraceLoginsRemaining()); + } + + return essence.createUserDetails(); + + } + + public void mapUserToContext(UserDetails user, DirContextAdapter ctx) { + throw new UnsupportedOperationException( + "LdapUserDetailsMapper only supports reading from a context. Please" + + "use a subclass if mapUserToContext() is required."); + } + + /** + * Extension point to allow customized creation of the user's password from the + * attribute stored in the directory. + * + * @param passwordValue the value of the password attribute + * @return a String representation of the password. + */ + protected String mapPassword(Object passwordValue) { + + if (!(passwordValue instanceof String)) { + // Assume it's binary + passwordValue = new String((byte[]) passwordValue); + } + + return (String) passwordValue; + + } +} diff --git a/libresonic-main/src/main/java/org/libresonic/player/security/WebSecurityConfig.java b/libresonic-main/src/main/java/org/libresonic/player/security/WebSecurityConfig.java index 8b101a97..99440c0e 100644 --- a/libresonic-main/src/main/java/org/libresonic/player/security/WebSecurityConfig.java +++ b/libresonic-main/src/main/java/org/libresonic/player/security/WebSecurityConfig.java @@ -1,6 +1,7 @@ package org.libresonic.player.security; import org.libresonic.player.service.SecurityService; +import org.libresonic.player.service.SettingsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -18,20 +19,37 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private SecurityService securityService; + @Autowired private CsrfSecurityRequestMatcher csrfSecurityRequestMatcher; + @Autowired LoginFailureLogger loginFailureLogger; + @Autowired + SettingsService settingsService; + + @Autowired + LibresonicUserDetailsContextMapper libresonicUserDetailsContextMapper; + @Override @Bean(name = "authenticationManager") public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } - @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + if (settingsService.isLdapEnabled()) { + auth.ldapAuthentication() + .contextSource() + .managerDn(settingsService.getLdapManagerDn()) + .managerPassword(settingsService.getLdapManagerPassword()) + .url(settingsService.getLdapUrl()) + .and() + .userSearchFilter(settingsService.getLdapSearchFilter()) + .userDetailsContextMapper(libresonicUserDetailsContextMapper); + } auth.userDetailsService(securityService); } @@ -89,7 +107,6 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { .passwordParameter("j_password") // see http://docs.spring.io/spring-security/site/docs/3.2.4.RELEASE/reference/htmlsingle/#csrf-logout .and().logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET")).logoutSuccessUrl("/login?logout") - .and().rememberMe().userDetailsService(securityService).key("libresonic"); - + .and().rememberMe().key("libresonic"); } } \ No newline at end of file diff --git a/libresonic-main/src/main/java/org/libresonic/player/service/SecurityService.java b/libresonic-main/src/main/java/org/libresonic/player/service/SecurityService.java index d4b07cc5..86dda168 100644 --- a/libresonic-main/src/main/java/org/libresonic/player/service/SecurityService.java +++ b/libresonic-main/src/main/java/org/libresonic/player/service/SecurityService.java @@ -62,11 +62,22 @@ public class SecurityService implements UserDetailsService { * @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); + 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 authorities = getGrantedAuthorities(username); + + return new org.springframework.security.core.userdetails.User(username, user.getPassword(), authorities); + } + + public List getGrantedAuthorities(String username) { String[] roles = userDao.getRolesForUser(username); List authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("IS_AUTHENTICATED_ANONYMOUSLY")); @@ -74,8 +85,7 @@ public class SecurityService implements UserDetailsService { for (int i = 0; i < roles.length; i++) { authorities.add(new SimpleGrantedAuthority("ROLE_" + roles[i].toUpperCase())); } - - return new org.springframework.security.core.userdetails.User(username, user.getPassword(), authorities); + return authorities; } /** @@ -86,7 +96,7 @@ public class SecurityService implements UserDetailsService { */ public User getCurrentUser(HttpServletRequest request) { String username = getCurrentUsername(request); - return username == null ? null : userDao.getUserByName(username); + return username == null ? null : getUserByName(username); } /** @@ -106,7 +116,17 @@ public class SecurityService implements UserDetailsService { * @return The user, or null if not found. */ public User getUserByName(String username) { - return userDao.getUserByName(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); } /** diff --git a/libresonic-main/src/main/resources/org/libresonic/player/i18n/ResourceBundle_en.properties b/libresonic-main/src/main/resources/org/libresonic/player/i18n/ResourceBundle_en.properties index 512346ff..b90fdc02 100644 --- a/libresonic-main/src/main/resources/org/libresonic/player/i18n/ResourceBundle_en.properties +++ b/libresonic-main/src/main/resources/org/libresonic/player/i18n/ResourceBundle_en.properties @@ -371,6 +371,7 @@ advancedsettings.ldapsearchfilter = LDAP search filter advancedsettings.ldapmanagerdn = LDAP manager DN
(Optional)
advancedsettings.ldapmanagerpassword = Password advancedsettings.ldapautoshadowing = Automatically create users in {0} +advancedsettings.ldapRequiresRestart = LDAP settings require a restart to take effect advancedsettings.smtpPort = SMTP port advancedsettings.smtpServer = SMTP server advancedsettings.smtpEncryption = SMTP encryption diff --git a/libresonic-main/src/main/webapp/WEB-INF/jsp/advancedSettings.jsp b/libresonic-main/src/main/webapp/WEB-INF/jsp/advancedSettings.jsp index 500124c4..ac66edfb 100644 --- a/libresonic-main/src/main/webapp/WEB-INF/jsp/advancedSettings.jsp +++ b/libresonic-main/src/main/webapp/WEB-INF/jsp/advancedSettings.jsp @@ -138,6 +138,8 @@ +

+ " style="margin-right:0.3em"> " onclick="location.href='nowPlaying.view'"> diff --git a/libresonic-main/src/test/java/org/libresonic/player/dao/UserDaoTestCase.java b/libresonic-main/src/test/java/org/libresonic/player/dao/UserDaoTestCase.java index 16aab9c5..0465e694 100644 --- a/libresonic-main/src/test/java/org/libresonic/player/dao/UserDaoTestCase.java +++ b/libresonic-main/src/test/java/org/libresonic/player/dao/UserDaoTestCase.java @@ -115,15 +115,15 @@ public class UserDaoTestCase extends DaoTestCaseBean2 { User user = new User("sindre", "secret", null); userDao.createUser(user); - User newUser = userDao.getUserByName("sindre"); + User newUser = userDao.getUserByName("sindre", true); assertNotNull("Error in getUserByName().", newUser); assertUserEquals(user, newUser); - assertNull("Error in getUserByName().", userDao.getUserByName("sindre2")); - assertNull("Error in getUserByName().", userDao.getUserByName("sindre ")); - assertNull("Error in getUserByName().", userDao.getUserByName("bente")); - assertNull("Error in getUserByName().", userDao.getUserByName("")); - assertNull("Error in getUserByName().", userDao.getUserByName(null)); + assertNull("Error in getUserByName().", userDao.getUserByName("sindre2", true)); + assertNull("Error in getUserByName().", userDao.getUserByName("sindre ", true)); + assertNull("Error in getUserByName().", userDao.getUserByName("bente", true)); + assertNull("Error in getUserByName().", userDao.getUserByName("", true)); + assertNull("Error in getUserByName().", userDao.getUserByName(null, true)); } @Test