提交 bfb51af7 编写于 作者: K kohsuke

Added 'login' and 'logout' commands so that you don't have to specify a...

Added 'login' and 'logout' commands so that you don't have to specify a credential for individual CLI invocation.

git-svn-id: https://hudson.dev.java.net/svn/hudson/trunk/hudson/main@28509 71c3de6d-444a-0410-be80-ed276b4c234a
上级 3f5917d6
......@@ -41,10 +41,12 @@ import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;
/**
* Base class for Hudson CLI.
......@@ -165,7 +167,10 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
try {
p.parseArgument(args.toArray(new String[args.size()]));
sc.setAuthentication(authenticator.authenticate()); // run the CLI with the right credential
Authentication auth = authenticator.authenticate();
if (auth==Hudson.ANONYMOUS)
auth = loadStoredAuthentication();
sc.setAuthentication(auth); // run the CLI with the right credential
return run();
} catch (CmdLineException e) {
stderr.println(e.getMessage());
......@@ -183,6 +188,19 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
}
}
/**
* Loads the persisted authentication information from {@link ClientAuthenticationCache}.
*/
protected Authentication loadStoredAuthentication() throws InterruptedException {
try {
return new ClientAuthenticationCache(channel).get();
} catch (IOException e) {
stderr.println("Failed to access the stored credential");
e.printStackTrace(stderr); // recover
return Hudson.ANONYMOUS;
}
}
/**
* Determines if the user authentication is attempted through CLI before running this command.
*
......@@ -232,6 +250,27 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
stderr.println(getShortDescription());
}
/**
* Convenience method for subtypes to obtain the system property of the client.
*/
protected String getClientSystemProperty(String name) throws IOException, InterruptedException {
return channel.call(new GetSystemProperty(name));
}
private static final class GetSystemProperty implements Callable<String, IOException> {
private final String name;
private GetSystemProperty(String name) {
this.name = name;
}
public String call() throws IOException {
return System.getProperty(name);
}
private static final long serialVersionUID = 1L;
}
/**
* Creates a clone to be used to execute a command.
*/
......@@ -263,4 +302,6 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
return cmd.createClone();
return null;
}
private static final Logger LOGGER = Logger.getLogger(CLICommand.class.getName());
}
package hudson.cli;
import hudson.FilePath;
import hudson.FilePath.FileCallable;
import hudson.model.Hudson;
import hudson.os.PosixAPI;
import hudson.remoting.Callable;
import hudson.remoting.Channel;
import hudson.remoting.VirtualChannel;
import hudson.util.Secret;
import org.acegisecurity.Authentication;
import org.acegisecurity.AuthenticationException;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.acegisecurity.userdetails.UserDetails;
import org.springframework.dao.DataAccessException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Properties;
/**
* Represents the authentication credential store of the CLI client.
*
* <p>
* This object encapsulates a remote manipulation of the credential store.
* We store encrypted user names.
*
* @author Kohsuke Kawaguchi
* @since 1.351
*/
public class ClientAuthenticationCache implements Serializable {
/**
* Where the store should be placed.
*/
private final FilePath store;
/**
* Loaded contents of the store.
*/
private final Properties props = new Properties();
public ClientAuthenticationCache(Channel channel) throws IOException, InterruptedException {
store = channel.call(new Callable<FilePath, IOException>() {
public FilePath call() throws IOException {
File home = new File(System.getProperty("user.home"));
return new FilePath(new File(home, ".hudson/cli-credentials"));
}
});
if (store.exists()) {
props.load(store.read());
}
}
/**
* Gets the persisted authentication for this Hudson.
*
* @return {@link Hudson#ANONYMOUS} if no such credential is found, or if the stored credential is invalid.
*/
public Authentication get() {
Hudson h = Hudson.getInstance();
Secret userName = Secret.decrypt(props.getProperty(getPropertyKey()));
if (userName==null) return Hudson.ANONYMOUS; // failed to decrypt
try {
UserDetails u = h.getSecurityRealm().loadUserByUsername(userName.toString());
return new UsernamePasswordAuthenticationToken(u.getUsername(), u.getPassword(), u.getAuthorities());
} catch (AuthenticationException e) {
return Hudson.ANONYMOUS;
} catch (DataAccessException e) {
return Hudson.ANONYMOUS;
}
}
/**
* Computes the key that identifies this Hudson among other Hudsons that the user has a credential for.
*/
private String getPropertyKey() {
String url = Hudson.getInstance().getRootUrl();
if (url!=null) return url;
return Secret.fromString("key").toString();
}
/**
* Persists the specified authentication.
*/
public void set(Authentication a) throws IOException, InterruptedException {
Hudson h = Hudson.getInstance();
// make sure that this security realm is capable of retrieving the authentication by name,
// as it's not required.
UserDetails u = h.getSecurityRealm().loadUserByUsername(a.getName());
props.setProperty(getPropertyKey(), Secret.fromString(u.getUsername()).getEncryptedValue());
save();
}
/**
* Removes the persisted credential, if there's one.
*/
public void remove() throws IOException, InterruptedException {
if (props.remove(getPropertyKey())!=null)
save();
}
private void save() throws IOException, InterruptedException {
store.act(new FileCallable<Void>() {
public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
f.getParentFile().mkdirs();
OutputStream os = new FileOutputStream(f);
try {
props.store(os,"Credential store");
} finally {
os.close();
}
// try to protect this file from other users, if we can.
PosixAPI.get().chmod(f.getAbsolutePath(),0600);
return null;
}
});
}
}
package hudson.cli;
import hudson.Extension;
import hudson.model.Hudson;
import org.acegisecurity.Authentication;
import org.kohsuke.args4j.CmdLineException;
/**
* Saves the current credential to allow future commands to run without explicit credential information.
*
* @author Kohsuke Kawaguchi
* @since 1.351
*/
@Extension
public class LoginCommand extends CLICommand {
@Override
public String getShortDescription() {
return "Saves the current credential to allow future commands to run without explicit credential information";
}
/**
* If we use the stored authentication for the login command, login becomes no-op, which is clearly not what
* the user has intended.
*/
@Override
protected Authentication loadStoredAuthentication() throws InterruptedException {
return Hudson.ANONYMOUS;
}
@Override
protected int run() throws Exception {
Authentication a = Hudson.getAuthentication();
if (a==Hudson.ANONYMOUS)
throw new CmdLineException("No credentials specified."); // this causes CLI to show the command line options.
ClientAuthenticationCache store = new ClientAuthenticationCache(channel);
store.set(a);
return 0;
}
}
package hudson.cli;
import hudson.Extension;
/**
* Deletes the credential stored with the login command.
*
* @author Kohsuke Kawaguchi
* @since 1.351
*/
@Extension
public class LogoutCommand extends CLICommand {
@Override
public String getShortDescription() {
return "Deletes the credential stored with the login command";
}
@Override
protected int run() throws Exception {
ClientAuthenticationCache store = new ClientAuthenticationCache(channel);
store.remove();
return 0;
}
}
......@@ -40,5 +40,5 @@ import java.lang.annotation.Target;
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface For {
Class value();
Class[] value();
}
......@@ -40,6 +40,9 @@ import hudson.model.Computer;
import hudson.model.Executor;
import hudson.model.Job;
import hudson.model.Queue.Executable;
import hudson.security.AbstractPasswordBasedSecurityRealm;
import hudson.security.GroupDetails;
import hudson.security.SecurityRealm;
import hudson.slaves.ComputerLauncher;
import hudson.tools.ToolProperty;
import hudson.remoting.Which;
......@@ -116,6 +119,11 @@ import junit.framework.TestCase;
import net.sourceforge.htmlunit.corejs.javascript.Context;
import net.sourceforge.htmlunit.corejs.javascript.ContextFactory;
import net.sourceforge.htmlunit.corejs.javascript.ContextFactory.Listener;
import org.acegisecurity.AuthenticationException;
import org.acegisecurity.BadCredentialsException;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.UsernameNotFoundException;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.io.FileUtils;
import org.apache.commons.beanutils.PropertyUtils;
......@@ -141,6 +149,7 @@ import org.mortbay.jetty.webapp.WebAppContext;
import org.mortbay.jetty.webapp.WebXmlConfiguration;
import org.mozilla.javascript.tools.debugger.Dim;
import org.mozilla.javascript.tools.shell.Global;
import org.springframework.dao.DataAccessException;
import org.w3c.css.sac.CSSException;
import org.w3c.css.sac.CSSParseException;
import org.w3c.css.sac.ErrorHandler;
......@@ -517,6 +526,30 @@ public abstract class HudsonTestCase extends TestCase implements RootAction {
return createSlave(l, null);
}
/**
* Creates a test {@link SecurityRealm} that recognizes username==password as valid.
*/
public SecurityRealm createDummySecurityRealm() {
return new AbstractPasswordBasedSecurityRealm() {
@Override
protected UserDetails authenticate(String username, String password) throws AuthenticationException {
if (username.equals(password))
return loadUserByUsername(username);
throw new BadCredentialsException(username);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
return new org.acegisecurity.userdetails.User(username,"",true,true,true,true,new GrantedAuthority[]{AUTHENTICATED_AUTHORITY});
}
@Override
public GroupDetails loadGroupByGroupname(String groupname) throws UsernameNotFoundException, DataAccessException {
throw new UsernameNotFoundException(groupname);
}
};
}
/**
* Returns the URL of the webapp top page.
* URL ends with '/'.
......
package hudson.security;
import hudson.cli.CLI;
import hudson.cli.CLICommand;
import hudson.cli.CliManagerImpl;
import hudson.cli.ClientAuthenticationCache;
import hudson.cli.LoginCommand;
import hudson.cli.LogoutCommand;
import hudson.model.Hudson;
import org.acegisecurity.Authentication;
import org.acegisecurity.AuthenticationException;
import org.acegisecurity.BadCredentialsException;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.userdetails.User;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.UsernameNotFoundException;
import org.jvnet.hudson.test.For;
import org.jvnet.hudson.test.HudsonTestCase;
import org.jvnet.hudson.test.TestExtension;
import org.springframework.dao.DataAccessException;
import junit.framework.Assert;
import java.io.IOException;
import java.util.Arrays;
import java.util.Locale;
......@@ -24,27 +23,17 @@ import java.util.Locale;
public class CliAuthenticationTest extends HudsonTestCase {
public void test1() throws Exception {
// dummy security realm that authenticates when username==password
hudson.setSecurityRealm(new AbstractPasswordBasedSecurityRealm() {
@Override
protected UserDetails authenticate(String username, String password) throws AuthenticationException {
if (username.equals(password))
return new User(username,password,true,true,true,true,new GrantedAuthority[]{AUTHENTICATED_AUTHORITY});
throw new BadCredentialsException(username);
}
hudson.setSecurityRealm(createDummySecurityRealm());
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
throw new UsernameNotFoundException(username);
}
successfulCommand("test","--username","abc","--password","abc");
}
@Override
public GroupDetails loadGroupByGroupname(String groupname) throws UsernameNotFoundException, DataAccessException {
throw new UsernameNotFoundException(groupname);
}
});
private void successfulCommand(String... args) throws Exception {
assertEquals(0, command(args));
}
assertEquals(0,new CliManagerImpl().main(Arrays.asList("test","--username","abc","--password","abc"),
Locale.ENGLISH, System.in, System.out, System.err));
private int command(String... args) throws Exception {
return new CLI(getURL()).execute(args);
}
@TestExtension
......@@ -56,11 +45,35 @@ public class CliAuthenticationTest extends HudsonTestCase {
@Override
protected int run() throws Exception {
Authentication auth = Hudson.getInstance().getAuthentication();
Authentication auth = Hudson.getAuthentication();
Assert.assertNotSame(Hudson.ANONYMOUS,auth);
Assert.assertEquals("abc", auth.getName());
return 0;
}
}
@TestExtension
public static class AnonymousCommand extends CLICommand {
@Override
public String getShortDescription() {
return "makes sure that the command is running as anonymous user";
}
@Override
protected int run() throws Exception {
Authentication auth = Hudson.getAuthentication();
Assert.assertSame(Hudson.ANONYMOUS,auth);
return 0;
}
}
@For({LoginCommand.class, LogoutCommand.class, ClientAuthenticationCache.class})
public void testLogin() throws Exception {
hudson.setSecurityRealm(createDummySecurityRealm());
successfulCommand("login","--username","abc","--password","abc");
successfulCommand("test"); // now we can run without an explicit credential
successfulCommand("logout");
successfulCommand("anonymous"); // now we should run as anonymous
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册