diff --git a/cli/pom.xml b/cli/pom.xml index 9db65c1c038b914f4d4898c766382bdb95fc1026..d1943f3ed9183585f85703f66c9fd2e4927492ef 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -72,5 +72,10 @@ localizer 1.10 + + org.jvnet.hudson + trilead-ssh2 + build212-hudson-5 + diff --git a/cli/src/main/java/hudson/cli/CLI.java b/cli/src/main/java/hudson/cli/CLI.java index b4f3ee07e4a8e56192ad12a524e3f314a3465222..f2d9fb819fe342633db42f47314ac49f3b08f090 100644 --- a/cli/src/main/java/hudson/cli/CLI.java +++ b/cli/src/main/java/hudson/cli/CLI.java @@ -23,30 +23,46 @@ */ package hudson.cli; +import com.trilead.ssh2.crypto.Base64; +import com.trilead.ssh2.crypto.PEMDecoder; +import com.trilead.ssh2.signature.DSASHA1Verify; +import com.trilead.ssh2.signature.RSASHA1Verify; +import hudson.cli.client.Messages; import hudson.remoting.Channel; +import hudson.remoting.PingThread; +import hudson.remoting.Pipe; import hudson.remoting.RemoteInputStream; import hudson.remoting.RemoteOutputStream; -import hudson.remoting.PingThread; import hudson.remoting.SocketInputStream; import hudson.remoting.SocketOutputStream; -import hudson.cli.client.Messages; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; import java.net.URL; import java.net.URLConnection; -import java.net.Socket; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.PublicKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; -import java.util.ArrayList; -import java.util.logging.Logger; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.DataOutputStream; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; +import java.util.logging.Logger; + +import static java.util.logging.Level.*; /** * CLI entry point to Jenkins. @@ -139,7 +155,7 @@ public class CLI { } public int execute(List args) { - return execute(args,System.in,System.out,System.err); + return execute(args, System.in, System.out, System.err); } public int execute(String... args) { @@ -154,7 +170,13 @@ public class CLI { } public static void main(final String[] _args) throws Exception { + System.exit(_main(_args)); + } + + public static int _main(String[] _args) throws Exception { List args = Arrays.asList(_args); + List candidateKeys = new ArrayList(); + boolean sshAuthRequestedExplicitly = false; String url = System.getenv("JENKINS_URL"); @@ -168,32 +190,169 @@ public class CLI { args = args.subList(2,args.size()); continue; } + if(head.equals("-i") && args.size()>=2) { + File f = new File(args.get(1)); + if (!f.exists()) { + printUsage(Messages.CLI_NoSuchFileExists(f)); + return -1; + } + try { + candidateKeys.add(loadKey(f)); + } catch (IOException e) { + throw new Exception("Failed to load key: "+f,e); + } catch (GeneralSecurityException e) { + throw new Exception("Failed to load key: "+f,e); + } + args = args.subList(2,args.size()); + sshAuthRequestedExplicitly = true; + continue; + } break; } - + if(url==null) { - printUsageAndExit(Messages.CLI_NoURL()); - return; + printUsage(Messages.CLI_NoURL()); + return -1; } if(args.isEmpty()) args = Arrays.asList("help"); // default to help + if (candidateKeys.isEmpty()) + addDefaultPrivateKeyLocations(candidateKeys); + CLI cli = new CLI(new URL(url)); try { + if (!candidateKeys.isEmpty()) { + try { + // TODO: server verification + cli.authenticate(candidateKeys); + } catch (UnsupportedOperationException e) { + if (sshAuthRequestedExplicitly) { + System.err.println("The server doesn't support public key authentication"); + return -1; + } + } catch (GeneralSecurityException e) { + System.err.println("Failed to authenticate with your SSH keys. Proceeding with anonymous access"); + LOGGER.log(FINE,"Failed to authenticate with your SSH keys. Proceeding with anonymous access",e); + } + } + // execute the command // Arrays.asList is not serializable --- see 6835580 args = new ArrayList(args); - System.exit(cli.execute(args, System.in, System.out, System.err)); + return cli.execute(args, System.in, System.out, System.err); } finally { cli.close(); } } - private static void printUsageAndExit(String msg) { + private static KeyPair loadKey(File f) throws IOException, GeneralSecurityException { + DataInputStream dis = new DataInputStream(new FileInputStream(f)); + byte[] bytes = new byte[(int) f.length()]; + dis.readFully(bytes); + dis.close(); + return loadKey(new String(bytes)); + } + + public static KeyPair loadKey(String pemString) throws IOException, GeneralSecurityException { + Object key = PEMDecoder.decode(pemString.toCharArray(), null); + if (key instanceof com.trilead.ssh2.signature.RSAPrivateKey) { + com.trilead.ssh2.signature.RSAPrivateKey x = (com.trilead.ssh2.signature.RSAPrivateKey)key; + System.out.println("ssh-rsa " + new String(Base64.encode(RSASHA1Verify.encodeSSHRSAPublicKey(x.getPublicKey())))); + // this doesn't work + // System.out.println("ssh-rsa " + new String(Base64.encode(x.toJCEKeyPair().getPublic().getEncoded()))); + + return x.toJCEKeyPair(); + } + if (key instanceof com.trilead.ssh2.signature.DSAPrivateKey) { + com.trilead.ssh2.signature.DSAPrivateKey x = (com.trilead.ssh2.signature.DSAPrivateKey)key; + KeyFactory kf = KeyFactory.getInstance("DSA"); + System.out.println("ssh-dsa " + new String(Base64.encode(DSASHA1Verify.encodeSSHDSAPublicKey(x.getPublicKey())))); + + return new KeyPair( + kf.generatePublic(new DSAPublicKeySpec(x.getY(), x.getP(), x.getQ(), x.getG())), + kf.generatePrivate(new DSAPrivateKeySpec(x.getX(), x.getP(), x.getQ(), x.getG()))); + } + + throw new UnsupportedOperationException("Unrecognizable key format: "+key); + } + + /** + * try all the default key locations + */ + private static void addDefaultPrivateKeyLocations(List keyFileCandidates) { + File home = new File(System.getProperty("user.home")); + for (String path : new String[]{".ssh/id_rsa",".ssh/id_dsa",".ssh/identity"}) { + File key = new File(home,path); + if (key.exists()) { + try { + keyFileCandidates.add(loadKey(key)); + } catch (IOException e) { + // don't report an error. the user can still see it by using the -i option + LOGGER.log(FINE, "Failed to load "+key,e); + } catch (GeneralSecurityException e) { + LOGGER.log(FINE, "Failed to load " + key, e); + } + } + } + } + + /** + * Authenticate ourselves against the server. + * + * @return + * identity of the server represented as a public key. + */ + public PublicKey authenticate(Iterable privateKeys) throws IOException, GeneralSecurityException { + Pipe c2s = Pipe.createLocalToRemote(); + Pipe s2c = Pipe.createRemoteToLocal(); + entryPoint.authenticate("ssh",c2s, s2c); + Connection c = new Connection(s2c.getIn(), c2s.getOut()); + + try { + byte[] sharedSecret = c.diffieHellman(false).generateSecret(); + PublicKey serverIdentity = c.verifyIdentity(sharedSecret); + + // try all the public keys + for (KeyPair key : privateKeys) { + c.proveIdentity(sharedSecret,key); + if (c.readBoolean()) + return serverIdentity; // succeeded + } + if (privateKeys.iterator().hasNext()) + throw new GeneralSecurityException("Authentication failed. No private key accepted."); + else + throw new GeneralSecurityException("No private key is available for use in authentication"); + } finally { + c.close(); + } + } + + private static KeyPair readKeyFile(File f) throws IOException, GeneralSecurityException { + DataInputStream dis = new DataInputStream(new FileInputStream(f)); + byte[] b = new byte[(int) f.length()]; + dis.readFully(b); + dis.close(); + + Object key = PEMDecoder.decode(new String(b).toCharArray(), null); + if (key instanceof com.trilead.ssh2.signature.RSAPrivateKey) { + return ((com.trilead.ssh2.signature.RSAPrivateKey)key).toJCEKeyPair(); + } + if (key instanceof com.trilead.ssh2.signature.DSAPrivateKey) { + com.trilead.ssh2.signature.DSAPrivateKey x = (com.trilead.ssh2.signature.DSAPrivateKey)key; + KeyFactory kf = KeyFactory.getInstance("DSA"); + return new KeyPair( + kf.generatePublic(new DSAPublicKeySpec(x.getY(), x.getP(), x.getQ(), x.getG())), + kf.generatePrivate(new DSAPrivateKeySpec(x.getX(), x.getP(), x.getQ(), x.getG()))); + } + + throw new UnsupportedOperationException("Unrecognizable key format: "+key); + } + + private static void printUsage(String msg) { if(msg!=null) System.out.println(msg); System.err.println(Messages.CLI_Usage()); - System.exit(-1); } private static final Logger LOGGER = Logger.getLogger(CLI.class.getName()); diff --git a/cli/src/main/java/hudson/cli/CliEntryPoint.java b/cli/src/main/java/hudson/cli/CliEntryPoint.java index 1f5478d191495bf86e20a3bc2d32ce78fab02912..57bd42e553b77336c2a0db0085ae4ce0474c76b2 100644 --- a/cli/src/main/java/hudson/cli/CliEntryPoint.java +++ b/cli/src/main/java/hudson/cli/CliEntryPoint.java @@ -23,6 +23,8 @@ */ package hudson.cli; +import hudson.remoting.Pipe; + import java.io.OutputStream; import java.io.InputStream; import java.util.List; @@ -53,5 +55,19 @@ public interface CliEntryPoint { */ int protocolVersion(); + /** + * Initiates authentication out of band. + *

+ * This method starts two-way byte channel that allows the client and the server to perform authentication. + * The current supported implementation is based on SSH public key authentication that mutually authenticates + * clients and servers. + * + * @param protocol + * Currently only "ssh" is supported. + * @throws UnsupportedOperationException + * If the specified protocol is not supported by the server. + */ + void authenticate(String protocol, Pipe c2s, Pipe s2c); + int VERSION = 1; } diff --git a/cli/src/main/java/hudson/cli/Connection.java b/cli/src/main/java/hudson/cli/Connection.java new file mode 100644 index 0000000000000000000000000000000000000000..543e6acb3e1d504bbf353156ebee6e9bb9e0b196 --- /dev/null +++ b/cli/src/main/java/hudson/cli/Connection.java @@ -0,0 +1,211 @@ +/* + * The MIT License + * + * Copyright (c) 2011, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.cli; + +import hudson.remoting.SocketInputStream; +import hudson.remoting.SocketOutputStream; +import org.apache.commons.codec.binary.Base64; + +import javax.crypto.KeyAgreement; +import javax.crypto.interfaces.DHPublicKey; +import javax.crypto.spec.DHParameterSpec; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.security.AlgorithmParameterGenerator; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.X509EncodedKeySpec; + +public class Connection { + public final InputStream in; + public final OutputStream out; + + public final DataInputStream din; + public final DataOutputStream dout; + + public Connection(Socket socket) throws IOException { + this(new SocketInputStream(socket),new SocketOutputStream(socket)); + } + + public Connection(InputStream in, OutputStream out) { + this.in = in; + this.out = out; + this.din = new DataInputStream(in); + this.dout = new DataOutputStream(out); + } + +// +// +// Convenience methods +// +// + public void writeUTF(String msg) throws IOException { + dout.writeUTF(msg); + } + + public String readUTF() throws IOException { + return din.readUTF(); + } + + public void writeBoolean(boolean b) throws IOException { + dout.writeBoolean(b); + } + + public boolean readBoolean() throws IOException { + return din.readBoolean(); + } + + /** + * Sends a serializable object. + */ + public void writeObject(Object o) throws IOException { + ObjectOutputStream oos = new ObjectOutputStream(out); + oos.writeObject(o); + // don't close oss, which will close the underlying stream + // no need to flush either, given the way oos is implemented + } + + /** + * Receives an object sent by {@link #writeObject(Object)} + */ + public T readObject() throws IOException, ClassNotFoundException { + ObjectInputStream ois = new ObjectInputStream(in); + return (T)ois.readObject(); + } + + public void writeKey(Key key) throws IOException { + writeUTF(new String(Base64.encodeBase64(key.getEncoded()))); + } + + public X509EncodedKeySpec readKey() throws IOException { + byte[] otherHalf = Base64.decodeBase64(readUTF()); + return new X509EncodedKeySpec(otherHalf); + } + + /** + * Performs a Diffie-Hellman key exchange and produce a common secret between two ends of the connection. + * + *

+ * DH is also useful as a coin-toss algorithm. Two parties get the same random number without trusting + * each other. + */ + public KeyAgreement diffieHellman(boolean side) throws IOException, GeneralSecurityException { + KeyPair keyPair; + PublicKey otherHalf; + + if (side) { + AlgorithmParameterGenerator paramGen = AlgorithmParameterGenerator.getInstance("DH"); + paramGen.init(512); + + KeyPairGenerator dh = KeyPairGenerator.getInstance("DH"); + dh.initialize(paramGen.generateParameters().getParameterSpec(DHParameterSpec.class)); + keyPair = dh.generateKeyPair(); + + // send a half and get a half + writeKey(keyPair.getPublic()); + otherHalf = KeyFactory.getInstance("DH").generatePublic(readKey()); + } else { + otherHalf = KeyFactory.getInstance("DH").generatePublic(readKey()); + + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("DH"); + keyPairGen.initialize(((DHPublicKey) otherHalf).getParams()); + keyPair = keyPairGen.generateKeyPair(); + + // send a half and get a half + writeKey(keyPair.getPublic()); + } + + KeyAgreement ka = KeyAgreement.getInstance("DH"); + ka.init(keyPair.getPrivate()); + ka.doPhase(otherHalf, true); + + return ka; + } + + private String detectKeyAlgorithm(KeyPair kp) { + return detectKeyAlgorithm(kp.getPublic()); + } + + private String detectKeyAlgorithm(PublicKey kp) { + if (kp instanceof RSAPublicKey) return "RSA"; + if (kp instanceof DSAPublicKey) return "DSA"; + throw new IllegalArgumentException("Unknown public key type: "+kp); + } + + /** + * Used in conjunction with {@link #verifyIdentity(byte[])} to prove + * that we actually own the private key of the given key pair. + */ + public void proveIdentity(byte[] sharedSecret, KeyPair key) throws IOException, GeneralSecurityException { + String algorithm = detectKeyAlgorithm(key); + writeUTF(algorithm); + writeKey(key.getPublic()); + + Signature sig = Signature.getInstance("SHA1with"+algorithm); + sig.initSign(key.getPrivate()); + sig.update(key.getPublic().getEncoded()); + sig.update(sharedSecret); + writeObject(sig.sign()); + } + + /** + * Verifies that we are talking to a peer that actually owns the private key corresponding to the public key we get. + */ + public PublicKey verifyIdentity(byte[] sharedSecret) throws IOException, GeneralSecurityException { + try { + String serverKeyAlgorithm = readUTF(); + PublicKey spk = KeyFactory.getInstance(serverKeyAlgorithm).generatePublic(readKey()); + + // verify the identity of the server + Signature sig = Signature.getInstance("SHA1with"+serverKeyAlgorithm); + sig.initVerify(spk); + sig.update(spk.getEncoded()); + sig.update(sharedSecret); + sig.verify((byte[]) readObject()); + + return spk; + } catch (ClassNotFoundException e) { + throw new Error(e); // impossible + } + } + + public void close() throws IOException { + in.close(); + out.close(); + } +} diff --git a/cli/src/main/resources/hudson/cli/client/Messages.properties b/cli/src/main/resources/hudson/cli/client/Messages.properties index 7da44d342b111f77e426b715af445ab60b59fdce..327b04a6f436150d8688e9d335909b81c32cf360 100644 --- a/cli/src/main/resources/hudson/cli/client/Messages.properties +++ b/cli/src/main/resources/hudson/cli/client/Messages.properties @@ -7,3 +7,4 @@ CLI.Usage=Jenkins CLI\n\ see the list. CLI.NoURL=Neither -s nor the JENKINS_URL env var is specified. CLI.VersionMismatch=Version mismatch. This CLI cannot work with this Hudson server +CLI.NoSuchFileExists=No such file exists: {0} diff --git a/core/src/main/java/hudson/TcpSlaveAgentListener.java b/core/src/main/java/hudson/TcpSlaveAgentListener.java index 2f59a91e0dbcb2a8bd5a9212e80b9b83ea74b5ce..ab3b163754e28ca176d756d1a933cecbb58d1d2d 100644 --- a/core/src/main/java/hudson/TcpSlaveAgentListener.java +++ b/core/src/main/java/hudson/TcpSlaveAgentListener.java @@ -217,7 +217,7 @@ public final class TcpSlaveAgentListener extends Thread { Computer.threadPoolForRemoting, Mode.BINARY, new BufferedInputStream(new SocketInputStream(this.s)), new BufferedOutputStream(new SocketOutputStream(this.s)), null, true); - channel.setProperty(CliEntryPoint.class.getName(),new CliManagerImpl()); + channel.setProperty(CliEntryPoint.class.getName(),new CliManagerImpl(channel)); channel.join(); } diff --git a/core/src/main/java/hudson/cli/CliManagerImpl.java b/core/src/main/java/hudson/cli/CliManagerImpl.java index 3f8e0b3c145fcb2abe85bb7d6ea6687711a669eb..8bef506b45b9773e2854e2d3f1a42d1265ff54c1 100644 --- a/core/src/main/java/hudson/cli/CliManagerImpl.java +++ b/core/src/main/java/hudson/cli/CliManagerImpl.java @@ -23,7 +23,10 @@ */ package hudson.cli; +import hudson.model.User; import hudson.remoting.Channel; +import hudson.remoting.Pipe; +import hudson.util.IOUtils; import jenkins.model.Jenkins; import org.apache.commons.discovery.resource.ClassLoaders; import org.apache.commons.discovery.resource.classes.DiscoverClasses; @@ -34,6 +37,10 @@ import org.kohsuke.args4j.spi.OptionHandler; import org.kohsuke.args4j.CmdLineParser; import org.jvnet.tiger_types.Types; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PublicKey; import java.util.List; import java.util.Locale; import java.util.Collections; @@ -41,6 +48,10 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; import java.io.Serializable; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static java.util.logging.Level.*; /** * {@link CliEntryPoint} implementation exposed to the remote CLI. @@ -48,7 +59,10 @@ import java.io.Serializable; * @author Kohsuke Kawaguchi */ public class CliManagerImpl implements CliEntryPoint, Serializable { - public CliManagerImpl() { + private final Channel channel; + + public CliManagerImpl(Channel channel) { + this.channel = channel; } public int main(List args, Locale locale, InputStream stdin, OutputStream stdout, OutputStream stderr) { @@ -76,6 +90,21 @@ public class CliManagerImpl implements CliEntryPoint, Serializable { return -1; } + public void authenticate(final String protocol, final Pipe c2s, final Pipe s2c) { + for (final CliTransportAuthenticator cta : CliTransportAuthenticator.all()) { + if (cta.supportsProtocol(protocol)) { + new Thread() { + @Override + public void run() { + cta.authenticate(protocol,channel,new Connection(c2s.getIn(), s2c.getOut())); + } + }.start(); + return; + } + } + throw new UnsupportedOperationException("Unsupported authentication protocol: "+protocol); + } + public boolean hasCommand(String name) { return CLICommand.clone(name)!=null; } @@ -104,4 +133,6 @@ public class CliManagerImpl implements CliEntryPoint, Serializable { CmdLineParser.registerHandler(c,h); } } + + private static final Logger LOGGER = Logger.getLogger(CliManagerImpl.class.getName()); } diff --git a/core/src/main/java/hudson/cli/CliTransportAuthenticator.java b/core/src/main/java/hudson/cli/CliTransportAuthenticator.java new file mode 100644 index 0000000000000000000000000000000000000000..514fee52f26ae520e59b105150d41b2d404323bb --- /dev/null +++ b/core/src/main/java/hudson/cli/CliTransportAuthenticator.java @@ -0,0 +1,54 @@ +package hudson.cli; + +import hudson.ExtensionList; +import hudson.ExtensionPoint; +import hudson.remoting.Channel; +import hudson.security.SecurityRealm; +import jenkins.model.Jenkins; + +/** + * Perform {@link SecurityRealm} independent authentication. + * + *

+ * Implementing this extension point requires changes in the CLI module, as during authentication + * neither side trusts each other enough to start code-transfer. But it does allow us to + * use different implementations of the same protocol. + * + *

Current Implementations

+ *

+ * Starting 1.419, CLI supports SSH public key based client/server mutual authentication. + * The protocol name of this is "ssh". + * + * @author Kohsuke Kawaguchi + * @since 1.419 + */ +public abstract class CliTransportAuthenticator implements ExtensionPoint { + /** + * Checks if this implementation supports the specified protocol. + * + * @param protocol + * Identifier. CLI.jar is hard-coded with the built-in knowledge about a specific protocol. + * @return + * true if this implementation supports the specified protocol, + * in which case {@link #authenticate(String, Channel, Connection)} would be called next. + */ + public abstract boolean supportsProtocol(String protocol); + + /** + * Performs authentication. + * + *

+ * The authentication + * + * @param protocol + * Protocol identifier that {@link #supportsProtocol(String)} returned true. + * @param channel + * Communication channel to the client. + * @param con + */ + public abstract void authenticate(String protocol, Channel channel, Connection con); + + public static ExtensionList all() { + return Jenkins.getInstance().getExtensionList(CliTransportAuthenticator.class); + } +} diff --git a/core/src/main/java/hudson/model/User.java b/core/src/main/java/hudson/model/User.java index 4ce1fb42f6d248f2ba11737abb7b35f9159c6011..aec04690d372ada81a12ed6622c07dc84bd2d963 100644 --- a/core/src/main/java/hudson/model/User.java +++ b/core/src/main/java/hudson/model/User.java @@ -36,13 +36,18 @@ import hudson.model.listeners.SaveableListener; import hudson.security.ACL; import hudson.security.AccessControlled; import hudson.security.Permission; +import hudson.security.SecurityRealm; import hudson.util.RunList; import hudson.util.XStream2; import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.acegisecurity.Authentication; +import org.acegisecurity.AuthenticationException; +import org.acegisecurity.GrantedAuthority; +import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; +import org.acegisecurity.userdetails.UserDetails; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.export.Exported; @@ -236,6 +241,20 @@ public class User extends AbstractModelObject implements AccessControlled, Savea return null; } + /** + * Creates an {@link Authentication} object that represents this user. + */ + public Authentication impersonate() { + try { + UserDetails u = Jenkins.getInstance().getSecurityRealm().loadUserByUsername(id); + return new UsernamePasswordAuthenticationToken(u.getUsername(), u.getPassword(), u.getAuthorities()); + } catch (AuthenticationException e) { + // TODO: use the stored GrantedAuthorities + return new UsernamePasswordAuthenticationToken(id, "", + new GrantedAuthority[]{SecurityRealm.AUTHENTICATED_AUTHORITY}); + } + } + /** * Accepts the new description. */ diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index c341d70ab74d5d20cac2449e408255ed6af62d78..5ec7293340c0553060a0f79086f05c988df90128 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -2836,7 +2836,7 @@ public class Jenkins extends AbstractCIBase implements ModifiableItemGroup