package hudson.security; import com.octo.captcha.service.CaptchaServiceException; import com.octo.captcha.service.image.DefaultManageableImageCaptchaService; import groovy.lang.Binding; import hudson.ExtensionPoint; import hudson.model.Describable; import hudson.model.Descriptor; import hudson.model.Hudson; import hudson.util.DescriptorList; import hudson.util.PluginServletFilter; import hudson.util.spring.BeanBuilder; import org.acegisecurity.Authentication; import org.acegisecurity.AuthenticationManager; import org.acegisecurity.context.SecurityContext; import org.acegisecurity.ui.rememberme.RememberMeServices; import org.acegisecurity.userdetails.UserDetailsService; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.springframework.context.ApplicationContext; import org.springframework.web.context.WebApplicationContext; import javax.imageio.ImageIO; import javax.servlet.Filter; import javax.servlet.FilterConfig; import java.io.IOException; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; /** * Pluggable security realm that connects external user database to Hudson. * *
* New implementations should be registered to {@link #LIST}. * *
* If additional views/URLs need to be exposed, * an active {@link SecurityRealm} is bound to CONTEXT_ROOT/securityRealm/ * through {@link Hudson#getSecurityRealm()}, so you can define additional pages and * operations on your {@link SecurityRealm}. * *
* For compatibility reasons, there are two somewhat different ways to implement a custom SecurityRealm. * *
* 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. * *
* 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. * *
* This model is expected to fit most {@link SecurityRealm} implementations. * * *
* 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}. * *
* This model is for those "weird" implementations. * * *
* {@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 config.jelly and then * captured as instance variables inside the {@link SecurityRealm} implementation. * *
* Your {@link SecurityRealm} may also wants to alter {@link Filter} set up by * overriding {@link #createFilter(FilterConfig)}. */ public abstract SecurityComponents createSecurityComponents(); /** * {@inheritDoc} * *
* {@link SecurityRealm} is a singleton resource in Hudson, and therefore
* it's always configured through config.jelly and never with
* global.jelly.
*/
public abstract Descriptor
* If the implementation needs to redirect the user to a different URL
* for signing up, use the following jelly script as signup.jelly
*
*
* 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.
*
*
* For other plugins that want to contribute {@link Filter}, see
* {@link PluginServletFilter}.
*
* @since 1.271
*/
public Filter createFilter(FilterConfig filterConfig) {
Binding binding = new Binding();
SecurityComponents sc = this.createSecurityComponents();
binding.setVariable("securityComponents", sc);
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;
}
});
}
/**
* This special instance is not configurable explicitly,
* so it doesn't have a descriptor.
*/
public Descriptor
* None of the fields are ever null.
*
* @see SecurityRealm#createSecurityComponents()
*/
public static final class SecurityComponents {
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;
}
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.
*/
public static final DescriptorList
*/
public boolean allowsSignup() {
Class clz = getClass();
return clz.getClassLoader().getResource(clz.getName().replace('.','/')+"/signup.jelly")!=null;
}
/**
* {@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}.
*
* This method is intended to be used to pick up a Acegi object from
* spring once the bean definition file is parsed.
*/
protected static