Skip to content

Commit

Permalink
Fix the anonymous crumb issue #15
Browse files Browse the repository at this point in the history
  • Loading branch information
martinda committed Dec 18, 2021
1 parent f1ad63e commit 2af8767
Show file tree
Hide file tree
Showing 15 changed files with 357 additions and 72 deletions.
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -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

Expand All @@ -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
Expand Down
12 changes: 8 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ tasks.withType(JavaCompile) {
}

task mockTest(type: Test) {
group = "Verification"
description = "Mock tests"
useTestNG()
include '**/**MockTest*'
maxParallelForks = 2
Expand All @@ -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()) {
Expand All @@ -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...'
}
Expand Down
27 changes: 14 additions & 13 deletions src/main/java/com/cdancy/jenkins/rest/JenkinsAuthentication.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

Expand Down
9 changes: 5 additions & 4 deletions src/main/java/com/cdancy/jenkins/rest/JenkinsClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/cdancy/jenkins/rest/JenkinsConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
27 changes: 14 additions & 13 deletions src/main/java/com/cdancy/jenkins/rest/JenkinsUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand Down
20 changes: 13 additions & 7 deletions src/main/java/com/cdancy/jenkins/rest/auth/AuthenticationType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
36 changes: 36 additions & 0 deletions src/main/java/com/cdancy/jenkins/rest/domain/user/ApiToken.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/cdancy/jenkins/rest/domain/user/Property.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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() {
Expand Down
Loading

0 comments on commit 2af8767

Please sign in to comment.