PageRenderTime 47ms CodeModel.GetById 2ms app.highlight 37ms RepoModel.GetById 2ms app.codeStats 0ms

/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
  1/*
  2 * The MIT License
  3 * 
  4 * Copyright (c) 2004-2011, Oracle Corporation, Kohsuke Kawaguchi, Nikita Levyankov
  5 * 
  6 * Permission is hereby granted, free of charge, to any person obtaining a copy
  7 * of this software and associated documentation files (the "Software"), to deal
  8 * in the Software without restriction, including without limitation the rights
  9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 10 * copies of the Software, and to permit persons to whom the Software is
 11 * furnished to do so, subject to the following conditions:
 12 * 
 13 * The above copyright notice and this permission notice shall be included in
 14 * all copies or substantial portions of the Software.
 15 * 
 16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 22 * THE SOFTWARE.
 23 */
 24package hudson.security;
 25
 26import com.octo.captcha.service.CaptchaServiceException;
 27import com.octo.captcha.service.image.DefaultManageableImageCaptchaService;
 28import groovy.lang.Binding;
 29import hudson.DescriptorExtensionList;
 30import hudson.EnvVars;
 31import hudson.Extension;
 32import hudson.ExtensionPoint;
 33import hudson.cli.CLICommand;
 34import hudson.model.AbstractDescribableImpl;
 35import hudson.model.Descriptor;
 36import hudson.model.Hudson;
 37import hudson.security.FederatedLoginService.FederatedIdentity;
 38import hudson.util.DescriptorList;
 39import hudson.util.PluginServletFilter;
 40import hudson.util.spring.BeanBuilder;
 41import java.io.IOException;
 42import java.util.Map;
 43import java.util.logging.Level;
 44import java.util.logging.Logger;
 45import javax.imageio.ImageIO;
 46import javax.servlet.Filter;
 47import javax.servlet.FilterConfig;
 48import javax.servlet.ServletException;
 49import javax.servlet.http.Cookie;
 50import javax.servlet.http.HttpSession;
 51import org.acegisecurity.Authentication;
 52import org.acegisecurity.AuthenticationManager;
 53import org.acegisecurity.GrantedAuthority;
 54import org.acegisecurity.GrantedAuthorityImpl;
 55import org.acegisecurity.context.SecurityContext;
 56import org.acegisecurity.context.SecurityContextHolder;
 57import org.acegisecurity.ui.rememberme.RememberMeServices;
 58import org.acegisecurity.userdetails.UserDetails;
 59import org.acegisecurity.userdetails.UserDetailsService;
 60import org.acegisecurity.userdetails.UsernameNotFoundException;
 61import org.kohsuke.stapler.HttpResponse;
 62import org.kohsuke.stapler.Stapler;
 63import org.kohsuke.stapler.StaplerRequest;
 64import org.kohsuke.stapler.StaplerResponse;
 65import org.springframework.context.ApplicationContext;
 66import org.springframework.dao.DataAccessException;
 67import org.springframework.web.context.WebApplicationContext;
 68
 69import static org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY;
 70
 71/**
 72 * Pluggable security realm that connects external user database to Hudson.
 73 * <p/>
 74 * <p/>
 75 * If additional views/URLs need to be exposed,
 76 * an active {@link SecurityRealm} is bound to <tt>CONTEXT_ROOT/securityRealm/</tt>
 77 * through {@link Hudson#getSecurityRealm()}, so you can define additional pages and
 78 * operations on your {@link SecurityRealm}.
 79 * <p/>
 80 * <h2>How do I implement this class?</h2>
 81 * <p/>
 82 * For compatibility reasons, there are two somewhat different ways to implement a custom SecurityRealm.
 83 * <p/>
 84 * <p/>
 85 * One is to override the {@link #createSecurityComponents()} and create key Acegi components
 86 * that control the authentication process.
 87 * The default {@link SecurityRealm#createFilter(FilterConfig)} implementation then assembles them
 88 * into a chain of {@link Filter}s. All the incoming requests to Hudson go through this filter chain,
 89 * and when the filter chain is done, {@link SecurityContext#getAuthentication()} would tell us
 90 * who the current user is.
 91 * <p/>
 92 * <p/>
 93 * If your {@link SecurityRealm} needs to touch the default {@link Filter} chain configuration
 94 * (e.g., adding new ones), then you can also override {@link #createFilter(FilterConfig)} to do so.
 95 * <p/>
 96 * <p/>
 97 * This model is expected to fit most {@link SecurityRealm} implementations.
 98 * <p/>
 99 * <p/>
100 * <p/>
101 * The other way of doing this is to ignore {@link #createSecurityComponents()} completely (by returning
102 * {@link SecurityComponents} created by the default constructor) and just concentrate on {@link #createFilter(FilterConfig)}.
103 * As long as the resulting filter chain properly sets up {@link Authentication} object at the end of the processing,
104 * Hudson doesn't really need you to fit the standard Acegi models like {@link AuthenticationManager} and
105 * {@link UserDetailsService}.
106 * <p/>
107 * <p/>
108 * This model is for those "weird" implementations.
109 * <p/>
110 * <p/>
111 * <h2>Views</h2>
112 * <dl>
113 * <dt>loginLink.jelly</dt>
114 * <dd>
115 * This view renders the login link on the top right corner of every page, when the user
116 * is anonymous. For {@link SecurityRealm}s that support user sign-up, this is a good place
117 * to show a "sign up" link. See {@link HudsonPrivateSecurityRealm} implementation
118 * for an example of this.
119 * <p/>
120 * <dt>config.jelly</dt>
121 * <dd>
122 * This view is used to render the configuration page in the system config screen.
123 * </dl>
124 *
125 * @author Kohsuke Kawaguchi
126 * @author Nikita Levyankov
127 * @see PluginServletFilter
128 * @since 1.160
129 */
130public abstract class SecurityRealm extends AbstractDescribableImpl<SecurityRealm> implements ExtensionPoint {
131
132    /**
133     * Creates fully-configured {@link AuthenticationManager} that performs authentication
134     * against the user realm. The implementation hides how such authentication manager
135     * is configured.
136     * <p/>
137     * <p/>
138     * {@link AuthenticationManager} instantiation often depends on the user-specified parameters
139     * (for example, if the authentication is based on LDAP, the user needs to specify
140     * the host name of the LDAP server.) Such configuration is expected to be
141     * presented to the user via <tt>config.jelly</tt> and then
142     * captured as instance variables inside the {@link SecurityRealm} implementation.
143     * <p/>
144     * <p/>
145     * Your {@link SecurityRealm} may also wants to alter {@link Filter} set up by
146     * overriding {@link #createFilter(FilterConfig)}.
147     */
148    public abstract SecurityComponents createSecurityComponents();
149
150    /**
151     * Creates a {@link CliAuthenticator} object that authenticates an invocation of a CLI command.
152     * See {@link CliAuthenticator} for more details.
153     *
154     * @param command The command about to be executed.
155     * @return never null. By default, this method returns a no-op authenticator that always authenticates
156     *         the session as authenticated by the transport (which is often just {@link Hudson#ANONYMOUS}.)
157     */
158    public CliAuthenticator createCliAuthenticator(final CLICommand command) {
159        return new CliAuthenticator() {
160            public Authentication authenticate() {
161                return command.getTransportAuthentication();
162            }
163        };
164    }
165
166    /**
167     * {@inheritDoc}
168     * <p/>
169     * <p/>
170     * {@link SecurityRealm} is a singleton resource in Hudson, and therefore
171     * it's always configured through <tt>config.jelly</tt> and never with
172     * <tt>global.jelly</tt>.
173     */
174    public Descriptor<SecurityRealm> getDescriptor() {
175        return super.getDescriptor();
176    }
177
178    /**
179     * Returns the URL to submit a form for the authentication.
180     * There's no need to override this, except for {@link LegacySecurityRealm}.
181     */
182    public String getAuthenticationGatewayUrl() {
183        return "j_acegi_security_check";
184    }
185
186    /**
187     * Gets the target URL of the "login" link.
188     * There's no need to override this, except for {@link LegacySecurityRealm}.
189     * On legacy implementation this should point to {@code loginEntry}, which
190     * is protected by <tt>web.xml</tt>, so that the user can be eventually authenticated
191     * by the container.
192     * <p/>
193     * <p/>
194     * Path is relative from the context root of the Hudson application.
195     * The URL returned by this method will get the "from" query parameter indicating
196     * the page that the user was at.
197     */
198    public String getLoginUrl() {
199        return "login";
200    }
201
202    /**
203     * Returns true if this {@link SecurityRealm} supports explicit logout operation.
204     * <p/>
205     * <p/>
206     * If the method returns false, "logout" link will not be displayed. This is useful
207     * when authentication doesn't require an explicit login activity (such as NTLM authentication
208     * or Kerberos authentication, where Hudson has no ability to log off the current user.)
209     * <p/>
210     * <p/>
211     * By default, this method returns true.
212     *
213     * @since 1.307
214     */
215    public boolean canLogOut() {
216        return true;
217    }
218
219    /**
220     * Controls where the user is sent to after a logout. By default, it's the top page
221     * of Hudson, but you can return arbitrary URL.
222     *
223     * @param req {@link StaplerRequest} that represents the current request. Primarily so that
224     * you can get the context path. By the time this method is called, the session
225     * is already invalidated. Never null.
226     * @param auth The {@link Authentication} object that represents the user that was logging in.
227     * This parameter allows you to redirect people to different pages depending on who they are.
228     * @return never null.
229     * @see #doLogout(StaplerRequest, StaplerResponse)
230     * @since 1.314
231     */
232    protected String getPostLogOutUrl(StaplerRequest req, Authentication auth) {
233        return req.getContextPath() + "/";
234    }
235
236    /**
237     * Handles the logout processing.
238     * <p/>
239     * <p/>
240     * The default implementation erases the session and do a few other clean up, then
241     * redirect the user to the URL specified by {@link #getPostLogOutUrl(StaplerRequest, Authentication)}.
242     *
243     * @since 1.314
244     */
245    public void doLogout(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
246        HttpSession session = req.getSession(false);
247        if (session != null) {
248            session.invalidate();
249        }
250        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
251        SecurityContextHolder.clearContext();
252
253        //Clear env property.
254        EnvVars.clearHudsonUserEnvVar();
255
256        // reset remember-me cookie
257        Cookie cookie = new Cookie(ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY, "");
258        cookie.setPath(req.getContextPath().length() > 0 ? req.getContextPath() : "/");
259        rsp.addCookie(cookie);
260
261        rsp.sendRedirect2(getPostLogOutUrl(req, auth));
262    }
263
264    /**
265     * Returns true if this {@link SecurityRealm} allows online sign-up.
266     * This creates a hyperlink that redirects users to <tt>CONTEXT_ROOT/signUp</tt>,
267     * which will be served by the <tt>signup.jelly</tt> view of this class.
268     * <p/>
269     * <p/>
270     * If the implementation needs to redirect the user to a different URL
271     * for signing up, use the following jelly script as <tt>signup.jelly</tt>
272     * <p/>
273     * <pre><xmp>
274     * <st:redirect url="http://www.sun.com/" xmlns:st="jelly:stapler"/>
275     * </xmp></pre>
276     */
277    public boolean allowsSignup() {
278        Class clz = getClass();
279        return clz.getClassLoader().getResource(clz.getName().replace('.', '/') + "/signup.jelly") != null;
280    }
281
282    /**
283     * Shortcut for {@link UserDetailsService#loadUserByUsername(String)}.
284     *
285     * @return never null.
286     * @throws UserMayOrMayNotExistException If the security realm cannot even tell if the user exists or not.
287     */
288    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
289        return getSecurityComponents().userDetails.loadUserByUsername(username);
290    }
291
292    /**
293     * If this {@link SecurityRealm} supports a look up of {@link GroupDetails} by their names, override this method
294     * to provide the look up.
295     * <p/>
296     * <p/>
297     * This information, when available, can be used by {@link AuthorizationStrategy}s to improve the UI and
298     * error diagnostics for the user.
299     */
300    public GroupDetails loadGroupByGroupname(String groupname) throws UsernameNotFoundException, DataAccessException {
301        throw new UserMayOrMayNotExistException(groupname);
302    }
303
304    /**
305     * Starts the user registration process for a new user that has the given verified identity.
306     * <p/>
307     * <p/>
308     * If the user logs in through a {@link FederatedLoginService}, verified that the current user
309     * owns an {@linkplain FederatedIdentity identity}, but no existing user account has claimed that identity,
310     * then this method is invoked.
311     * <p/>
312     * <p/>
313     * The expected behaviour is to confirm that the user would like to create a new account, and
314     * associate this federated identity to the newly created account (via {@link FederatedIdentity#addToCurrentUser()}.
315     *
316     * @throws UnsupportedOperationException If this implementation doesn't support the signup through this mechanism.
317     *                                       This is the default implementation.
318     * @since 1.394
319     */
320    public HttpResponse commenceSignup(FederatedIdentity identity) {
321        throw new UnsupportedOperationException();
322    }
323
324    /**
325     * {@link DefaultManageableImageCaptchaService} holder to defer initialization.
326     */
327    private static final class CaptchaService {
328        private static final DefaultManageableImageCaptchaService INSTANCE = new DefaultManageableImageCaptchaService();
329    }
330
331    /**
332     * Generates a captcha image.
333     */
334    public final void doCaptcha(StaplerRequest req, StaplerResponse rsp) throws IOException {
335        String id = req.getSession().getId();
336        rsp.setContentType("image/png");
337        rsp.addHeader("Cache-Control", "no-cache");
338        ImageIO.write(CaptchaService.INSTANCE.getImageChallengeForID(id), "PNG", rsp.getOutputStream());
339    }
340
341    /**
342     * Validates the captcha.
343     */
344    protected final boolean validateCaptcha(String text) {
345        try {
346            String id = Stapler.getCurrentRequest().getSession().getId();
347            Boolean b = CaptchaService.INSTANCE.validateResponseForID(id, text);
348            return b != null && b;
349        } catch (CaptchaServiceException e) {
350            LOGGER.log(Level.INFO, "Captcha validation had a problem", e);
351            return false;
352        }
353    }
354
355    /**
356     * Picks up the instance of the given type from the spring context.
357     * If there are multiple beans of the same type or if there are none,
358     * this method treats that as an {@link IllegalArgumentException}.
359     * <p/>
360     * This method is intended to be used to pick up a Acegi object from
361     * spring once the bean definition file is parsed.
362     */
363    protected static <T> T findBean(Class<T> type, ApplicationContext context) {
364        Map m = context.getBeansOfType(type);
365        switch (m.size()) {
366            case 0:
367                throw new IllegalArgumentException("No beans of " + type + " are defined");
368            case 1:
369                return type.cast(m.values().iterator().next());
370            default:
371                throw new IllegalArgumentException("Multiple beans of " + type + " are defined: " + m);
372        }
373    }
374
375    /**
376     * Holder for the SecurityComponents.
377     */
378    private transient SecurityComponents securityComponents;
379
380    /**
381     * Use this function to get the security components, without necessarily
382     * recreating them.
383     */
384    public synchronized SecurityComponents getSecurityComponents() {
385        if (this.securityComponents == null) {
386            this.securityComponents = this.createSecurityComponents();
387        }
388        return this.securityComponents;
389    }
390
391    /**
392     * Creates {@link Filter} that all the incoming HTTP requests will go through
393     * for authentication.
394     * <p/>
395     * <p/>
396     * The default implementation uses {@link #getSecurityComponents()} and builds
397     * a standard filter chain from /WEB-INF/security/SecurityFilters.groovy.
398     * But subclasses can override this to completely change the filter sequence.
399     * <p/>
400     * <p/>
401     * For other plugins that want to contribute {@link Filter}, see
402     * {@link PluginServletFilter}.
403     *
404     * @since 1.271
405     */
406    public Filter createFilter(FilterConfig filterConfig) {
407        LOGGER.entering(SecurityRealm.class.getName(), "createFilter");
408
409        Binding binding = new Binding();
410        SecurityComponents sc = getSecurityComponents();
411        binding.setVariable("securityComponents", sc);
412        binding.setVariable("securityRealm", this);
413        BeanBuilder builder = new BeanBuilder();
414        builder.parse(filterConfig.getServletContext().getResourceAsStream("/WEB-INF/security/SecurityFilters.groovy"),
415            binding);
416        WebApplicationContext context = builder.createApplicationContext();
417        return (Filter) context.getBean("filter");
418    }
419
420    /**
421     * Singleton constant that represents "no authentication."
422     */
423    public static final SecurityRealm NO_AUTHENTICATION = new None();
424
425    private static class None extends SecurityRealm {
426        public SecurityComponents createSecurityComponents() {
427            return new SecurityComponents(new AuthenticationManager() {
428                public Authentication authenticate(Authentication authentication) {
429                    return authentication;
430                }
431            }, new UserDetailsService() {
432                public UserDetails loadUserByUsername(String username)
433                    throws UsernameNotFoundException, DataAccessException {
434                    throw new UsernameNotFoundException(username);
435                }
436            });
437        }
438
439        /**
440         * This special instance is not configurable explicitly,
441         * so it doesn't have a descriptor.
442         */
443        @Override
444        public Descriptor<SecurityRealm> getDescriptor() {
445            return null;
446        }
447
448        /**
449         * There's no group.
450         */
451        @Override
452        public GroupDetails loadGroupByGroupname(String groupname)
453            throws UsernameNotFoundException, DataAccessException {
454            throw new UsernameNotFoundException(groupname);
455        }
456
457        /**
458         * We don't need any filter for this {@link SecurityRealm}.
459         */
460        @Override
461        public Filter createFilter(FilterConfig filterConfig) {
462            return new ChainedServletFilter();
463        }
464
465        /**
466         * Maintain singleton semantics.
467         */
468        private Object readResolve() {
469            return NO_AUTHENTICATION;
470        }
471    }
472
473    /**
474     * Just a tuple so that we can create various inter-related security related objects and
475     * return them all at once.
476     * <p/>
477     * <p/>
478     * None of the fields are ever null.
479     *
480     * @see SecurityRealm#createSecurityComponents()
481     */
482    public static final class SecurityComponents {
483        //TODO: review and check whether we can do it private
484        public final AuthenticationManager manager;
485        public final UserDetailsService userDetails;
486        public final RememberMeServices rememberMe;
487
488        public SecurityComponents() {
489            // we use AuthenticationManagerProxy here just as an implementation that fails all the time,
490            // not as a proxy. No one is supposed to use this as a proxy.
491            this(new AuthenticationManagerProxy());
492        }
493
494        public SecurityComponents(AuthenticationManager manager) {
495            // we use UserDetailsServiceProxy here just as an implementation that fails all the time,
496            // not as a proxy. No one is supposed to use this as a proxy.
497            this(manager, new UserDetailsServiceProxy());
498        }
499
500        public SecurityComponents(AuthenticationManager manager, UserDetailsService userDetails) {
501            this(manager, userDetails, createRememberMeService(userDetails));
502        }
503
504        public SecurityComponents(AuthenticationManager manager, UserDetailsService userDetails,
505                                  RememberMeServices rememberMe) {
506            assert manager != null && userDetails != null && rememberMe != null;
507            this.manager = manager;
508            this.userDetails = userDetails;
509            this.rememberMe = rememberMe;
510        }
511
512        public AuthenticationManager getManager() {
513            return manager;
514        }
515
516        public UserDetailsService getUserDetails() {
517            return userDetails;
518        }
519
520        public RememberMeServices getRememberMe() {
521            return rememberMe;
522        }
523
524        private static RememberMeServices createRememberMeService(UserDetailsService uds) {
525            // create our default TokenBasedRememberMeServices, which depends on the availability of the secret key
526            TokenBasedRememberMeServices2 rms = new TokenBasedRememberMeServices2();
527            rms.setUserDetailsService(uds);
528            rms.setKey(Hudson.getInstance().getSecretKey());
529            rms.setParameter("remember_me"); // this is the form field name in login.jelly
530            return rms;
531        }
532    }
533
534    /**
535     * All registered {@link SecurityRealm} implementations.
536     *
537     * @deprecated as of 1.286
538     *             Use {@link #all()} for read access, and use {@link Extension} for registration.
539     */
540    public static final DescriptorList<SecurityRealm> LIST = new DescriptorList<SecurityRealm>(SecurityRealm.class);
541
542    /**
543     * Returns all the registered {@link SecurityRealm} descriptors.
544     */
545    public static DescriptorExtensionList<SecurityRealm, Descriptor<SecurityRealm>> all() {
546        return Hudson.getInstance().<SecurityRealm, Descriptor<SecurityRealm>>getDescriptorList(SecurityRealm.class);
547    }
548
549
550    private static final Logger LOGGER = Logger.getLogger(SecurityRealm.class.getName());
551
552    /**
553     * {@link GrantedAuthority} that represents the built-in "authenticated" role, which is granted to
554     * anyone non-anonymous.
555     */
556    public static final GrantedAuthority AUTHENTICATED_AUTHORITY = new GrantedAuthorityImpl("authenticated");
557}