/******************************************************************************* * * Copyright (c) 2004-2011 Oracle Corporation. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * * Kohsuke Kawaguchi, Nikita Levyankov, Winston Prakash * *******************************************************************************/ package hudson.security; import hudson.DescriptorExtensionList; import hudson.EnvVars; import hudson.Extension; import hudson.ExtensionPoint; import hudson.Functions; import hudson.cli.CLICommand; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; import hudson.model.Hudson; import hudson.security.FederatedLoginService.FederatedIdentity; import org.eclipse.hudson.security.captcha.CaptchaSupport; import hudson.util.DescriptorList; import hudson.util.PluginServletFilter; import java.io.IOException; import java.util.List; import java.util.ArrayList; import java.util.Arrays; import java.util.Map; import java.util.logging.Logger; import javax.servlet.Filter; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpSession; import org.eclipse.hudson.security.HudsonSecurityEntitiesHolder; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.springframework.context.ApplicationContext; import org.springframework.dao.DataAccessException; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.GrantedAuthorityImpl; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; 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.core.userdetails.memory.UserAttribute; import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; import org.springframework.security.web.authentication.RememberMeServices; import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter; import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; /** * Pluggable security realm that connects external user database to Hudson. * <p/> * < * p/> * If additional views/URLs need to be exposed, an active {@link SecurityRealm} * is bound to <tt>CONTEXT_ROOT/securityRealm/</tt> through * {@link Hudson#getSecurityRealm()}, so you can define additional pages and * operations on your {@link SecurityRealm}. * <p/> * <h2>How do I implement this class?</h2> * <p/> * For compatibility reasons, there are two somewhat different ways to implement * a custom SecurityRealm. * <p/> * < * p/> * One is to override the {@link #createSecurityComponents()} and create key * Spring Security components that control the authentication process. The * default {@link SecurityRealm#createFilter(FilterConfig)} implementation then * assembles them into a chain of {@link Filter}s. All the incoming requests to * Hudson go through this filter chain, and when the filter chain is done, * {@link SecurityContext#getAuthentication()} would tell us who the current * user is. * <p/> * < * p/> * If your {@link SecurityRealm} needs to touch the default {@link Filter} chain * configuration (e.g., adding new ones), then you can also override * {@link #createFilter(FilterConfig)} to do so. * <p/> * < * p/> * This model is expected to fit most {@link SecurityRealm} implementations. * <p/> * < * p/> * < * p/> * The other way of doing this is to ignore {@link #createSecurityComponents()} * completely (by returning {@link SecurityComponents} created by the default * constructor) and just concentrate on {@link #createFilter(FilterConfig)}. As * long as the resulting filter chain properly sets up {@link Authentication} * object at the end of the processing, Hudson doesn't really need you to fit * the standard Spring Security models like {@link AuthenticationManager} and * {@link UserDetailsService}. * <p/> * < * p/> * This model is for those "weird" implementations. * <p/> * < * p/> * <h2>Views</h2> <dl> <dt>loginLink.jelly</dt> <dd> This view renders the login * link on the top right corner of every page, when the user is anonymous. For * {@link SecurityRealm}s that support user sign-up, this is a good place to * show a "sign up" link. See {@link HudsonPrivateSecurityRealm} implementation * for an example of this. * <p/> * <dt>config.jelly</dt> <dd> This view is used to render the configuration page * in the system config screen. </dl> * * @author Kohsuke Kawaguchi * @author Nikita Levyankov * @see PluginServletFilter * @since 1.160 */ public abstract class SecurityRealm extends AbstractDescribableImpl<SecurityRealm> implements ExtensionPoint { /** * Creates fully-configured {@link AuthenticationManager} that performs * authentication against the user realm. The implementation hides how such * authentication manager is configured. * <p/> * < * p/> * {@link AuthenticationManager} instantiation often depends on the * user-specified parameters (for example, if the authentication is based on * LDAP, the user needs to specify the host name of the LDAP server.) Such * configuration is expected to be presented to the user via * <tt>config.jelly</tt> and then captured as instance variables inside the * {@link SecurityRealm} implementation. * <p/> * < * p/> * Your {@link SecurityRealm} may also wants to alter {@link Filter} set up * by overriding {@link #createFilter(FilterConfig)}. */ public abstract SecurityComponents createSecurityComponents(); /** * Captcha Support to be used with this SecurityRealm for User Signup */ private CaptchaSupport captchaSupport; /** * Creates a {@link CliAuthenticator} object that authenticates an * invocation of a CLI command. See {@link CliAuthenticator} for more * details. * * @param command The command about to be executed. * @return never null. By default, this method returns a no-op authenticator * that always authenticates the session as authenticated by the transport * (which is often just {@link Hudson#ANONYMOUS}.) */ public CliAuthenticator createCliAuthenticator(final CLICommand command) { return new CliAuthenticator() { public Authentication authenticate() { return command.getTransportAuthentication(); } }; } /** * {@inheritDoc} * <p/> * < * p/> * {@link SecurityRealm} is a singleton resource in Hudson, and therefore * it's always configured through <tt>config.jelly</tt> and never with * <tt>global.jelly</tt>. */ public Descriptor<SecurityRealm> getDescriptor() { return super.getDescriptor(); } /** * Returns the URL to submit a form for the authentication. There's no need * to override this, except for {@link LegacySecurityRealm}. */ public String getAuthenticationGatewayUrl() { return "j_spring_security_check"; } /** * Gets the target URL of the "login" link. There's no need to override * this, except for {@link LegacySecurityRealm}. On legacy implementation * this should point to {@code loginEntry}, which is protected by * <tt>web.xml</tt>, so that the user can be eventually authenticated by the * container. * <p/> * < * p/> * Path is relative from the context root of the Hudson application. The URL * returned by this method will get the "from" query parameter indicating * the page that the user was at. */ public String getLoginUrl() { return "login"; } /** * Returns true if this {@link SecurityRealm} supports explicit logout * operation. * <p/> * < * p/> * If the method returns false, "logout" link will not be displayed. This is * useful when authentication doesn't require an explicit login activity * (such as NTLM authentication or Kerberos authentication, where Hudson has * no ability to log off the current user.) * <p/> * < * p/> * By default, this method returns true. * * @since 1.307 */ public boolean canLogOut() { return true; } /** * Controls where the user is sent to after a logout. By default, it's the * top page of Hudson, but you can return arbitrary URL. * * @param req {@link StaplerRequest} that represents the current request. * Primarily so that you can get the context path. By the time this method * is called, the session is already invalidated. Never null. * @param auth The {@link Authentication} object that represents the user * that was logging in. This parameter allows you to redirect people to * different pages depending on who they are. * @return never null. * @see #doLogout(StaplerRequest, StaplerResponse) * @since 1.314 */ protected String getPostLogOutUrl(StaplerRequest req, Authentication auth) { return Functions.getRequestRootPath(req) + "/"; } public CaptchaSupport getCaptchaSupport() { return captchaSupport; } public void setCaptchaSupport(CaptchaSupport captchaSupport) { this.captchaSupport = captchaSupport; } public List<Descriptor<CaptchaSupport>> getCaptchaSupportDescriptors() { return CaptchaSupport.all(); } /** * Handles the logout processing. * <p/> * < * p/> * The default implementation erases the session and do a few other clean * up, then redirect the user to the URL specified by * {@link #getPostLogOutUrl(StaplerRequest, Authentication)}. * * @since 1.314 */ public void doLogout(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { HttpSession session = req.getSession(false); if (session != null) { session.invalidate(); } Authentication auth = SecurityContextHolder.getContext().getAuthentication(); SecurityContextHolder.clearContext(); //Clear env property. EnvVars.clearHudsonUserEnvVar(); // reset remember-me cookie Cookie cookie = new Cookie(TokenBasedRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, ""); cookie.setPath(Functions.getRequestRootPath(req).length() > 0 ? Functions.getRequestRootPath(req) : "/"); rsp.addCookie(cookie); rsp.sendRedirect2(getPostLogOutUrl(req, auth)); } /** * Returns true if this {@link SecurityRealm} allows online sign-up. This * creates a hyperlink that redirects users to <tt>CONTEXT_ROOT/signUp</tt>, * which will be served by the <tt>signup.jelly</tt> view of this class. * <p/> * < * p/> * If the implementation needs to redirect the user to a different URL for * signing up, use the following jelly script as <tt>signup.jelly</tt> * <p/> * < * pre><xmp> * <st:redirect url="http://www.sun.com/" xmlns:st="jelly:stapler"/> * </xmp></pre> */ public boolean allowsSignup() { Class clz = getClass(); return clz.getClassLoader().getResource(clz.getName().replace('.', '/') + "/signup.jelly") != null; } /** * Shortcut for {@link UserDetailsService#loadUserByUsername(String)}. * * @return never null. * @throws UserMayOrMayNotExistException If the security realm cannot even * tell if the user exists or not. */ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { return getSecurityComponents().userDetails.loadUserByUsername(username); } /** * If this {@link SecurityRealm} supports a look up of {@link GroupDetails} * by their names, override this method to provide the look up. * <p/> * < * p/> * This information, when available, can be used by * {@link AuthorizationStrategy}s to improve the UI and error diagnostics * for the user. */ public GroupDetails loadGroupByGroupname(String groupname) throws UsernameNotFoundException, DataAccessException { throw new UserMayOrMayNotExistException(groupname); } /** * Starts the user registration process for a new user that has the given * verified identity. * <p/> * < * p/> * If the user logs in through a {@link FederatedLoginService}, verified * that the current user owns an {@linkplain FederatedIdentity identity}, * but no existing user account has claimed that identity, then this method * is invoked. * <p/> * < * p/> * The expected behaviour is to confirm that the user would like to create a * new account, and associate this federated identity to the newly created * account (via {@link FederatedIdentity#addToCurrentUser()}. * * @throws UnsupportedOperationException If this implementation doesn't * support the signup through this mechanism. This is the default * implementation. * @since 1.394 */ public HttpResponse commenceSignup(FederatedIdentity identity) { throw new UnsupportedOperationException(); } /** * Generates a captcha image. */ public final void doCaptcha(StaplerRequest req, StaplerResponse rsp) throws IOException { if (captchaSupport != null) { String id = req.getSession().getId(); rsp.setContentType("image/png"); rsp.addHeader("Cache-Control", "no-cache"); captchaSupport.generateImage(id, rsp.getOutputStream()); } } /** * Validates the captcha. */ protected final boolean validateCaptcha(String text) { if (captchaSupport != null) { String id = Stapler.getCurrentRequest().getSession().getId(); return captchaSupport.validateCaptcha(id, text); } // If no Captcha Support then bogus validation always retuers true return true; } /** * Picks up the instance of the given type from the spring context. If there * are multiple beans of the same type or if there are none, this method * treats that as an {@link IllegalArgumentException}. * <p/> * This method is intended to be used to pick up a Spring Security object * from spring once the bean definition file is parsed. */ protected static <T> T findBean(Class<T> type, ApplicationContext context) { Map m = context.getBeansOfType(type); switch (m.size()) { case 0: throw new IllegalArgumentException("No beans of " + type + " are defined"); case 1: return type.cast(m.values().iterator().next()); default: throw new IllegalArgumentException("Multiple beans of " + type + " are defined: " + m); } } /** * Holder for the SecurityComponents. */ private transient SecurityComponents securityComponents; /** * Use this function to get the security components, without necessarily * recreating them. */ public synchronized SecurityComponents getSecurityComponents() { if (this.securityComponents == null) { this.securityComponents = this.createSecurityComponents(); } return this.securityComponents; } /** * Creates {@link Filter} that all the incoming HTTP requests will go * through for authentication. * <p/> * < * p/> * The default implementation uses {@link #getSecurityComponents()} and * builds a standard filter But subclasses can override this to completely * change the filter sequence. * <p/> * < * p/> * For other plugins that want to contribute {@link Filter}, see * {@link PluginServletFilter}. * * @since 1.271 */ public Filter createFilter(FilterConfig filterConfig) throws ServletException { LOGGER.entering(SecurityRealm.class.getName(), "createFilter"); List<Filter> filters = new ArrayList<Filter>(); SecurityComponents sc = getSecurityComponents(); HttpSessionContextIntegrationFilter2 httpSessionContextIntegrationFilter = new HttpSessionContextIntegrationFilter2(); filters.add(httpSessionContextIntegrationFilter); // 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 // page as a failure org.springframework.security.web.authentication.www.BasicAuthenticationFilter basicProcessingFilter = new org.springframework.security.web.authentication.www.BasicAuthenticationFilter(sc.getManager()); BasicAuthenticationEntryPoint basicProcessingFilterEntryPoint = new BasicAuthenticationEntryPoint(); basicProcessingFilterEntryPoint.setRealmName("Hudson"); basicProcessingFilter.setAuthenticationEntryPoint(basicProcessingFilterEntryPoint); filters.add(basicProcessingFilter); AuthenticationProcessingFilter2 authenticationProcessingFilter = new AuthenticationProcessingFilter2(); authenticationProcessingFilter.setAuthenticationManager(sc.getManager()); authenticationProcessingFilter.setRememberMeServices(sc.getRememberMe()); // authenticationProcessingFilter.setAuthenticationFailureUrl("/loginError"); // authenticationProcessingFilter.setDefaultTargetUrl("/"); authenticationProcessingFilter.setFilterProcessesUrl("/j_spring_security_check"); filters.add(authenticationProcessingFilter); RememberMeAuthenticationFilter rememberMeProcessingFilter = new RememberMeAuthenticationFilter(sc.getManager(), sc.getRememberMe()); filters.add(rememberMeProcessingFilter); filters.addAll(Arrays.asList(getCommonFilters())); return new ChainedServletFilter(filters); } public Filter[] getCommonFilters() { AnonymousAuthenticationFilter anonymousProcessingFilter = new AnonymousAuthenticationFilter("anonymous"); UserAttribute userAttribute = new UserAttribute(); userAttribute.setPassword("anonymous"); String authorities = "anonymous, ROLE_ANONYMOUS"; userAttribute.setAuthoritiesAsString(Arrays.asList(authorities)); anonymousProcessingFilter.setUserAttribute(userAttribute); ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(); AccessDeniedHandlerImpl accessDeniedHandler = new AccessDeniedHandlerImpl(); exceptionTranslationFilter.setAccessDeniedHandler(accessDeniedHandler); HudsonAuthenticationEntryPoint hudsonAuthenticationEntryPoint = new HudsonAuthenticationEntryPoint(); hudsonAuthenticationEntryPoint.setLoginFormUrl('/' + getLoginUrl() + "?from={0}"); exceptionTranslationFilter.setAuthenticationEntryPoint(hudsonAuthenticationEntryPoint); UnwrapSecurityExceptionFilter unwrapSecurityExceptionFilter = new UnwrapSecurityExceptionFilter(); Filter[] filters = { anonymousProcessingFilter, exceptionTranslationFilter, unwrapSecurityExceptionFilter }; return filters; } /** * Singleton constant that represents "no authentication." */ public static final SecurityRealm NO_AUTHENTICATION = new None(); private static class None extends SecurityRealm { public SecurityComponents createSecurityComponents() { return new SecurityComponents(new AuthenticationManager() { public Authentication authenticate(Authentication authentication) { return authentication; } }, new UserDetailsService() { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { throw new UsernameNotFoundException(username); } }); } /** * This special instance is not configurable explicitly, so it doesn't * have a descriptor. */ @Override public Descriptor<SecurityRealm> getDescriptor() { return null; } /** * There's no group. */ @Override public GroupDetails loadGroupByGroupname(String groupname) throws UsernameNotFoundException, DataAccessException { throw new UsernameNotFoundException(groupname); } /** * We don't need any filter for this {@link SecurityRealm}. */ @Override public Filter createFilter(FilterConfig filterConfig) { return new ChainedServletFilter(); } /** * Maintain singleton semantics. */ private Object readResolve() { return NO_AUTHENTICATION; } } /** * Just a tuple so that we can create various inter-related security related * objects and return them all at once. * <p/> * < * p/> * None of the fields are ever null. * * @see SecurityRealm#createSecurityComponents() */ public static final class SecurityComponents { //TODO: review and check whether we can do it private public final AuthenticationManager manager; public final UserDetailsService userDetails; public final RememberMeServices rememberMe; public SecurityComponents() { // we use AuthenticationManagerProxy here just as an implementation that fails all the time, // not as a proxy. No one is supposed to use this as a proxy. this(new AuthenticationManagerProxy()); } public SecurityComponents(AuthenticationManager manager) { // we use UserDetailsServiceProxy here just as an implementation that fails all the time, // not as a proxy. No one is supposed to use this as a proxy. this(manager, new UserDetailsServiceProxy()); } public SecurityComponents(AuthenticationManager manager, UserDetailsService userDetails) { this(manager, userDetails, createRememberMeService(userDetails)); } public SecurityComponents(AuthenticationManager manager, UserDetailsService userDetails, RememberMeServices rememberMe) { assert manager != null && userDetails != null && rememberMe != null; this.manager = manager; this.userDetails = userDetails; this.rememberMe = rememberMe; } public AuthenticationManager getManager() { return manager; } public UserDetailsService getUserDetails() { return userDetails; } public RememberMeServices getRememberMe() { return rememberMe; } private static RememberMeServices createRememberMeService(UserDetailsService uds) { // create our default TokenBasedRememberMeServices, which depends on the availability of the secret key TokenBasedRememberMeServices2 rms = new TokenBasedRememberMeServices2(); rms.setUserDetailsService(uds); rms.setKey(HudsonSecurityEntitiesHolder.getHudsonSecurityManager().getSecretKey()); rms.setParameter("remember_me"); // this is the form field name in login.jelly return rms; } } /** * All registered {@link SecurityRealm} implementations. * * @deprecated as of 1.286 Use {@link #all()} for read access, and use * {@link Extension} for registration. */ public static final DescriptorList<SecurityRealm> LIST = new DescriptorList<SecurityRealm>(SecurityRealm.class); /** * Returns all the registered {@link SecurityRealm} descriptors. */ public static DescriptorExtensionList<SecurityRealm, Descriptor<SecurityRealm>> all() { return Hudson.getInstance().<SecurityRealm, Descriptor<SecurityRealm>>getDescriptorList(SecurityRealm.class); } private static final Logger LOGGER = Logger.getLogger(SecurityRealm.class.getName()); /** * {@link GrantedAuthority} that represents the built-in "authenticated" * role, which is granted to anyone non-anonymous. */ public static final GrantedAuthority AUTHENTICATED_AUTHORITY = new GrantedAuthorityImpl("authenticated"); }