diff --git a/README.md b/README.md index 48e674f6..50205979 100644 --- a/README.md +++ b/README.md @@ -53,16 +53,28 @@ Setting the `endpoint` can be done with any of the following (searched in order) Setting the `credentials` can be done with any of the following (searched in order): +- `jenkins.rest.api.token` +- `jenkinsRestApiToken` +- `JENKINS_REST_API_TOKEN` - `jenkins.rest.credentials` - `jenkinsRestCredentials` - `JENKINS_REST_CREDENTIALS` ## Credentials -jenkins-rest credentials can take 1 of 2 forms: +jenkins-rest credentials can take 1 of 3 forms: -- Colon delimited username and password: __admin:password__ -- Base64 encoded username and password: __YWRtaW46cGFzc3dvcmQ=__ +- Colon delimited username and api token: __admin:apiToken__ + - use `JenkinsClient.builder().apiToken("admin:apiToken")` +- Colon delimited username and password: __admin:password__ + - use `JenkinsClient.builder().credentials("admin:password")` +- Base64 encoded username followed by password __YWRtaW46cGFzc3dvcmQ=__ or api token __YWRtaW46YXBpVG9rZW4=__ + - use `JenkinsClient.builder().apiToken("YWRtaW46YXBpVG9rZW4=")` + - use `JenkinsClient.builder().credentials("YWRtaW46cGFzc3dvcmQ=")` + +The Jenkins crumb is automatically requested for the anonymous and the username:password authentication methods. +It is not requested when you use the apiToken as it is not needed. +For more details, see the [Cloudbees crumb documentation](https://support.cloudbees.com/hc/en-us/articles/219257077-CSRF-Protection-Explained). ## Examples @@ -80,12 +92,17 @@ Running mock tests can be done like so: ./gradlew clean build mockTest -Running integration tests can be done like so (requires existing jenkins instance): +Running integration tests require an existing jenkins instance which can be obtained with docker: + docker build -t jenkins-rest/jenkins src/main/docker + docker run -d --rm -p 8080:8080 --name jenkins-rest jenkins-rest/jenkins ./gradlew clean build integTest ### Integration tests settings +If you use the provided docker instance, there is no other preparation necessary. +If you wish to run integration tests against your own Jenkins server, the requirements are outlined in the next section. + #### Jenkins instance requirements - a running instance accessible on http://127.0.0.1:8080 (can be changed in the gradle.properties file) @@ -95,6 +112,7 @@ Running integration tests can be done like so (requires existing jenkins instanc - Plugins - [CloudBees Credentials](https://plugins.jenkins.io/cloudbees-credentials): otherwise an http 500 error occurs when accessing to http://127.0.0.1:8080/job/test-folder/job/test-folder-1/ `java.lang.NoClassDefFoundError: com/cloudbees/hudson/plugins/folder/properties/FolderCredentialsProvider` + - [CloudBees Folder](https://plugins.jenkins.io/cloudbees-folder) plugin installed - [OWASP Markup Formatter](https://plugins.jenkins.io/antisamy-markup-formatter) configured to use `Safe HTML` - [Configuration As Code](https://plugins.jenkins.io/configuration-as-code) plugin installed @@ -103,7 +121,7 @@ This project provides instructions to setup a [pre-configured Docker container]( #### Integration tests configuration - jenkins url and authentication method used by the tests are defined in the `gradle.properties` file -- by default, tests use the `credentials` authentication but this can be changed to use the `token` authentication +- by default, tests use the `apiToken` authentication but this can be changed to use the other authentication methods. #### Running integration tests from within your IDE diff --git a/build.gradle b/build.gradle index cbf0e14e..99ff372c 100644 --- a/build.gradle +++ b/build.gradle @@ -60,6 +60,8 @@ tasks.withType(JavaCompile) { } task mockTest(type: Test) { + group = "Verification" + description = "Mock tests" useTestNG() include '**/**MockTest*' maxParallelForks = 2 @@ -70,6 +72,8 @@ task mockTest(type: Test) { } task integTest(type: Test, dependsOn: mockTest) { + group = "Verification" + description = "Integration tests - Jenkins must be running. See the README." doFirst { def integProjectDir = project.file("${buildDir}/integ-projects") if (!integProjectDir.exists()) { @@ -86,13 +90,13 @@ task integTest(type: Test, dependsOn: mockTest) { events 'started', 'passed', 'failed' } def authentication = [:] - def possibleAuth = project.findProperty('testJenkinsRestCredentials') + def possibleAuth = project.findProperty('testJenkinsRestApiToken') if (possibleAuth) { - authentication['test.jenkins.rest.credentials'] = possibleAuth + authentication['test.jenkins.rest.api.token'] = possibleAuth } else { - possibleAuth = project.findProperty('testJenkinsRestToken') + possibleAuth = project.findProperty('testJenkinsRestCredentials') if (possibleAuth) { - authentication['test.jenkins.rest.token'] = possibleAuth + authentication['test.jenkins.rest.credentials'] = possibleAuth } else { logger.quiet 'No authentication parameters found. Assuming anonymous...' } diff --git a/src/main/java/com/cdancy/jenkins/rest/JenkinsAuthentication.java b/src/main/java/com/cdancy/jenkins/rest/JenkinsAuthentication.java index 75b97505..65d3eee8 100644 --- a/src/main/java/com/cdancy/jenkins/rest/JenkinsAuthentication.java +++ b/src/main/java/com/cdancy/jenkins/rest/JenkinsAuthentication.java @@ -36,10 +36,11 @@ public class JenkinsAuthentication extends Credentials { * Create instance of JenkinsAuthentication * * @param authValue value to use for authentication type HTTP header. - * @param authType authentication type (e.g. Basic, Bearer, Anonymous). + * @param authType authentication type (e.g. UsernamePassword, ApiToken, Anonymous). */ private JenkinsAuthentication(final String authValue, final AuthenticationType authType) { - super(null, authType == AuthenticationType.Basic && authValue.contains(":") + //super(authValue != null ? (authValue.contains(":") ? authValue.split(":")[0] : null) : null, (authType == AuthenticationType.UsernamePassword || authType == AuthenticationType.ApiToken) && authValue.contains(":") + super(null, (authType == AuthenticationType.UsernamePassword || authType == AuthenticationType.ApiToken) && authValue.contains(":") ? base64().encode(authValue.getBytes()) : authValue); this.authType = authType; @@ -64,26 +65,26 @@ public static class Builder { private AuthenticationType authType; /** - * Set 'Basic' credentials. + * Set 'UsernamePassword' credentials. * - * @param basicCredentials value to use for 'Basic' credentials. + * @param usernamePassword value to use for 'UsernamePassword' credentials. * @return this Builder. */ - public Builder credentials(final String basicCredentials) { - this.authValue = Objects.requireNonNull(basicCredentials); - this.authType = AuthenticationType.Basic; + public Builder credentials(final String usernamePassword) { + this.authValue = Objects.requireNonNull(usernamePassword); + this.authType = AuthenticationType.UsernamePassword; return this; } /** - * Set 'Bearer' credentials. - * - * @param tokenCredentials value to use for 'Bearer' credentials. + * Set 'ApiToken' credentials. + * + * @param apiTokenCredentials value to use for 'ApiToken' credentials. * @return this Builder. */ - public Builder token(final String tokenCredentials) { - this.authValue = Objects.requireNonNull(tokenCredentials); - this.authType = AuthenticationType.Bearer; + public Builder apiToken(final String apiTokenCredentials) { + this.authValue = Objects.requireNonNull(apiTokenCredentials); + this.authType = AuthenticationType.ApiToken; return this; } diff --git a/src/main/java/com/cdancy/jenkins/rest/JenkinsClient.java b/src/main/java/com/cdancy/jenkins/rest/JenkinsClient.java index cd344344..bae330e1 100644 --- a/src/main/java/com/cdancy/jenkins/rest/JenkinsClient.java +++ b/src/main/java/com/cdancy/jenkins/rest/JenkinsClient.java @@ -168,14 +168,15 @@ public Builder credentials(final String optionallyBase64EncodedCredentials) { } /** - * Optional token to use for authentication. + * Optional Api token to use for authentication. + * This is not a Bearer token, hence the name apiToken. * - * @param token authentication token. + * @param apiToken authentication token. * @return this Builder. */ - public Builder token(final String token) { + public Builder apiToken(final String apiToken) { authBuilder = JenkinsAuthentication.builder() - .token(token); + .apiToken(apiToken); return this; } diff --git a/src/main/java/com/cdancy/jenkins/rest/JenkinsConstants.java b/src/main/java/com/cdancy/jenkins/rest/JenkinsConstants.java index a5259744..1ce07aa4 100644 --- a/src/main/java/com/cdancy/jenkins/rest/JenkinsConstants.java +++ b/src/main/java/com/cdancy/jenkins/rest/JenkinsConstants.java @@ -28,8 +28,8 @@ public class JenkinsConstants { public static final String CREDENTIALS_SYSTEM_PROPERTY = "jenkins.rest.credentials"; public static final String CREDENTIALS_ENVIRONMENT_VARIABLE = CREDENTIALS_SYSTEM_PROPERTY.replaceAll("\\.", "_").toUpperCase(); - public static final String TOKEN_SYSTEM_PROPERTY = "jenkins.rest.token"; - public static final String TOKEN_ENVIRONMENT_VARIABLE = TOKEN_SYSTEM_PROPERTY.replaceAll("\\.", "_").toUpperCase(); + public static final String API_TOKEN_SYSTEM_PROPERTY = "jenkins.rest.api.token"; + public static final String API_TOKEN_ENVIRONMENT_VARIABLE = API_TOKEN_SYSTEM_PROPERTY.replaceAll("\\.", "_").toUpperCase(); public static final String DEFAULT_ENDPOINT = "http://127.0.0.1:7990"; diff --git a/src/main/java/com/cdancy/jenkins/rest/JenkinsUtils.java b/src/main/java/com/cdancy/jenkins/rest/JenkinsUtils.java index c3e9c8c8..bbbe9b36 100644 --- a/src/main/java/com/cdancy/jenkins/rest/JenkinsUtils.java +++ b/src/main/java/com/cdancy/jenkins/rest/JenkinsUtils.java @@ -26,8 +26,8 @@ import static com.cdancy.jenkins.rest.JenkinsConstants.ENDPOINT_SYSTEM_PROPERTY; import static com.cdancy.jenkins.rest.JenkinsConstants.JCLOUDS_PROPERTY_ID; import static com.cdancy.jenkins.rest.JenkinsConstants.JCLOUDS_VARIABLE_ID; -import static com.cdancy.jenkins.rest.JenkinsConstants.TOKEN_ENVIRONMENT_VARIABLE; -import static com.cdancy.jenkins.rest.JenkinsConstants.TOKEN_SYSTEM_PROPERTY; +import static com.cdancy.jenkins.rest.JenkinsConstants.API_TOKEN_ENVIRONMENT_VARIABLE; +import static com.cdancy.jenkins.rest.JenkinsConstants.API_TOKEN_SYSTEM_PROPERTY; import com.google.common.base.Throwables; @@ -159,28 +159,29 @@ public static String inferEndpoint() { } /** - * Find credentials (Basic, Bearer, or Anonymous) from system/environment. + * Find credentials (ApiToken, UsernamePassword, or Anonymous) from system/environment. * * @return BitbucketCredentials */ public static JenkinsAuthentication inferAuthentication() { - // 1.) Check for "Basic" auth credentials. final JenkinsAuthentication.Builder inferAuth = JenkinsAuthentication.builder(); + // 1.) Check for API Token as this requires no crumb hence is faster String authValue = JenkinsUtils + .retriveExternalValue(API_TOKEN_SYSTEM_PROPERTY, + API_TOKEN_ENVIRONMENT_VARIABLE); + if (authValue != null) { + inferAuth.apiToken(authValue); + return inferAuth.build(); + } + + // 2.) Check for UsernamePassword auth credentials. + authValue = JenkinsUtils .retriveExternalValue(CREDENTIALS_SYSTEM_PROPERTY, CREDENTIALS_ENVIRONMENT_VARIABLE); if (authValue != null) { inferAuth.credentials(authValue); - } else { - - // 2.) Check for "Bearer" auth token. - authValue = JenkinsUtils - .retriveExternalValue(TOKEN_SYSTEM_PROPERTY, - TOKEN_ENVIRONMENT_VARIABLE); - if (authValue != null) { - inferAuth.token(authValue); - } + return inferAuth.build(); } // 3.) If neither #1 or #2 find anything "Anonymous" access is assumed. diff --git a/src/main/java/com/cdancy/jenkins/rest/auth/AuthenticationType.java b/src/main/java/com/cdancy/jenkins/rest/auth/AuthenticationType.java index 8cc9d4bd..7cab43f3 100644 --- a/src/main/java/com/cdancy/jenkins/rest/auth/AuthenticationType.java +++ b/src/main/java/com/cdancy/jenkins/rest/auth/AuthenticationType.java @@ -22,18 +22,24 @@ */ public enum AuthenticationType { - Basic("Basic"), - Bearer("Bearer"), - Anonymous(""); + UsernamePassword("UsernamePassword", "Basic"), + ApiToken("ApiToken", "Basic"), + Anonymous("Anonymous", ""); - private final String type; + private final String authName; + private final String authScheme; - private AuthenticationType(final String type) { - this.type = type; + private AuthenticationType(final String authName, final String authScheme) { + this.authName = authName; + this.authScheme = authScheme; + } + + public String getAuthScheme() { + return authScheme; } @Override public String toString() { - return type; + return authName; } } diff --git a/src/main/java/com/cdancy/jenkins/rest/domain/user/ApiToken.java b/src/main/java/com/cdancy/jenkins/rest/domain/user/ApiToken.java new file mode 100644 index 00000000..b6bfe01d --- /dev/null +++ b/src/main/java/com/cdancy/jenkins/rest/domain/user/ApiToken.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cdancy.jenkins.rest.domain.user; + +import com.google.auto.value.AutoValue; +import org.jclouds.json.SerializedNames; + +@AutoValue +public abstract class ApiToken { + + public abstract String status(); + + public abstract ApiTokenData data(); + + ApiToken() { + } + + @SerializedNames({"status", "data"}) + public static ApiToken create(final String status, final ApiTokenData data) { + return new AutoValue_ApiToken(status, data); + } +} diff --git a/src/main/java/com/cdancy/jenkins/rest/domain/user/ApiTokenData.java b/src/main/java/com/cdancy/jenkins/rest/domain/user/ApiTokenData.java new file mode 100644 index 00000000..bb85d6f1 --- /dev/null +++ b/src/main/java/com/cdancy/jenkins/rest/domain/user/ApiTokenData.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cdancy.jenkins.rest.domain.user; + +import com.google.auto.value.AutoValue; +import org.jclouds.json.SerializedNames; + +@AutoValue +public abstract class ApiTokenData { + + public abstract String tokenName(); + public abstract String tokenUuid(); + public abstract String tokenValue(); + + ApiTokenData() { + } + + @SerializedNames({"tokenName", "tokenUuid", "tokenValue"}) + public static ApiTokenData create(final String tokenName, final String tokenUuid, final String tokenValue) { + return new AutoValue_ApiTokenData(tokenName, tokenUuid, tokenValue); + } +} diff --git a/src/main/java/com/cdancy/jenkins/rest/domain/user/Property.java b/src/main/java/com/cdancy/jenkins/rest/domain/user/Property.java new file mode 100644 index 00000000..1c3e99a2 --- /dev/null +++ b/src/main/java/com/cdancy/jenkins/rest/domain/user/Property.java @@ -0,0 +1,18 @@ +package com.cdancy.jenkins.rest.domain.user; + +import com.google.auto.value.AutoValue; +import org.jclouds.json.SerializedNames; + +@AutoValue +public abstract class Property { + + public abstract String clazz(); + + Property() { + } + + @SerializedNames({"_class"}) + public static Property create(final String clazz) { + return new AutoValue_Property(clazz); + } +} diff --git a/src/main/java/com/cdancy/jenkins/rest/filters/JenkinsAuthenticationFilter.java b/src/main/java/com/cdancy/jenkins/rest/filters/JenkinsAuthenticationFilter.java index e07c7bde..d3fc2940 100644 --- a/src/main/java/com/cdancy/jenkins/rest/filters/JenkinsAuthenticationFilter.java +++ b/src/main/java/com/cdancy/jenkins/rest/filters/JenkinsAuthenticationFilter.java @@ -51,14 +51,16 @@ public class JenkinsAuthenticationFilter implements HttpRequestFilter { @Override public HttpRequest filter(final HttpRequest request) throws HttpException { - if (creds.authType() == AuthenticationType.Anonymous) { - return request; - } else { - final String authHeader = creds.authType() + " " + creds.authValue(); - final HttpRequest.Builder<? extends HttpRequest.Builder<?>> builder = request.toBuilder(); + final HttpRequest.Builder<? extends HttpRequest.Builder<?>> builder = request.toBuilder(); + + // Password and API Token are both Basic authentication + if (creds.authType() == AuthenticationType.ApiToken || creds.authType() == AuthenticationType.UsernamePassword) { + final String authHeader = creds.authType().getAuthScheme() + " " + creds.authValue(); builder.addHeader(HttpHeaders.AUTHORIZATION, authHeader); + } - // whether to add crumb header or not + // Anon and Password need the crumb + if (creds.authType() == AuthenticationType.UsernamePassword || creds.authType() == AuthenticationType.Anonymous) { final Pair<Crumb, Boolean> localCrumb = getCrumb(); if (localCrumb.getKey().value() != null) { builder.addHeader(CRUMB_HEADER, localCrumb.getKey().value()); @@ -69,9 +71,8 @@ public HttpRequest filter(final HttpRequest request) throws HttpException { throw new RuntimeException("Unexpected exception being thrown: error=" + localCrumb.getKey().errors().get(0)); } } - - return builder.build(); } + return builder.build(); } private Pair<Crumb, Boolean> getCrumb() { diff --git a/src/main/java/com/cdancy/jenkins/rest/filters/JenkinsNoCrumbAuthenticationFilter.java b/src/main/java/com/cdancy/jenkins/rest/filters/JenkinsNoCrumbAuthenticationFilter.java index 918e5697..2daa407a 100644 --- a/src/main/java/com/cdancy/jenkins/rest/filters/JenkinsNoCrumbAuthenticationFilter.java +++ b/src/main/java/com/cdancy/jenkins/rest/filters/JenkinsNoCrumbAuthenticationFilter.java @@ -43,7 +43,7 @@ public HttpRequest filter(final HttpRequest request) throws HttpException { if (creds.authType() == AuthenticationType.Anonymous) { return request; } else { - final String authHeader = creds.authType() + " " + creds.authValue(); + final String authHeader = creds.authType().getAuthScheme() + " " + creds.authValue(); return request.toBuilder().addHeader(HttpHeaders.AUTHORIZATION, authHeader).build(); } } diff --git a/src/test/java/com/cdancy/jenkins/rest/BaseJenkinsMockTest.java b/src/test/java/com/cdancy/jenkins/rest/BaseJenkinsMockTest.java index f4d6195e..609efd6c 100644 --- a/src/test/java/com/cdancy/jenkins/rest/BaseJenkinsMockTest.java +++ b/src/test/java/com/cdancy/jenkins/rest/BaseJenkinsMockTest.java @@ -31,6 +31,7 @@ import org.jclouds.ContextBuilder; import com.cdancy.jenkins.rest.config.JenkinsAuthenticationModule; +import com.cdancy.jenkins.rest.auth.AuthenticationType; import com.google.common.base.Charsets; import com.google.common.base.Functions; @@ -42,15 +43,22 @@ import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.MockWebServer; import com.squareup.okhttp.mockwebserver.RecordedRequest; +import static com.google.common.io.BaseEncoding.base64; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static org.assertj.core.api.Assertions.assertThat; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +import org.jclouds.logging.slf4j.config.SLF4JLoggingModule; /** * Base class for all Jenkins mock tests. */ public class BaseJenkinsMockTest { + public static final String USERNAME_PASSWORD = "user:passwd"; + public static final String USERNAME_APITOKEN = "user:token"; + protected final String provider; private final JsonParser parser = new JsonParser(); @@ -65,18 +73,46 @@ public BaseJenkinsMockTest() { * @return instance of JenkinsApi. */ public JenkinsApi api(final URL url) { - final JenkinsAuthentication creds = JenkinsAuthentication.builder().build(); + return api(url, AuthenticationType.ApiToken); + } + + /** + * Create API from passed URL and passed authentication type. + * + * @param url endpoint of instance. + * @param authType authentication type. + */ + public JenkinsApi api(final URL url, final AuthenticationType authType) { + final JenkinsAuthentication creds = creds(authType); final JenkinsAuthenticationModule credsModule = new JenkinsAuthenticationModule(creds); return ContextBuilder.newBuilder(provider) .endpoint(url.toString()) .overrides(setupProperties()) - .modules(Lists.newArrayList(credsModule)) + .modules(Lists.newArrayList(credsModule, new SLF4JLoggingModule())) .buildApi(JenkinsApi.class); } + /** + * Create the Jenkins Authentication instance. + * + * @param authType authentication type. Falls back to anonymous when null. + * @return an authenticaition instance. + */ + public JenkinsAuthentication creds(final AuthenticationType authType) { + final JenkinsAuthentication.Builder authBuilder = JenkinsAuthentication.builder(); + if (authType == AuthenticationType.UsernamePassword) { + authBuilder.credentials(USERNAME_PASSWORD); + } else if (authType == AuthenticationType.ApiToken) { + authBuilder.apiToken(USERNAME_APITOKEN); + } + // Anonymous authentication is the default when not specified + return authBuilder.build(); + } + protected Properties setupProperties() { final Properties properties = new Properties(); properties.setProperty(Constants.PROPERTY_MAX_RETRIES, "0"); + properties.setProperty(Constants.PROPERTY_CONNECTION_TIMEOUT, "1"); return properties; } @@ -215,4 +251,16 @@ protected RecordedRequest assertSent(MockWebServer server, String method, String assertEquals(parser.parse(request.getUtf8Body()), parser.parse(json)); return request; } + + protected RecordedRequest assertSentAcceptAuth(MockWebServer server, String method, String path, String acceptType, AuthenticationType authType) throws InterruptedException { + RecordedRequest request = assertSentAccept(server, method, path, acceptType); + if (authType == AuthenticationType.UsernamePassword) { + assertEquals(request.getHeader("Authorization"), authType.getAuthScheme() + " " + base64().encode(USERNAME_PASSWORD.getBytes())); + } else if (authType == AuthenticationType.ApiToken) { + assertEquals(request.getHeader("Authorization"), authType.getAuthScheme() + " " + base64().encode(USERNAME_APITOKEN.getBytes())); + } else { + assertNull(request.getHeader("Authorization")); + } + return request; + } } diff --git a/src/test/java/com/cdancy/jenkins/rest/TestUtilities.java b/src/test/java/com/cdancy/jenkins/rest/TestUtilities.java index bbae21d7..1fa84aa7 100644 --- a/src/test/java/com/cdancy/jenkins/rest/TestUtilities.java +++ b/src/test/java/com/cdancy/jenkins/rest/TestUtilities.java @@ -43,8 +43,8 @@ public class TestUtilities extends JenkinsUtils { public static final String TEST_CREDENTIALS_SYSTEM_PROPERTY = "test.jenkins.rest.credentials"; public static final String TEST_CREDENTIALS_ENVIRONMENT_VARIABLE = TEST_CREDENTIALS_SYSTEM_PROPERTY.replaceAll("\\.", "_").toUpperCase(); - public static final String TEST_TOKEN_SYSTEM_PROPERTY = "test.jenkins.rest.token"; - public static final String TEST_TOKEN_ENVIRONMENT_VARIABLE = TEST_TOKEN_SYSTEM_PROPERTY.replaceAll("\\.", "_").toUpperCase(); + public static final String TEST_API_TOKEN_SYSTEM_PROPERTY = "test.jenkins.rest.api.token"; + public static final String TEST_API_TOKEN_ENVIRONMENT_VARIABLE = TEST_API_TOKEN_SYSTEM_PROPERTY.replaceAll("\\.", "_").toUpperCase(); private static final char[] CHARS = "abcdefghijklmnopqrstuvwxyz".toCharArray(); @@ -73,28 +73,30 @@ public static String randomString() { } /** - * Find credentials (Basic, Bearer, or Anonymous) from system/environment. + * Find credentials (ApiToken, UsernamePassword, or Anonymous) from system/environment. * * @return JenkinsCredentials */ public static JenkinsAuthentication inferTestAuthentication() { - // 1.) Check for "Basic" auth credentials. final JenkinsAuthentication.Builder inferAuth = JenkinsAuthentication.builder(); + + // 1.) Check for API Token as this requires no crumb hence is faster String authValue = JenkinsUtils + .retriveExternalValue(TEST_API_TOKEN_SYSTEM_PROPERTY, + TEST_API_TOKEN_ENVIRONMENT_VARIABLE); + if (authValue != null) { + inferAuth.apiToken(authValue); + return inferAuth.build(); + } + + // 2.) Check for UsernamePassword auth credentials. + authValue = JenkinsUtils .retriveExternalValue(TEST_CREDENTIALS_SYSTEM_PROPERTY, TEST_CREDENTIALS_ENVIRONMENT_VARIABLE); if (authValue != null) { inferAuth.credentials(authValue); - } else { - - // 2.) Check for "Bearer" auth token. - authValue = JenkinsUtils - .retriveExternalValue(TEST_TOKEN_SYSTEM_PROPERTY, - TEST_TOKEN_ENVIRONMENT_VARIABLE); - if (authValue != null) { - inferAuth.token(authValue); - } + return inferAuth.build(); } // 3.) If neither #1 or #2 find anything "Anonymous" access is assumed. diff --git a/src/test/java/com/cdancy/jenkins/rest/filters/JenkinsAuthenticationFilterMockTest.java b/src/test/java/com/cdancy/jenkins/rest/filters/JenkinsAuthenticationFilterMockTest.java new file mode 100644 index 00000000..9b522c96 --- /dev/null +++ b/src/test/java/com/cdancy/jenkins/rest/filters/JenkinsAuthenticationFilterMockTest.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cdancy.jenkins.rest.filters; + +import javax.ws.rs.core.MediaType; + +import com.cdancy.jenkins.rest.JenkinsApi; +import com.cdancy.jenkins.rest.JenkinsAuthentication; +import com.cdancy.jenkins.rest.auth.AuthenticationType; +import com.cdancy.jenkins.rest.BaseJenkinsMockTest; + +import org.jclouds.http.HttpRequest; + +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import com.google.common.collect.Multimap; + +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.MockWebServer; + +public class JenkinsAuthenticationFilterMockTest extends BaseJenkinsMockTest { + + @Test + public void testAnonymousNeedsCrumb() throws Exception { + MockWebServer server = mockWebServer(); + + final String value = "04a1109fc2db171362c966ebe9fc87f0"; + server.enqueue(new MockResponse().setBody("Jenkins-Crumb:" + value).setResponseCode(200)); + JenkinsApi jenkinsApi = api(server.getUrl("/"), AuthenticationType.Anonymous); + + JenkinsAuthentication creds = creds(AuthenticationType.Anonymous); + JenkinsAuthenticationFilter filter = new JenkinsAuthenticationFilter(creds, jenkinsApi); + HttpRequest httpRequest = HttpRequest.builder().endpoint(server.getUrl("/").toString()).method("GET").build(); + try { + httpRequest = filter.filter(httpRequest); + assertSentAcceptAuth(server, "GET", "/crumbIssuer/api/xml?xpath=concat%28//crumbRequestField,%22%3A%22,//crumb%29", MediaType.TEXT_PLAIN, AuthenticationType.Anonymous); + assertEquals(httpRequest.getEndpoint().toString(), server.getUrl("/").toString()); + Multimap<String,String> headers = httpRequest.getHeaders(); + assertEquals(headers.size(), 2); + assertTrue(headers.containsEntry("Jenkins-Crumb",value)); + assertTrue(headers.containsEntry("Cookie","")); + } finally { + jenkinsApi.close(); + server.shutdown(); + } + } + + @Test + public void testUsernamePasswordNeedsCrumb() throws Exception { + MockWebServer server = mockWebServer(); + + final String value = "04a1109fc2db171362c966ebe9fc87f0"; + server.enqueue(new MockResponse().setBody("Jenkins-Crumb:" + value).setResponseCode(200)); + JenkinsApi jenkinsApi = api(server.getUrl("/"), AuthenticationType.UsernamePassword); + + JenkinsAuthentication creds = creds(AuthenticationType.UsernamePassword); + JenkinsAuthenticationFilter filter = new JenkinsAuthenticationFilter(creds, jenkinsApi); + HttpRequest httpRequest = HttpRequest.builder().endpoint(server.getUrl("/").toString()).method("GET").build(); + try { + httpRequest = filter.filter(httpRequest); + assertSentAcceptAuth(server, "GET", "/crumbIssuer/api/xml?xpath=concat%28//crumbRequestField,%22%3A%22,//crumb%29", MediaType.TEXT_PLAIN, AuthenticationType.UsernamePassword); + assertEquals(httpRequest.getEndpoint().toString(), server.getUrl("/").toString()); + Multimap<String,String> headers = httpRequest.getHeaders(); + assertEquals(headers.size(), 3); + assertTrue(headers.containsEntry("Jenkins-Crumb",value)); + assertTrue(headers.containsEntry("Authorization", creds.authType().getAuthScheme() + " " + creds.authValue())); + assertTrue(headers.containsEntry("Cookie","")); + System.out.println(httpRequest.toString()); + } finally { + jenkinsApi.close(); + server.shutdown(); + } + } + + @Test + public void testFilterApiTokenAuthType() throws Exception { + MockWebServer server = mockWebServer(); + + JenkinsApi jenkinsApi = api(server.getUrl("/"), AuthenticationType.ApiToken); + + JenkinsAuthentication creds = creds(AuthenticationType.ApiToken); + JenkinsAuthenticationFilter filter = new JenkinsAuthenticationFilter(creds, jenkinsApi); + HttpRequest httpRequest = HttpRequest.builder().endpoint(server.getUrl("/").toString()).method("GET").build(); + try { + httpRequest = filter.filter(httpRequest); + assertEquals(httpRequest.getEndpoint().toString(), server.getUrl("/").toString()); + System.out.println(httpRequest.toString()); + Multimap<String,String> headers = httpRequest.getHeaders(); + assertEquals(headers.size(), 1); + assertTrue(headers.containsEntry("Authorization", creds.authType().getAuthScheme() + " " + creds.authValue())); + } finally { + jenkinsApi.close(); + server.shutdown(); + } + } +}