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 extends GrantedAuthority> 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