diff --git a/cli/pom.xml b/cli/pom.xml index e65b174223..670e416a26 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -81,6 +81,10 @@ annotations provided + + commons-lang + commons-lang + diff --git a/cli/src/main/java/hudson/cli/CLI.java b/cli/src/main/java/hudson/cli/CLI.java index 5da726be0e..52c6785d58 100644 --- a/cli/src/main/java/hudson/cli/CLI.java +++ b/cli/src/main/java/hudson/cli/CLI.java @@ -77,6 +77,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import static java.util.logging.Level.*; import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; /** * CLI entry point to Jenkins. @@ -452,6 +453,10 @@ public class CLI implements AutoCloseable { String user = null; String auth = null; + + String userIdEnv = System.getenv("JENKINS_USER_ID"); + String tokenEnv = System.getenv("JENKINS_API_TOKEN"); + boolean strictHostKey = false; while(!args.isEmpty()) { @@ -563,6 +568,17 @@ public class CLI implements AutoCloseable { return -1; } + if (auth == null) { + // -auth option not set + if (StringUtils.isNotBlank(userIdEnv) && StringUtils.isNotBlank(tokenEnv)) { + auth = StringUtils.defaultString(userIdEnv).concat(":").concat(StringUtils.defaultString(tokenEnv)); + } else if (StringUtils.isNotBlank(userIdEnv) || StringUtils.isNotBlank(tokenEnv)) { + printUsage(Messages.CLI_BadAuth()); + return -1; + } // Otherwise, none credentials were set + + } + if (!url.endsWith("/")) { url += '/'; } diff --git a/cli/src/main/resources/hudson/cli/client/Messages.properties b/cli/src/main/resources/hudson/cli/client/Messages.properties index 921fe67a21..44fa79c6fd 100644 --- a/cli/src/main/resources/hudson/cli/client/Messages.properties +++ b/cli/src/main/resources/hudson/cli/client/Messages.properties @@ -13,10 +13,12 @@ CLI.Usage=Jenkins CLI\n\ -strictHostKey : request strict host key checking (for use with -ssh)\n\ -logger FINE : enable detailed logging from the client\n\ -auth [ USER:SECRET | @FILE ] : specify username and either password or API token (or load from them both from a file);\n\ - for use with -http, or -remoting but only when the JNLP agent port is disabled\n\ + for use with -http, or -remoting but only when the JNLP agent port is disabled.\n\ + Not necessary if JENKINS_USER_ID and JENKINS_API_TOKEN env vars are set\n\ \n\ The available commands depend on the server. Run the 'help' command to\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 Jenkins server. CLI.NoSuchFileExists=No such file exists: {0} +CLI.BadAuth=The JENKINS_USER_ID and JENKINS_API_TOKEN env vars should be both set or left empty. diff --git a/cli/src/main/resources/hudson/cli/client/Messages_es.properties b/cli/src/main/resources/hudson/cli/client/Messages_es.properties index f9979a5619..b8ff42b3c2 100644 --- a/cli/src/main/resources/hudson/cli/client/Messages_es.properties +++ b/cli/src/main/resources/hudson/cli/client/Messages_es.properties @@ -1,10 +1,24 @@ CLI.Usage=Jenkins CLI\n\ - Usar: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\ - Options:\n\ - \ -s URL : dirección web (por defecto se usa la variable JENKINS_URL)\n\ + Uso: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\ + Opciones:\n\ + -s URL : direcci\u00f3n web (por defecto se usa la variable JENKINS_URL)\n\ + -http : usa un protocolo plano sobre HTTP(S) (es el uso por defecto; excluyente con -ssh y -remoting)\n\ + -ssh : usa el protocolo SSH (requiere -user; el puerto SSH debe estar abierto en el servidor y el usuario debe tener registrada su clave publica)\n\ + -remoting : usa el protocolo deprecado de Remoting (siempre que est\u00e9 habilitado en el servidor; s\u00f3lo para compatibilidad con comandos heredados o legacy)\n\ + -i KEY : clave privada SSH usada para autenticaci\u00f3n (usado con -ssh o -remoting)\n\ + -p HOST:PORT : host y puerto para el uso de proxy HTTPS. Ver https://jenkins.io/redirect/cli-https-proxy-tunnel\n\ + -noCertificateCheck : elude por completo la verificaci\u00f3n del certificado HTTPS. Usar con precauci\u00f3n\n\ + -noKeyAuth : intenta no cargar la clave privada de autenticaci\u00f3n SSH. Presenta conflicto con -i\n\ + -user : especifica el usuario (se usa con -ssh)\n\ + -strictHostKey : solicita la comprobaci\u00f3n estricta de la clave del host (se usa con -ssh)\n\ + -logger FINE : habilita logs detallados en el cliente\n\ + -auth [ USER:SECRET | @FILE ] : especifica el usuario y contrase\u00f1a o API token (o los carga de un fichero);\n\ + se usa con -http o -remoting pero s\u00f3lo si el puerto del agente JNLP est\u00e1 deshabilitado.\n\ + No es necesario si las variables de entorno JENKINS_USER_ID y JENKINS_API_TOKEN se encuentran configuradas.\n\ \n\ - La lista completa de comandos disponibles depende del servidor. Ejecuta\n\ - el comando ''help'' para ver la lista. -CLI.NoURL=No se ha especificado el parámetro -s ni la variable JENKINS_URL -CLI.VersionMismatch=La versión no coincide. Esta consola "CLI" no se puede usar en este Jenkins + La lista de comandos disponibles depende del servidor. Ejecuta\n\ + el comando ''help'' para ver la lista completa. +CLI.NoURL=No se ha especificado el par\u00e1metro -s ni la variable JENKINS_URL +CLI.VersionMismatch=La versi\u00f3n no coincide. Esta consola "CLI" no se puede usar en este Jenkins CLI.NoSuchFileExists=El fichero {0} no existe. +CLI.BadAuth=Ambas variables de entorno JENKINS_USER_ID y JENKINS_API_TOKEN deber\u00edan estar configuradas o mantenerse vac\u00edas. diff --git a/core/pom.xml b/core/pom.xml index f57e8453e8..a940e93423 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -279,7 +279,6 @@ THE SOFTWARE. commons-lang commons-lang - 2.6 commons-digester diff --git a/pom.xml b/pom.xml index c809736be4..e5f3dc51df 100644 --- a/pom.xml +++ b/pom.xml @@ -148,6 +148,12 @@ THE SOFTWARE. 2.4 + + commons-lang + commons-lang + 2.6 + + org.mockito mockito-core diff --git a/test/src/test/java/hudson/cli/CLIEnvVarTest.java b/test/src/test/java/hudson/cli/CLIEnvVarTest.java new file mode 100644 index 0000000000..0a5b18a928 --- /dev/null +++ b/test/src/test/java/hudson/cli/CLIEnvVarTest.java @@ -0,0 +1,242 @@ +package hudson.cli; + +import hudson.EnvVars; +import hudson.Launcher; +import hudson.model.User; +import hudson.util.StreamTaskListener; +import jenkins.model.Jenkins; +import jenkins.security.ApiTokenProperty; +import jenkins.security.apitoken.ApiTokenTestHelper; +import org.apache.commons.io.FileUtils; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.MockAuthorizationStrategy; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThat; + +public class CLIEnvVarTest { + + @Rule + public JenkinsRule r = new JenkinsRule(); + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + private File home; + private File jar; + + @Before + public void grabCliJar() throws IOException { + home = tmp.newFolder(); + jar = tmp.newFile("jenkins-cli.jar"); + FileUtils.copyURLToFile(r.jenkins.getJnlpJars("jenkins-cli.jar").getURL(), jar); + } + + @Test + public void testSOptionWithoutJENKINS_URL() throws Exception { + assertEquals(0, launch("java", + "-Duser.home=" + home, + "-jar", jar.getAbsolutePath(), + "-s", r.getURL().toString(), + "who-am-i") + ); + } + + @Test + public void testWithoutSOptionAndWithoutJENKINS_URL() throws Exception { + assertNotEquals(0, launch("java", + "-Duser.home=" + home, + "-jar", jar.getAbsolutePath(), + "who-am-i") + ); + } + + @Test + public void testJENKINS_URLWithoutSOption() throws Exception { + // Valid URL + Map envars = new HashMap<>(); + envars.put("JENKINS_URL", r.getURL().toString()); + assertEquals(0, launch(envars, + "java", + "-Duser.home=" + home, + "-jar", jar.getAbsolutePath(), + "who-am-i") + ); + + // Invalid URL + envars = new HashMap<>(); + envars.put("JENKINS_URL", "http://invalid-url"); + assertNotEquals(0, launch(envars, + "java", + "-Duser.home=" + home, + "-jar", jar.getAbsolutePath(), + "who-am-i") + ); + + } + + @Test + public void testSOptionOverridesJENKINS_URL() throws Exception { + Map envars = new HashMap<>(); + envars.put("JENKINS_URL", "http://invalid-url"); + assertEquals(0, launch(envars, + "java", + "-Duser.home=" + home, + "-jar", jar.getAbsolutePath(), + "-s", r.getURL().toString(), + "who-am-i") + ); + } + + @Test + public void testAuthOptionWithoutEnvVars() throws Exception { + String token = getToken(); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + assertEquals(0, launch(Collections.emptyMap(), baos, null, + "java", + "-Duser.home=" + home, + "-jar", jar.getAbsolutePath(), + "-s", r.getURL().toString(), + "-auth", String.format("%s:%s", "admin", token), + "who-am-i") + ); + assertThat(baos.toString(), containsString("Authenticated as: admin")); + } + } + + @Test + public void testWithoutEnvVarsAndWithoutAuthOption() throws Exception { + r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); + r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("admin")); + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + assertEquals(0, launch(Collections.emptyMap(), baos, null, + "java", + "-Duser.home=" + home, + "-jar", jar.getAbsolutePath(), + "-s", r.getURL().toString(), + "who-am-i") + ); + assertThat(baos.toString(), containsString("Authenticated as: anonymous")); + } + } + + @Test + public void testEnvVarsWithoutAuthOption() throws Exception { + String token = getToken(); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + Map envars = new HashMap<>(); + envars.put("JENKINS_USER_ID", "admin"); + envars.put("JENKINS_API_TOKEN", token); + assertEquals(0, launch(envars, baos, null, + "java", + "-Duser.home=" + home, + "-jar", jar.getAbsolutePath(), + "-s", r.getURL().toString(), + "who-am-i") + ); + assertThat(baos.toString(), containsString("Authenticated as: admin")); + } + } + + @Test + public void testOnlyOneEnvVar() throws Exception { + String token = getToken(); + + // only JENKINS_USER_ID + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + Map envars = new HashMap<>(); + envars.put("JENKINS_USER_ID", "admin"); + assertNotEquals(0, launch(envars, + "java", + "-Duser.home=" + home, + "-jar", jar.getAbsolutePath(), + "-s", r.getURL().toString(), + "who-am-i") + ); + } + + // only JENKINS_API_TOKEN + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + Map envars = new HashMap<>(); + envars.put("JENKINS_API_TOKEN", token); + assertNotEquals(0, launch(envars, + "java", + "-Duser.home=" + home, + "-jar", jar.getAbsolutePath(), + "-s", r.getURL().toString(), + "who-am-i") + ); + } + } + + @Test + public void testAuthOptionOverridesEnvVars() throws Exception { + String token = getToken(); + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + Map envars = new HashMap<>(); + envars.put("JENKINS_USER_ID", "other-user"); + envars.put("JENKINS_API_TOKEN", "other-user"); + assertEquals(0, launch(envars, baos, null, + "java", + "-Duser.home=" + home, + "-jar", jar.getAbsolutePath(), + "-s", r.getURL().toString(), + "-auth", String.format("%s:%s", "admin", token), + "who-am-i") + ); + assertThat(baos.toString(), containsString("Authenticated as: admin")); + } + } + + private String getToken() { + ApiTokenTestHelper.enableLegacyBehavior(); + + r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); + r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("admin")); + + return User.get("admin").getProperty(ApiTokenProperty.class).getApiToken(); + } + + private int launch(String... cmdArgs) throws Exception { + return launch(Collections.emptyMap(), cmdArgs); + } + + private int launch(Map envars, String... cmdArgs) throws Exception { + return launch(envars, null, null, cmdArgs); + } + + private int launch(Map envars, OutputStream out, OutputStream err, String... cmdArgs) throws Exception { + if (out == null) { + out = System.out; + } + + if (err == null) { + err = System.err; + } + + return new Launcher.LocalLauncher(StreamTaskListener.fromStderr()) + .decorateByEnv(new EnvVars(envars)) + .launch() + .cmds(cmdArgs) + .stdout(out) + .stderr(err) + .join(); + } +}