diff --git a/core/src/main/java/hudson/model/Hudson.java b/core/src/main/java/hudson/model/Hudson.java index f1138ff7a9a8213d95c782b5bf98d109de4c2867..393ba1ab05a60f4a6143480e65a9925ab0002096 100644 --- a/core/src/main/java/hudson/model/Hudson.java +++ b/core/src/main/java/hudson/model/Hudson.java @@ -40,6 +40,7 @@ import hudson.security.Permission; import hudson.security.PermissionGroup; import hudson.security.SecurityMode; import hudson.security.SecurityRealm; +import hudson.security.SecurityRealm.SecurityComponents; import hudson.tasks.BuildStep; import hudson.tasks.BuildWrapper; import hudson.tasks.BuildWrappers; @@ -951,7 +952,9 @@ public final class Hudson extends View implements ItemGroup, Node, public void setSecurityRealm(SecurityRealm securityRealm) { this.securityRealm = securityRealm; - HudsonFilter.AUTHENTICATION_MANAGER.setManager(securityRealm.createAuthenticationManager()); + SecurityComponents sc = securityRealm.createSecurityComponents(); + HudsonFilter.AUTHENTICATION_MANAGER.setDelegate(sc.manager); + HudsonFilter.USER_DETAILS_SERVICE_PROXY.setDelegate(sc.userDetails); } /** diff --git a/core/src/main/java/hudson/security/AuthenticationManagerProxy.java b/core/src/main/java/hudson/security/AuthenticationManagerProxy.java index b9b9614a33e50a5ab44e49aef2c911d5ff790ceb..6347097bdac236b407aad551851278d84469d820 100644 --- a/core/src/main/java/hudson/security/AuthenticationManagerProxy.java +++ b/core/src/main/java/hudson/security/AuthenticationManagerProxy.java @@ -16,10 +16,10 @@ import org.acegisecurity.DisabledException; * @author Kohsuke Kawaguchi */ public class AuthenticationManagerProxy implements AuthenticationManager { - private volatile AuthenticationManager manager; + private volatile AuthenticationManager delegate; public Authentication authenticate(Authentication authentication) throws AuthenticationException { - AuthenticationManager m = manager; // fix the reference we are working with + AuthenticationManager m = delegate; // fix the reference we are working with if(m ==null) throw new DisabledException("Authentication service is still not ready yet"); @@ -27,7 +27,7 @@ public class AuthenticationManagerProxy implements AuthenticationManager { return m.authenticate(authentication); } - public void setManager(AuthenticationManager manager) { - this.manager = manager; + public void setDelegate(AuthenticationManager manager) { + this.delegate = manager; } } diff --git a/core/src/main/java/hudson/security/HudsonFilter.java b/core/src/main/java/hudson/security/HudsonFilter.java index fb01c6707b120447077ddbe382a5a6abfa593479..5f3ca20fa42bf0bcec9576b3aad4efe70a293071 100644 --- a/core/src/main/java/hudson/security/HudsonFilter.java +++ b/core/src/main/java/hudson/security/HudsonFilter.java @@ -4,6 +4,7 @@ import groovy.lang.Binding; import hudson.model.Hudson; import hudson.util.spring.BeanBuilder; import org.acegisecurity.AuthenticationManager; +import org.acegisecurity.userdetails.UserDetailsService; import org.springframework.web.context.WebApplicationContext; import javax.servlet.Filter; @@ -37,14 +38,22 @@ public class HudsonFilter implements Filter { private Filter acegi; /** - * {@link AuthenticationManager} proxy so that the acegi filter chain can be configured - * before {@link Hudson} instance is loaded. + * {@link AuthenticationManager} proxy so that the acegi filter chain can stay the same + * even when security setting is reconfigured. */ public static final AuthenticationManagerProxy AUTHENTICATION_MANAGER = new AuthenticationManagerProxy(); + /** + * {@link UserDetailsService} proxy so that the acegi filter chain can stay the same + * even when security setting is reconfigured. + */ + public static final UserDetailsServiceProxy USER_DETAILS_SERVICE_PROXY = new UserDetailsServiceProxy(); + public void init(FilterConfig filterConfig) throws ServletException { Binding binding = new Binding(); - binding.setVariable("authenticationManager", HudsonFilter.AUTHENTICATION_MANAGER); + binding.setVariable("authenticationManagerProxy", AUTHENTICATION_MANAGER); + binding.setVariable("UserDetailsServiceProxy", USER_DETAILS_SERVICE_PROXY); + binding.setVariable("app", Hudson.getInstance()); BeanBuilder builder = new BeanBuilder(); builder.parse(filterConfig.getServletContext().getResourceAsStream("/WEB-INF/security/SecurityFilters.groovy"),binding); diff --git a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java index 2d4c7f9f9b5658f0c45f3439f87766005489528d..bfaccac82cfb8d83366949eecd277724ab1cbb4f 100644 --- a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java +++ b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java @@ -9,7 +9,6 @@ import hudson.util.Scrambler; import hudson.util.Protector; import hudson.util.spring.BeanBuilder; import hudson.Util; -import hudson.security.HudsonPrivateSecurityRealm.Details; import hudson.tasks.Mailer; import net.sf.json.JSONObject; import org.acegisecurity.Authentication; @@ -24,6 +23,7 @@ import org.acegisecurity.userdetails.UserDetailsService; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.Stapler; +import org.springframework.web.context.WebApplicationContext; import javax.servlet.ServletException; import java.io.IOException; @@ -35,10 +35,13 @@ import java.io.IOException; */ public class HudsonPrivateSecurityRealm extends SecurityRealm { @Override - public AuthenticationManager createAuthenticationManager() { + public SecurityComponents createSecurityComponents() { BeanBuilder builder = new BeanBuilder(); builder.parse(Hudson.getInstance().servletContext.getResourceAsStream("/WEB-INF/security/HudsonPrivateSecurityRealm.groovy")); - return findBean(AuthenticationManager.class,builder.createApplicationContext()); + WebApplicationContext context = builder.createApplicationContext(); + return new SecurityComponents( + findBean(AuthenticationManager.class, context), + findBean(UserDetailsService.class, context)); } public Descriptor getDescriptor() { diff --git a/core/src/main/java/hudson/security/LDAPSecurityRealm.java b/core/src/main/java/hudson/security/LDAPSecurityRealm.java index a978403885cb671d6fdd536832cfa6e237803e95..c801c9e67be9302f9ba2f29c96498531a9de3fb9 100644 --- a/core/src/main/java/hudson/security/LDAPSecurityRealm.java +++ b/core/src/main/java/hudson/security/LDAPSecurityRealm.java @@ -9,11 +9,13 @@ import hudson.util.FormFieldValidator; import hudson.util.spring.BeanBuilder; import net.sf.json.JSONObject; import org.acegisecurity.AuthenticationManager; +import org.acegisecurity.userdetails.UserDetailsService; import org.acegisecurity.ldap.search.FilterBasedLdapUserSearch; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; +import org.springframework.web.context.WebApplicationContext; import javax.naming.NamingException; import javax.naming.directory.Attribute; @@ -119,13 +121,16 @@ public class LDAPSecurityRealm extends SecurityRealm { return "ldap://"+server+'/'+rootDN; } - public AuthenticationManager createAuthenticationManager() { + public SecurityComponents createSecurityComponents() { Binding binding = new Binding(); binding.setVariable("instance", this); BeanBuilder builder = new BeanBuilder(); builder.parse(Hudson.getInstance().servletContext.getResourceAsStream("/WEB-INF/security/LDAPBindSecurityRealm.groovy"),binding); - return findBean(AuthenticationManager.class,builder.createApplicationContext()); + WebApplicationContext appContext = builder.createApplicationContext(); + return new SecurityComponents( + findBean(AuthenticationManager.class, appContext), + findBean(UserDetailsService.class, appContext)); } public DescriptorImpl getDescriptor() { diff --git a/core/src/main/java/hudson/security/LegacySecurityRealm.java b/core/src/main/java/hudson/security/LegacySecurityRealm.java index 85979a59e9327dabf788628f6f5cf376d520c597..b69a1afa66162d90b155fbbcc0956eff713b5660 100644 --- a/core/src/main/java/hudson/security/LegacySecurityRealm.java +++ b/core/src/main/java/hudson/security/LegacySecurityRealm.java @@ -14,8 +14,8 @@ import net.sf.json.JSONObject; * @author Kohsuke Kawaguchi */ public final class LegacySecurityRealm extends SecurityRealm implements AuthenticationManager { - public AuthenticationManager createAuthenticationManager() { - return this; + public SecurityComponents createSecurityComponents() { + return new SecurityComponents(this); } public Authentication authenticate(Authentication authentication) throws AuthenticationException { diff --git a/core/src/main/java/hudson/security/SecurityRealm.java b/core/src/main/java/hudson/security/SecurityRealm.java index 486c6a97431c9f5a0164f75de0e6825c57ec3674..f3bbb256b92d3f3ef608c2251fb3dc457c31d291 100644 --- a/core/src/main/java/hudson/security/SecurityRealm.java +++ b/core/src/main/java/hudson/security/SecurityRealm.java @@ -7,6 +7,7 @@ import hudson.model.Hudson; import hudson.util.DescriptorList; import org.acegisecurity.Authentication; import org.acegisecurity.AuthenticationManager; +import org.acegisecurity.userdetails.UserDetailsService; import org.springframework.context.ApplicationContext; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; @@ -58,7 +59,7 @@ public abstract class SecurityRealm implements Describable, Exten * depends on the user configuration), and such configuration is expected to be * captured as instance variables of {@link SecurityRealm} implementation. */ - public abstract AuthenticationManager createAuthenticationManager(); + public abstract SecurityComponents createSecurityComponents(); /** * {@inheritDoc} @@ -164,12 +165,12 @@ public abstract class SecurityRealm implements Describable, Exten public static final SecurityRealm NO_AUTHENTICATION = new None(); private static class None extends SecurityRealm { - public AuthenticationManager createAuthenticationManager() { - return new AuthenticationManager() { + public SecurityComponents createSecurityComponents() { + return new SecurityComponents(new AuthenticationManager() { public Authentication authenticate(Authentication authentication) { return authentication; } - }; + }); } /** @@ -188,6 +189,22 @@ public abstract class SecurityRealm implements Describable, Exten } } + public static final class SecurityComponents { + public AuthenticationManager manager; + public UserDetailsService userDetails; + + public SecurityComponents() {} + + public SecurityComponents(AuthenticationManager manager) { + this.manager = manager; + } + + public SecurityComponents(AuthenticationManager manager, UserDetailsService userDetails) { + this.manager = manager; + this.userDetails = userDetails; + } + } + /** * All registered {@link SecurityRealm} implementations. */ diff --git a/core/src/main/java/hudson/security/UserDetailsServiceProxy.java b/core/src/main/java/hudson/security/UserDetailsServiceProxy.java new file mode 100644 index 0000000000000000000000000000000000000000..1ea01c669129e8702cc33f69920a43a57a58e9dd --- /dev/null +++ b/core/src/main/java/hudson/security/UserDetailsServiceProxy.java @@ -0,0 +1,28 @@ +package hudson.security; + +import org.acegisecurity.userdetails.UserDetails; +import org.acegisecurity.userdetails.UserDetailsService; +import org.acegisecurity.userdetails.UsernameNotFoundException; +import org.springframework.dao.DataAccessException; + +/** + * {@link UserDetailsService} proxy that delegates to another instance. + * + * @author Kohsuke Kawaguchi + */ +public class UserDetailsServiceProxy implements UserDetailsService { + private volatile UserDetailsService delegate; + + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { + UserDetailsService uds = delegate; // fix the reference for concurrency support + + if(uds ==null) + throw new UsernameNotFoundException(Messages.UserDetailsServiceProxy_UnableToQuery(username)); + return uds.loadUserByUsername(username); + } + + public void setDelegate(UserDetailsService core) { + this.delegate = core; + } + +} diff --git a/core/src/main/resources/hudson/security/Messages.properties b/core/src/main/resources/hudson/security/Messages.properties index 2d3e4930269849fc4ae29ba81fa486741b00631a..92f034eced96331c2b75ab7220b41e85f0fdf06a 100644 --- a/core/src/main/resources/hudson/security/Messages.properties +++ b/core/src/main/resources/hudson/security/Messages.properties @@ -4,5 +4,7 @@ LegacyAuthorizationStrategy.DisplayName=Legacy mode HudsonPrivateSecurityRealm.Details.DisplayName=Password +UserDetailsServiceProxy.UnableToQuery=Unable to query user information: {0} + # not in use Permission.Permissions.Title=N/A diff --git a/war/resources/WEB-INF/security/HudsonPrivateSecurityRealm.groovy b/war/resources/WEB-INF/security/HudsonPrivateSecurityRealm.groovy index aee26a551603e905b536bf7c49bb80f0a0f93e1c..fa4ac420f53ddda5e56019585636113afedce99d 100644 --- a/war/resources/WEB-INF/security/HudsonPrivateSecurityRealm.groovy +++ b/war/resources/WEB-INF/security/HudsonPrivateSecurityRealm.groovy @@ -5,6 +5,7 @@ import org.acegisecurity.providers.ProviderManager import hudson.security.HudsonPrivateSecurityRealm.HudsonUserDetailsService import org.acegisecurity.providers.dao.DaoAuthenticationProvider import org.acegisecurity.providers.anonymous.AnonymousAuthenticationProvider +import org.acegisecurity.providers.rememberme.RememberMeAuthenticationProvider authenticationManager(ProviderManager) { @@ -13,6 +14,11 @@ authenticationManager(ProviderManager) { bean(DaoAuthenticationProvider) { userDetailsService = new HudsonUserDetailsService() }, + + // these providers apply everywhere + bean(RememberMeAuthenticationProvider) { + key = app.getSecretKey(); + }, // this doesn't mean we allow anonymous access. // we just authenticate anonymous users as such, // so that later authorization can reject them if so configured diff --git a/war/resources/WEB-INF/security/LDAPBindSecurityRealm.groovy b/war/resources/WEB-INF/security/LDAPBindSecurityRealm.groovy index 23ed6a6f5838c99851f5a3327d513c8b3b33b723..06fe76ec8653b53db54edaa7ec7508c6ccae9eec 100644 --- a/war/resources/WEB-INF/security/LDAPBindSecurityRealm.groovy +++ b/war/resources/WEB-INF/security/LDAPBindSecurityRealm.groovy @@ -5,6 +5,7 @@ import org.acegisecurity.providers.ldap.authenticator.BindAuthenticator import org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator import org.acegisecurity.ldap.DefaultInitialDirContextFactory import org.acegisecurity.ldap.search.FilterBasedLdapUserSearch +import org.acegisecurity.providers.rememberme.RememberMeAuthenticationProvider /* Configure LDAP as the authentication realm. @@ -38,6 +39,11 @@ authenticationManager(ProviderManager) { providers = [ // talk to LDAP bean(LdapAuthenticationProvider,bindAuthenticator,authoritiesPopulator), + + // these providers apply everywhere + bean(RememberMeAuthenticationProvider) { + key = app.getSecretKey(); + }, // this doesn't mean we allow anonymous access. // we just authenticate anonymous users as such, // so that later authorization can reject them if so configured diff --git a/war/resources/WEB-INF/security/SecurityFilters.groovy b/war/resources/WEB-INF/security/SecurityFilters.groovy index c4428790fd0d6942a9ab17516bc9e40fa1c6c373..2ee2e8ab866baf08eec2c9154381cf2fda14ff54 100644 --- a/war/resources/WEB-INF/security/SecurityFilters.groovy +++ b/war/resources/WEB-INF/security/SecurityFilters.groovy @@ -14,6 +14,8 @@ import hudson.security.AccessDeniedHandlerImpl import hudson.security.BasicAuthenticationFilter import hudson.security.AuthenticationProcessingFilter2 import hudson.security.UnwrapSecurityExceptionFilter +import org.acegisecurity.ui.rememberme.RememberMeProcessingFilter +import org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices // providers that apply to both patterns def commonProviders(redirectUrl) { @@ -39,7 +41,7 @@ filter(ChainedServletFilter) { }, // allow clients to submit basic authentication credential bean(BasicProcessingFilter) { - authenticationManager = authenticationManager + authenticationManager = authenticationManagerProxy // if basic authentication fails (which only happens incorrect basic auth credential is sent), // respond with 401 with basic auth request, instead of redirecting the user to the login page, // since users of basic auth tends to be a program and won't see the redirection to the form @@ -48,8 +50,14 @@ filter(ChainedServletFilter) { realmName = "Hudson" } }, + bean(RememberMeProcessingFilter) { + rememberMeServices = bean(TokenBasedRememberMeServices) { + userDetailsService = userDetailsServiceProxy; + key = app.getSecretKey(); + } + }, bean(AuthenticationProcessingFilter2) { - authenticationManager = authenticationManager + authenticationManager = authenticationManagerProxy authenticationFailureUrl = "/loginError" defaultTargetUrl = "/" filterProcessesUrl = "/j_acegi_security_check"