package hudson.plugins.sourceip;
import hudson.Extension;
import hudson.Util;
import static hudson.Util.fixEmptyAndTrim;
import hudson.model.Descriptor;
import hudson.security.GroupDetails;
import hudson.security.SecurityRealm;
import hudson.util.FormValidation;
import org.acegisecurity.Authentication;
import org.acegisecurity.AuthenticationCredentialsNotFoundException;
import org.acegisecurity.AuthenticationException;
import org.acegisecurity.AuthenticationManager;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.GrantedAuthorityImpl;
import org.acegisecurity.context.SecurityContextHolder;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
import org.acegisecurity.userdetails.User;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.UserDetailsService;
import org.acegisecurity.userdetails.UsernameNotFoundException;
import org.kohsuke.stapler.DataBoundConstructor;
import org.springframework.dao.DataAccessException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
* {@link SecurityRealm} that
*
* @author Kohsuke Kawaguchi
*/
public class SourceIpSecurityRealm extends SecurityRealm {
/**
* Represents the subnet that gets authenticated.
* String like "192.168.1.0/255.255.255.0"
*/
public final String pattern;
@DataBoundConstructor
public SourceIpSecurityRealm(String pattern) throws FormValidation {
DescriptorImpl.decomposePattern(pattern); // error check
this.pattern = pattern;
}
/**
* There's no way to log out, because authentication is automatic.
*/
public boolean canLogOut() {
return false;
}
/**
* The incoming request is "authenticated" automatically as an user that represents the remote IP address.
* There's no real authentication involved here (in the sense that no one sends a password), so we
* create {@link Authentication#isAuthenticated() an pre-authenticated Authentication object} right
* from the get-go.
*/
@Override
public Filter createFilter(FilterConfig filterConfig) {
return new Filter() {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Authentication auth = createAuth(request);
SecurityContextHolder.getContext().setAuthentication(auth);
chain.doFilter(request,response);
}
public void destroy() {
}
private Authentication createAuth(ServletRequest request) throws FormValidation, UnknownHostException {
String addr = request.getRemoteAddr();
Inet4Address[] adrs = DescriptorImpl.decomposePattern(pattern);
Inet4Address user = (Inet4Address) InetAddress.getByName(addr);
int mask = toInt(adrs[1]);
if((toInt(user)&mask)==(toInt(adrs[0])&mask))
return new UsernamePasswordAuthenticationToken(addr, addr, buildGroupList(addr));
else
return new AnonymousAuthenticationToken("anonymous","anonymous",new GrantedAuthority[]{new GrantedAuthorityImpl("anonymous")});
}
};
}
private int toInt(Inet4Address a) {
byte[] x = a.getAddress();
return (x[0]<<24)|((x[1]<<16)&0x00FF0000)|((x[2]<<8)&0x0000FF00)|((x[3])&0x000000FF);
}
/**
* Approximation of the IP address.
*/
private static final Pattern IP = Pattern.compile("[0-9]+(\\.[0-9]+)*");
/**
* Accepts a dot-separated numbers and count the number of tokens.
*/
private int decomposeIP(String s) {
try {
// quickly check if it looks roughly right.
// this simplifies the rest of the check
if(!IP.matcher(s).matches()) return 0;
String[] tokens = s.split("\\.");
for (String t : tokens) {
int i = Integer.parseInt(t);
if(i<0 || i>255)
return 0;
}
return tokens.length;
} catch (NumberFormatException e) {
// happens if the number is too big
return 0;
}
}
/**
* Builds up the group list from the IP address.
*/
private GrantedAuthority[] buildGroupList(String addr) {
// add subnet as groups
List<GrantedAuthority> groups = new ArrayList<GrantedAuthority>();
for(String a=addr;;) {
int idx=a.lastIndexOf('.');
if(idx<0) break;
a=a.substring(0,idx);
groups.add(new GrantedAuthorityImpl(a));
}
return groups.toArray(new GrantedAuthority[groups.size()]);
}
@Override
public GroupDetails loadGroupByGroupname(final String groupname) throws UsernameNotFoundException, DataAccessException {
int cnt = decomposeIP(groupname);
if(0<cnt && cnt<4)
return new GroupDetails() {
public String getName() {
return groupname;
}
};
throw new UsernameNotFoundException(groupname+" is not a valid partial IP address");
}
public SecurityComponents createSecurityComponents() {
return new SecurityComponents(
new AuthenticationManager() {
public Authentication authenticate(Authentication a) throws AuthenticationException {
// the filter above creates a fully authenticated Authentication object,
// so no need to do anything further
if(!a.isAuthenticated())
throw new AuthenticationCredentialsNotFoundException("Not authenticated");
return a;
}
},
new UserDetailsService() {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
if(decomposeIP(username)!=4)
throw new UsernameNotFoundException(username+" is not a valid IP address");
return new User(username,username,true,true,true,true,buildGroupList(username));
}
}
);
}
@Extension
public static class DescriptorImpl extends Descriptor<SecurityRealm> {
public String getDisplayName() {
return "By the IP address of the user";
}
public static Inet4Address[] decomposePattern(String p) throws FormValidation {
try {
if(fixEmptyAndTrim(p)==null) // if empty, authenticate everyone
return new Inet4Address[] {
(Inet4Address)InetAddress.getByName("0.0.0.0"),
(Inet4Address)InetAddress.getByName("0.0.0.0")};
String[] tokens = p.split("/");
if(tokens.length!=2 || !IP.matcher(tokens[0]).matches() || !IP.matcher(tokens[1]).matches())
throw FormValidation.error("Expecting a string like 192.168.1.0/255.255.255.0");
return new Inet4Address[] {
(Inet4Address)InetAddress.getByName(tokens[0]),
(Inet4Address)InetAddress.getByName(tokens[1])};
} catch (UnknownHostException e) {
throw FormValidation.error("Expecting a string like 192.168.1.0/255.255.255.0: "+e.getMessage());
}
}
}
}