/hudson-core/src/main/java/hudson/security/SecurityRealm.java
http://github.com/hudson/hudson · Java · 557 lines · 218 code · 46 blank · 293 comment · 13 complexity · b3957cba7cf60684759bcf0079524be6 MD5 · raw file
- /*
- * The MIT License
- *
- * Copyright (c) 2004-2011, Oracle Corporation, Kohsuke Kawaguchi, Nikita Levyankov
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
- package hudson.security;
- import com.octo.captcha.service.CaptchaServiceException;
- import com.octo.captcha.service.image.DefaultManageableImageCaptchaService;
- import groovy.lang.Binding;
- import hudson.DescriptorExtensionList;
- import hudson.EnvVars;
- import hudson.Extension;
- import hudson.ExtensionPoint;
- import hudson.cli.CLICommand;
- import hudson.model.AbstractDescribableImpl;
- import hudson.model.Descriptor;
- import hudson.model.Hudson;
- import hudson.security.FederatedLoginService.FederatedIdentity;
- import hudson.util.DescriptorList;
- import hudson.util.PluginServletFilter;
- import hudson.util.spring.BeanBuilder;
- import java.io.IOException;
- import java.util.Map;
- import java.util.logging.Level;
- import java.util.logging.Logger;
- import javax.imageio.ImageIO;
- import javax.servlet.Filter;
- import javax.servlet.FilterConfig;
- import javax.servlet.ServletException;
- import javax.servlet.http.Cookie;
- import javax.servlet.http.HttpSession;
- import org.acegisecurity.Authentication;
- import org.acegisecurity.AuthenticationManager;
- import org.acegisecurity.GrantedAuthority;
- import org.acegisecurity.GrantedAuthorityImpl;
- import org.acegisecurity.context.SecurityContext;
- import org.acegisecurity.context.SecurityContextHolder;
- import org.acegisecurity.ui.rememberme.RememberMeServices;
- import org.acegisecurity.userdetails.UserDetails;
- import org.acegisecurity.userdetails.UserDetailsService;
- import org.acegisecurity.userdetails.UsernameNotFoundException;
- 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.web.context.WebApplicationContext;
- import static org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY;
- /**
- * 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 Acegi 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 Acegi 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();
- /**
- * 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_acegi_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 req.getContextPath() + "/";
- }
- /**
- * 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(ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY, "");
- cookie.setPath(req.getContextPath().length() > 0 ? req.getContextPath() : "/");
- 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();
- }
- /**
- * {@link DefaultManageableImageCaptchaService} holder to defer initialization.
- */
- private static final class CaptchaService {
- private static final DefaultManageableImageCaptchaService INSTANCE = new DefaultManageableImageCaptchaService();
- }
- /**
- * Generates a captcha image.
- */
- public final void doCaptcha(StaplerRequest req, StaplerResponse rsp) throws IOException {
- String id = req.getSession().getId();
- rsp.setContentType("image/png");
- rsp.addHeader("Cache-Control", "no-cache");
- ImageIO.write(CaptchaService.INSTANCE.getImageChallengeForID(id), "PNG", rsp.getOutputStream());
- }
- /**
- * Validates the captcha.
- */
- protected final boolean validateCaptcha(String text) {
- try {
- String id = Stapler.getCurrentRequest().getSession().getId();
- Boolean b = CaptchaService.INSTANCE.validateResponseForID(id, text);
- return b != null && b;
- } catch (CaptchaServiceException e) {
- LOGGER.log(Level.INFO, "Captcha validation had a problem", e);
- return false;
- }
- }
- /**
- * 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 Acegi 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 chain from /WEB-INF/security/SecurityFilters.groovy.
- * 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) {
- LOGGER.entering(SecurityRealm.class.getName(), "createFilter");
- Binding binding = new Binding();
- SecurityComponents sc = getSecurityComponents();
- binding.setVariable("securityComponents", sc);
- binding.setVariable("securityRealm", this);
- BeanBuilder builder = new BeanBuilder();
- builder.parse(filterConfig.getServletContext().getResourceAsStream("/WEB-INF/security/SecurityFilters.groovy"),
- binding);
- WebApplicationContext context = builder.createApplicationContext();
- return (Filter) context.getBean("filter");
- }
- /**
- * 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(Hudson.getInstance().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");
- }