Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jira Bearer Authentication #521

Merged
merged 29 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8021b08
Added JiraRestService construct that accept bearer token authentication
Mar 27, 2023
ee90ba2
Added class BearerHttpAuthenticationHandler
Mar 27, 2023
7495860
Added token in JiraConfig and added bearer auth tests class
Mar 27, 2023
a9d9ab6
Updated config.jelly.
Mar 27, 2023
049406c
Moved BearerHttpAuthenticationHandler to auth folder
Mar 27, 2023
85bbe68
Created JiraSessionFactory class
Mar 27, 2023
702e60d
Updated JiraSite logic with useBearerAuth
Mar 27, 2023
8cb07a8
Added JiraRestServiceBearerAuthTest class
Mar 27, 2023
322d22f
Fixed DescriptorImpltest
Mar 27, 2023
b94659e
Fixed typo
Mar 28, 2023
8714cf6
Updated doValidate JiraSite method
Mar 28, 2023
e831f45
Added BearerHttpAuthenticationHandlerTest class
Mar 28, 2023
e9f6ed2
Updated config.jelly
Mar 30, 2023
e7f9425
Testing config.jelly radio button on authentication method
Mar 30, 2023
34ab5d0
Fixed config.jelly error
Mar 30, 2023
ebb5203
Testing jelly config
Mar 30, 2023
81369ce
Fix jelly config
Mar 30, 2023
e62e3ff
Testing default value for authentication method
Mar 30, 2023
192dc24
Authentication default value
Mar 30, 2023
9cceca0
Testing useBearerAuth
Mar 30, 2023
66d7d20
Hide useHTTPAuth entry
Mar 30, 2023
a41e901
Updated use Bearer authentication title
Mar 30, 2023
7c6d4ba
Testing apache.httpcomponentns library
Apr 7, 2023
e4628e6
Revert "Testing apache.httpcomponentns library"
Apr 7, 2023
7329d31
Added note on http authentication bearer checkbox
May 11, 2023
c2d7a9d
Moved useBearerAuth note to description
May 12, 2023
ff1a63c
Fixing config.jelly
May 12, 2023
3cb87dd
Moved descriptio to config.properties
May 12, 2023
03b2440
Moved description to entry tag
May 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {

/**
* 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()) {
BearerHttpAuthenticationHandler bearerHttpAuthenticationHandler = new BearerHttpAuthenticationHandler(
credentials.getPassword().getPlainText());

jiraRestClient = new ExtendedAsynchronousJiraRestClientFactory()
.create(
uri,
bearerHttpAuthenticationHandler,
jiraSite.getHttpClientOptions());

jiraRestService = new JiraRestService(
uri,
jiraRestClient,
credentials.getPassword().getPlainText(),
jiraSite.getReadTimeout());
} else {
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,
updateJiraIssueForAllStatus, groupVisibility, roleVisibility, useHTTPAuth, timeout, readTimeout, threadExecutorNumber);
this.useBearerAuth = useBearerAuth;
}

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);
}
}
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>
<f:checkbox title="${%Use Bearer authentication instead of Basic authentication (Note: Bearer authentication is only supported in Jira Server, for Jira Cloud leave this unchecked)}" field="useBearerAuth" />
elia-bracci-hs marked this conversation as resolved.
Show resolved Hide resolved
</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
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
elia-bracci-hs marked this conversation as resolved.
Show resolved Hide resolved
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);
rantoniuk marked this conversation as resolved.
Show resolved Hide resolved


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 );
rantoniuk marked this conversation as resolved.
Show resolved Hide resolved
}

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