Skip to content

Commit

Permalink
Jira Bearer Authentication (#521)
Browse files Browse the repository at this point in the history
Add Jira Server Bearer auth support (#497)
  • Loading branch information
EliaBracciSumo committed May 25, 2023
1 parent fbfeb9e commit 851f553
Show file tree
Hide file tree
Showing 12 changed files with 364 additions and 18 deletions.
14 changes: 13 additions & 1 deletion src/main/java/hudson/plugins/jira/JiraRestService.java
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,19 @@ public JiraRestService(URI uri, ExtendedJiraRestClient jiraRestClient, String us
throw new RuntimeException("failed to encode username:password using Base64");
}
this.jiraRestClient = jiraRestClient;
baseApiPath = buildBaseApiPath(uri);
}

public JiraRestService(URI uri, ExtendedJiraRestClient jiraRestClient, String token, int timeout) {
this.uri = uri;
this.objectMapper = new ObjectMapper();
this.timeout = timeout;
this.authHeader = "Bearer " + token;
this.jiraRestClient = jiraRestClient;
baseApiPath = buildBaseApiPath(uri);
}

private String buildBaseApiPath(URI uri) {
final StringBuilder builder = new StringBuilder();
if (uri.getPath() != null) {
builder.append(uri.getPath());
Expand All @@ -127,7 +139,7 @@ public JiraRestService(URI uri, ExtendedJiraRestClient jiraRestClient, String us
builder.append('/');
}
builder.append(BASE_API_PATH);
baseApiPath = builder.toString();
return builder.toString();
}

public void addComment(String issueId, String commentBody,
Expand Down
70 changes: 70 additions & 0 deletions src/main/java/hudson/plugins/jira/JiraSessionFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package hudson.plugins.jira;

import java.net.URI;

import com.atlassian.jira.rest.client.auth.BasicHttpAuthenticationHandler;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;

import hudson.plugins.jira.JiraSite.ExtendedAsynchronousJiraRestClientFactory;
import hudson.plugins.jira.auth.BearerHttpAuthenticationHandler;
import hudson.plugins.jira.extension.ExtendedJiraRestClient;

/**
* Jira Session factory implementation
*
* @author Elia Bracci
*/
public class JiraSessionFactory {

Check warning on line 17 in src/main/java/hudson/plugins/jira/JiraSessionFactory.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 17 is not covered by tests

/**
* This method takes as parameters the JiraSite class, the jira URI and
* credentials and returns a JiraSession with Basic authentication if
* useBearerAuth is set to false, otherwise it returns a JiraSession with Bearer
* authentication if useBearerAuth is set to true.
*
* @param jiraSite jiraSite class
* @param uri jira uri
* @param credentials Jenkins credentials
* @return JiraSession instance
*/
public static JiraSession create(JiraSite jiraSite, URI uri,
StandardUsernamePasswordCredentials credentials) {
ExtendedJiraRestClient jiraRestClient;
JiraRestService jiraRestService;

if (jiraSite.isUseBearerAuth()) {

Check warning on line 35 in src/main/java/hudson/plugins/jira/JiraSessionFactory.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 35 is only partially covered, one branch is missing
BearerHttpAuthenticationHandler bearerHttpAuthenticationHandler = new BearerHttpAuthenticationHandler(

Check warning on line 36 in src/main/java/hudson/plugins/jira/JiraSessionFactory.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 36 is not covered by tests
credentials.getPassword().getPlainText());

Check warning on line 37 in src/main/java/hudson/plugins/jira/JiraSessionFactory.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 37 is not covered by tests

jiraRestClient = new ExtendedAsynchronousJiraRestClientFactory()

Check warning on line 39 in src/main/java/hudson/plugins/jira/JiraSessionFactory.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 39 is not covered by tests
.create(

Check warning on line 40 in src/main/java/hudson/plugins/jira/JiraSessionFactory.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 40 is not covered by tests
uri,
bearerHttpAuthenticationHandler,
jiraSite.getHttpClientOptions());

Check warning on line 43 in src/main/java/hudson/plugins/jira/JiraSessionFactory.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 43 is not covered by tests

jiraRestService = new JiraRestService(

Check warning on line 45 in src/main/java/hudson/plugins/jira/JiraSessionFactory.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 45 is not covered by tests
uri,
jiraRestClient,
credentials.getPassword().getPlainText(),

Check warning on line 48 in src/main/java/hudson/plugins/jira/JiraSessionFactory.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 48 is not covered by tests
jiraSite.getReadTimeout());

Check warning on line 49 in src/main/java/hudson/plugins/jira/JiraSessionFactory.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 49 is not covered by tests
} else {

Check warning on line 50 in src/main/java/hudson/plugins/jira/JiraSessionFactory.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 50 is not covered by tests
jiraRestClient = new ExtendedAsynchronousJiraRestClientFactory()
.create(
uri,
new BasicHttpAuthenticationHandler(
credentials.getUsername(),
credentials.getPassword().getPlainText()),
jiraSite.getHttpClientOptions());

jiraRestService = new JiraRestService(
uri,
jiraRestClient,
credentials.getUsername(),
credentials.getPassword().getPlainText(),
jiraSite.getReadTimeout());
}

return new JiraSession(jiraSite, jiraRestService);
}

}
37 changes: 28 additions & 9 deletions src/main/java/hudson/plugins/jira/JiraSite.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import hudson.plugins.jira.extension.ExtendedJiraRestClient;
import hudson.plugins.jira.extension.ExtendedVersion;
import hudson.plugins.jira.model.JiraIssue;
import hudson.plugins.jira.JiraSessionFactory;
import hudson.security.ACL;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
Expand Down Expand Up @@ -139,6 +140,11 @@ public class JiraSite extends AbstractDescribableImpl<JiraSite> {
*/
public String credentialsId;

/**
* Jira requires Bearer Authentication for login
*/
public boolean useBearerAuth;

/**
* User name needed to login. Optional.
* @deprecated use credentialsId
Expand Down Expand Up @@ -322,6 +328,15 @@ public JiraSite(URL url, URL alternativeUrl, StandardUsernamePasswordCredentials
updateJiraIssueForAllStatus, groupVisibility, roleVisibility, useHTTPAuth, timeout, readTimeout, threadExecutorNumber);
}

// Deprecate the previous constructor but leave it in place for Java-level compatibility.
@Deprecated
public JiraSite(URL url, URL alternativeUrl, StandardUsernamePasswordCredentials credentials, boolean supportsWikiStyleComment, boolean recordScmChanges, String userPattern,
boolean updateJiraIssueForAllStatus, String groupVisibility, String roleVisibility, boolean useHTTPAuth, int timeout, int readTimeout, int threadExecutorNumber, boolean useBearerAuth) {
this(url, alternativeUrl, credentials==null?null:credentials.getId(), supportsWikiStyleComment, recordScmChanges, userPattern,

Check warning on line 335 in src/main/java/hudson/plugins/jira/JiraSite.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 335 is only partially covered, 2 branches are missing
updateJiraIssueForAllStatus, groupVisibility, roleVisibility, useHTTPAuth, timeout, readTimeout, threadExecutorNumber);
this.useBearerAuth = useBearerAuth;

Check warning on line 337 in src/main/java/hudson/plugins/jira/JiraSite.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 337 is not covered by tests
}

Check warning on line 338 in src/main/java/hudson/plugins/jira/JiraSite.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 338 is not covered by tests

static URL toURL(String url) {
url = Util.fixEmptyAndTrim(url);
if (url == null) return null;
Expand Down Expand Up @@ -414,6 +429,10 @@ public boolean isUseHTTPAuth() {
return useHTTPAuth;
}

public boolean isUseBearerAuth() {
return useBearerAuth;
}

public String getGroupVisibility() {
return groupVisibility;
}
Expand Down Expand Up @@ -444,6 +463,11 @@ public void setUseHTTPAuth(boolean useHTTPAuth) {
this.useHTTPAuth = useHTTPAuth;
}

@DataBoundSetter
public void setUseBearerAuth(boolean useBearerAuth) {
this.useBearerAuth = useBearerAuth;
}

@DataBoundSetter
public void setGroupVisibility(String groupVisibility) {
this.groupVisibility = Util.fixEmptyAndTrim(groupVisibility);
Expand Down Expand Up @@ -553,14 +577,7 @@ JiraSession createSession(Item item) {
}
LOGGER.fine("creating Jira Session: " + uri);

ExtendedJiraRestClient jiraRestClient = new ExtendedAsynchronousJiraRestClientFactory()
.create(uri, new BasicHttpAuthenticationHandler(
credentials.getUsername(), credentials.getPassword().getPlainText()
),
getHttpClientOptions()
);
return new JiraSession(this, new JiraRestService(uri, jiraRestClient, credentials.getUsername(),
credentials.getPassword().getPlainText(), readTimeout));
return JiraSessionFactory.create(this, uri, credentials);
}

Lock getProjectUpdateLock() {
Expand Down Expand Up @@ -599,7 +616,7 @@ private StandardUsernamePasswordCredentials resolveCredentials(Item item) {

}

private HttpClientOptions getHttpClientOptions() {
protected HttpClientOptions getHttpClientOptions() {
final HttpClientOptions options = new HttpClientOptions();
options.setRequestTimeout(readTimeout, TimeUnit.SECONDS);
options.setSocketTimeout(timeout, TimeUnit.SECONDS);
Expand Down Expand Up @@ -1161,6 +1178,7 @@ public FormValidation doValidate(@QueryParameter String url,
@QueryParameter int timeout,
@QueryParameter int readTimeout,
@QueryParameter int threadExecutorNumber,
@QueryParameter boolean useBearerAuth,
@AncestorInPath Item item) {

if (item == null) {
Expand Down Expand Up @@ -1213,6 +1231,7 @@ public FormValidation doValidate(@QueryParameter String url,
site.setTimeout(timeout);
site.setReadTimeout(readTimeout);
site.setThreadExecutorNumber(threadExecutorNumber);
site.setUseBearerAuth(useBearerAuth);
JiraSession session = null;
try {
session = site.getSession(item);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package hudson.plugins.jira.auth;

import com.atlassian.jira.rest.client.api.AuthenticationHandler;
import com.atlassian.httpclient.api.Request.Builder;

/**
* Authentication handler for bearer authentication
*
* @author Elia Bracci
*/
public class BearerHttpAuthenticationHandler implements AuthenticationHandler {

private static final String AUTHORIZATION_HEADER = "Authorization";
private final String token;

/**
* Bearer http authentication handler constructor
* @param token pat or api token to use for bearer authentication
*/
public BearerHttpAuthenticationHandler(final String token) {
this.token = token;
}


@Override
public void configure(Builder builder) {
builder.setHeader(AUTHORIZATION_HEADER, "Bearer " + token);
}
}
7 changes: 5 additions & 2 deletions src/main/resources/hudson/plugins/jira/JiraSite/config.jelly
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
<f:entry title="Link URL" field="alternativeUrl" description="${%site.alternativeUrl}">
<f:textbox />
</f:entry>
<f:entry>
<f:invisibleEntry>
<f:checkbox title="${%Use HTTP authentication instead of normal login}" field="useHTTPAuth" />
</f:invisibleEntry>
<f:entry description="${%site.useBearerAuth}">
<f:checkbox title="${%Use Bearer authentication instead of Basic authentication}" field="useBearerAuth" />
</f:entry>
<f:entry>
<f:checkbox title="${%Supports Wiki notation}" field="supportsWikiStyleComment" />
Expand Down Expand Up @@ -50,7 +53,7 @@
</f:entry>
<f:entry>
<f:validateButton title="${%Validate Settings}"
method="validate" with="url,credentialsId,groupVisibility,roleVisibility,useHTTPAuth,alternativeUrl,timeout,readTimeout,threadExecutorNumber" />
method="validate" with="url,credentialsId,groupVisibility,roleVisibility,useHTTPAuth,alternativeUrl,timeout,readTimeout,threadExecutorNumber,useBearerAuth" />
</f:entry>
<f:entry title="">
<div align="right">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
site.alternativeUrl=Jira alternative URL
site.timeout=in seconds
site.useBearerAuth=Note: Bearer authentication is only supported in Jira Server, for Jira Cloud leave this unchecked
4 changes: 4 additions & 0 deletions src/test/java/JiraConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ public static String getUsername() {
public static String getPassword() {
return CONFIG.getString("password");
}

public static String getToken() {
return CONFIG.getString("token");
}
}
123 changes: 123 additions & 0 deletions src/test/java/JiraTesterBearerAuth.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@

import com.atlassian.jira.rest.client.api.domain.Component;
import com.atlassian.jira.rest.client.api.domain.Issue;
import com.atlassian.jira.rest.client.api.domain.IssueType;
import com.atlassian.jira.rest.client.api.domain.Status;
import com.atlassian.jira.rest.client.api.domain.Transition;
import com.atlassian.jira.rest.client.api.domain.User;
import hudson.plugins.jira.JiraRestService;
import hudson.plugins.jira.JiraSite;
import hudson.plugins.jira.auth.BearerHttpAuthenticationHandler;
import hudson.plugins.jira.extension.ExtendedJiraRestClient;
import hudson.plugins.jira.extension.ExtendedVersion;

import java.net.URI;
import java.net.URL;
import java.util.List;

import static hudson.plugins.jira.JiraSite.ExtendedAsynchronousJiraRestClientFactory;

/**
* Test bed to play with Jira.
*
* @author Elia Bracci
*/
public class JiraTesterBearerAuth {
public static void main(String[] args) throws Exception {

final URI uri = new URL(JiraConfig.getUrl()).toURI();
final BearerHttpAuthenticationHandler handler = new BearerHttpAuthenticationHandler(JiraConfig.getToken());
final ExtendedJiraRestClient jiraRestClient = new ExtendedAsynchronousJiraRestClientFactory()
.createWithAuthenticationHandler(uri, handler);

final JiraRestService restService = new JiraRestService(uri, jiraRestClient, JiraConfig.getToken(), JiraSite.DEFAULT_TIMEOUT);

final String projectKey = "TESTPROJECT";
final String issueId = "TESTPROJECT-425";
final Integer actionId = 21;

final Issue issue = restService.getIssue(issueId);
System.out.println("issue:" + issue);


final List<Transition> availableActions = restService.getAvailableActions(issueId);
for (Transition action : availableActions) {
System.out.println("Action:" + action);
}

for (IssueType issueType : restService.getIssueTypes()) {
System.out.println(" issue type: " + issueType);
}

// restService.addVersion("TESTPROJECT", "0.0.2");

final List<Component> components = restService.getComponents(projectKey);
for (Component component : components) {
System.out.println("component: " + component);
}

// BasicComponent backendComponent = null;
// final Iterable<BasicComponent> components1 = Lists.newArrayList(backendComponent);
// restService.createIssue("TESTPROJECT", "This is a test issue created using Jira jenkins plugin. Please ignore it.", "TESTUSER", components1, "test issue from Jira jenkins plugin");

final List<Issue> searchResults = restService.getIssuesFromJqlSearch("project = \"TESTPROJECT\"", 3);
for (Issue searchResult : searchResults) {
System.out.println("JQL search result: " + searchResult);
}

final List<String> projectsKeys = restService.getProjectsKeys();
for (String projectsKey : projectsKeys) {
System.out.println("project key: " + projectsKey);
}

final List<Status> statuses = restService.getStatuses();
for (Status status : statuses) {
System.out.println("status:" + status);
}

final User user = restService.getUser("TESTUSER");
System.out.println("user: " + user);

final List<ExtendedVersion> versions = restService.getVersions(projectKey);
for (ExtendedVersion version : versions) {
System.out.println("version: " + version);
}

// Version releaseVersion = new Version(version.getSelf(), version.getId(), version.getName(),
// version.getDescription(), version.isArchived(), true, new DateTime());
// System.out.println(" >>>> release version 0.0.2");
// restService.releaseVersion("TESTPROJECT", releaseVersion);

// System.out.println(" >>> update issue TESTPROJECT-425");
// restService.updateIssue(issueId, Collections.singletonList(releaseVersion));

// final Issue updatedIssue = restService.progressWorkflowAction(issueId, actionId);
// System.out.println("Updated issue:" + updatedIssue);



for(int i=0;i<10;i++){
callUniq( restService );
}

for(int i=0;i<10;i++){
callDuplicate( restService );
}

}

private static void callUniq(final JiraRestService restService) throws Exception {
long start = System.currentTimeMillis();
List<Issue> issues = restService.getIssuesFromJqlSearch( "key in ('JENKINS-53320','JENKINS-51057')", Integer.MAX_VALUE );
long end = System.currentTimeMillis();
System.out.println( "time uniq " + (end -start) );
}

private static void callDuplicate(final JiraRestService restService) throws Exception {
long start = System.currentTimeMillis();
List<Issue> issues = restService.getIssuesFromJqlSearch( "key in ('JENKINS-53320','JENKINS-53320','JENKINS-53320','JENKINS-53320','JENKINS-53320','JENKINS-51057','JENKINS-51057','JENKINS-51057','JENKINS-51057','JENKINS-51057')", Integer.MAX_VALUE );
long end = System.currentTimeMillis();
System.out.println( "time duplicate " + (end -start) );
}

}
Loading

0 comments on commit 851f553

Please sign in to comment.