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()); }