[JENKINS-53792] Env vars to configure CLI -auth option (#3653)

* [JENKINS-53792] Add env vars for auth option

* [JENKINS-53792] Fix encoding after first commit

* [JENKINS-53792] Better readability

* [JENKINS-53792] Refactor and use the ApiToken

* [JENKINS-53792] Fix encoding
This commit is contained in:
Francisco Fernández authored and Oleg Nenashev committed 2018-10-06 16:05:38 +02:00
1 parent e0ed9a86e0
commit e998a7808f
7 files changed
+292 -9

No files matched your search

+4
View File
@@ -81,6 +81,10 @@
<artifactId>annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
</dependencies>
<build>
+16
View File
@@ -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 += '/';
}
@@ -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.
@@ -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.
-1
View File
@@ -279,7 +279,6 @@ THE SOFTWARE.
<dependency><!-- hudson doesn't use this directly, but some plugins wanted to use the latest -->
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>commons-digester</groupId>
+6
View File
@@ -148,6 +148,12 @@ THE SOFTWARE.
<version>2.4</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
@@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> envars, String... cmdArgs) throws Exception {
return launch(envars, null, null, cmdArgs);
}
private int launch(Map<String, String> 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();
}
}