package jenkins.security;
import hudson.Extension;
import hudson.model.Descriptor.FormException;
import hudson.model.User;
import hudson.model.UserProperty;
import hudson.model.UserPropertyDescriptor;
import hudson.security.SecurityRealm;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.acegisecurity.Authentication;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.GrantedAuthorityImpl;
import org.acegisecurity.userdetails.UserDetails;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.StaplerRequest;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Remembers the set of {@link GrantedAuthority}s that was obtained the last time the user has logged in.
*
* This allows us to implement {@link User#impersonate()} with proper set of groups.
*
* @author Kohsuke Kawaguchi
* @since 1.556
* @see ImpersonatingUserDetailsService
*/
public class LastGrantedAuthoritiesProperty extends UserProperty {
private volatile String[] roles;
private long timestamp;
/**
* Stick to the same object since there's no UI for this.
*/
@Override
public UserProperty reconfigure(StaplerRequest req, JSONObject form) throws FormException {
req.bindJSON(this, form);
return this;
}
public GrantedAuthority[] getAuthorities() {
String[] roles = this.roles; // capture to a variable for immutability
GrantedAuthority[] r = new GrantedAuthority[roles==null ? 1 : roles.length+1];
r[0] = SecurityRealm.AUTHENTICATED_AUTHORITY;
if (roles != null) {
for (int i = 1; i < r.length; i++) {
r[i] = new GrantedAuthorityImpl(roles[i - 1]);
}
}
return r;
}
/**
* Persist the information with the new {@link UserDetails}.
*/
public void update(@Nonnull Authentication auth) throws IOException {
List<String> roles = new ArrayList<String>();
for (GrantedAuthority ga : auth.getAuthorities()) {
roles.add(ga.getAuthority());
}
String[] a = roles.toArray(new String[roles.size()]);
if (!Arrays.equals(this.roles,a)) {
this.roles = a;
this.timestamp = System.currentTimeMillis();
user.save();
}
}
/**
* Removes the recorded information
*/
public void invalidate() throws IOException {
if (roles!=null) {
roles = null;
timestamp = System.currentTimeMillis();
user.save();
}
}
/**
* Listen to the login success/failure event to persist {@link GrantedAuthority}s properly.
*/
@Extension
public static class SecurityListenerImpl extends SecurityListener {
@Override
protected void authenticated(@Nonnull UserDetails details) {
}
@Override
protected void failedToAuthenticate(@Nonnull String username) {
}
@Override
protected void loggedIn(@Nonnull String username) {
try {
// user should have been created but may not have been saved for some realms
// but as this is a callback of a successful login we can safely create the user.
User u = User.getById(username, true);
LastGrantedAuthoritiesProperty o = u.getProperty(LastGrantedAuthoritiesProperty.class);
if (o==null)
u.addProperty(o=new LastGrantedAuthoritiesProperty());
Authentication a = Jenkins.getAuthentication();
if (a!=null && a.getName().equals(username))
o.update(a); // just for defensive sanity checking
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to record granted authorities",e);
}
}
@Override
protected void failedToLogIn(@Nonnull String username) {
// while this initially seemed like a good idea to avoid allowing wrong impersonation for too long,
// doing this means a malicious user can break the impersonation capability
// just by failing to login. See ApiTokenFilter that does the following, which seems better:
/*
try {
Jenkins.getInstance().getSecurityRealm().loadUserByUsername(username);
} catch (UserMayOrMayNotExistException x) {
// OK, give them the benefit of the doubt.
} catch (UsernameNotFoundException x) {
// Not/no longer a user; deny the API token. (But do not leak the information that this happened.)
chain.doFilter(request, response);
return;
} catch (DataAccessException x) {
throw new ServletException(x);
}
*/
// try {
// User u = User.getById(username,false);
// LastGrantedAuthoritiesProperty o = u.getProperty(LastGrantedAuthoritiesProperty.class);
// if (o!=null)
// o.invalidate();
// } catch (IOException e) {
// LOGGER.log(Level.WARNING, "Failed to record granted authorities",e);
// }
}
@Override
protected void loggedOut(@Nonnull String username) {
}
}
@Extension @Symbol("lastGrantedAuthorities")
public static final class DescriptorImpl extends UserPropertyDescriptor {
@Override
public boolean isEnabled() {
return false;
}
public UserProperty newInstance(User user) {
return null;
}
}
private static final Logger LOGGER = Logger.getLogger(LastGrantedAuthoritiesProperty.class.getName());
}