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] connector - OAuth 2.0 (3LO) #18

Merged
merged 1 commit into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 8 additions & 5 deletions jira/.env-template
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# Authentication
[email protected]
JIRA_AUTH_TYPE=<oauth or basic>
# OAuth
JIRA_OAUTH_CLIENT_ID=
# Basic Auth
JIRA_USER_EMAIL=
JIRA_API_TOKEN=
JIRA_ORG_DOMAIN=https://space.example.com

JIRA_ORG_DOMAIN=
# Optional
JIRA_ISSUE_KEY=EX
JIRA_ISSUE_KEY=BBQ
JIRA_SEARCH_LIMIT=10
JIRA_CONNECTOR_API_KEY=
JIRA_CONNECTOR_API_KEY=
57 changes: 55 additions & 2 deletions jira/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,71 @@ The Jira connector will perform a full-text search based on the subject and desc

## Configuration

This connector requires the following environment variables to enable Jira's search API.
The Jira connector provides two authentication
methods: [Basic](https://developer.atlassian.com/cloud/jira/platform/basic-auth-for-rest-apis/)
and [OAuth 2.0 (3LO)](https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/)

To use <b>Basic</b> auth flow this connector requires the following environment variables:

```
JIRA_AUTH_TYPE=basic
JIRA_USER_EMAIL
JIRA_API_TOKEN
JIRA_ORG_DOMAIN
```

It uses Basic Auth with a user email and API token pair. To create an API token, click on your top-right profile icon, select Manage Account > Security > Create API Token. The org domain is the URL for your organization's Jira board, including the https:// scheme. You will need to put these environment variables in a `.env` file for development, see `.env-template`.
It uses Basic Auth with a user email and API token pair. To create an API token, click on your top-right profile icon, select Manage Account > Security > Create API Token.
The org domain is the URL for your organization's Jira board, including the https:// scheme.
You will need to put these environment variables in a `.env` file for development, see `.env-template`.

In order to use the `dev/load_data.py` script to load test tickets, the additional env var `JIRA_ISSUE_KEY` should be set. The key is typically a two-letter sequence that forms part of the issue number.

To use <b>OAuth 2.0 (3LO)</b> auth flow this connector requires the following environment variables:
```
JIRA_OAUTH_CLIENT_ID
```
You need to register your app in Jira and get the client ID. See [here](https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/) for more details.
You need to set the Jira API Permissions to `View Jira issue data(read:jira-work)` on the App Permissions page.
Also, you need to set the Authorization callback URL to `https://api.cohere.com/v1/connectors/oauth/token` on the App Authorization page.

Next, register the connector with Cohere's API using the following configuration.
Please note that here we need to add `offline_access` to the scope,
so that we can get a refresh token and use it to get a new access token when the current one expires.

```bash
curl -X POST \
'https://api.cohere.ai/v1/connectors' \
--header 'Accept: */*' \
--header 'Authorization: Bearer {COHERE-API-KEY}' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Jira with OAuth",
"url": "{YOUR_CONNECTOR-URL}",
"oauth": {
"client_id": "{Your Jira CLIENT-ID}",
"client_secret": "{Your Jira App SECRET}",
"authorize_url": "https://auth.atlassian.com/authorize",
"token_url": "https://auth.atlassian.com/oauth/token",
"scope": "read:jira-work offline_access"
}
}'
```

Once properly registered, whenever a search request is made Cohere will take care of authorizing the current user and passing the correct access tokens in the request headers.



## Optional Configuration

```
JIRA_SEARCH_LIMIT
JIRA_CONNECTOR_API_KEY
```

The `JIRA_SEARCH_LIMIT` variable may contain the maximum number of results to return for a search. If this variable is not set, the default is 10.
The `JIRA_CONNECTOR_API_KEY` variable may contain the API key for the Cohere connector. Don't set this variable if you are using OAuth 2.0 (3LO) auth flow.


## Development

Create a virtual environment and install dependencies with poetry. We recommend using in-project virtual environments:
Expand Down
6 changes: 3 additions & 3 deletions jira/dev/load_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@

# Set your Jira details here
jira = Jira(
url=os.environ.get("JIRA_PRODUCT_URL"),
username=os.environ.get("JIRA_CLIENT_USER"),
password=os.environ.get("JIRA_CLIENT_PASS"),
url=os.environ.get("JIRA_ORG_DOMAIN"),
username=os.environ.get("JIRA_USER_EMAIL"),
password=os.environ.get("JIRA_API_TOKEN"),
)
project_key = os.environ.get("JIRA_ISSUE_KEY")

Expand Down
12 changes: 1 addition & 11 deletions jira/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

115 changes: 104 additions & 11 deletions jira/provider/client.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,114 @@
from atlassian import Jira
from flask import current_app as app
import requests
from flask import current_app as app, request
from . import UpstreamProviderError

client = None
AUTHORIZATION_HEADER = "Authorization"
BEARER_PREFIX = "Bearer "


def get_client():
global client
class JiraClient:
JIRA_API_URL = "https://api.atlassian.com/ex/jira/"
JIRA_RESOURCE_URL = "https://api.atlassian.com/oauth/token/accessible-resources"
DEFAULT_SEARCH_LIMIT = 10

def __init__(self):
self.client = None
self.limit = self.DEFAULT_SEARCH_LIMIT

def set_limit(self, limit):
self.limit = limit

def search(self, query):
issues = self.client.jql(
'text ~ "' + query + '"',
limit=self.limit,
)["issues"]

return issues

def construct_api_request_url(self, token):
headers = {"Authorization": f"Bearer {token}"}

if not client:
response = requests.get(self.JIRA_RESOURCE_URL, headers=headers)
if response.status_code != 200:
raise UpstreamProviderError(
f"Error while fetching Jira API URL: {response.status_code} {response.reason}"
)
data = response.json()
if not data:
raise UpstreamProviderError(
f"Error while fetching Jira API URL: no data found"
)
resources = data[0]
cloud_id = resources["id"] if "id" in resources else None
return f"{self.JIRA_API_URL}{cloud_id}"

def setup_oauth_client(self, client_id, token):
if not token:
raise UpstreamProviderError(
f"Error while initializing Jira client: no access token found"
)
try:
client = Jira(
url=app.config["ORG_DOMAIN"],
username=app.config["USER_EMAIL"],
password=app.config["API_TOKEN"],
url = self.construct_api_request_url(token)
self.client = Jira(
url=url,
oauth2={
"client_id": client_id,
"token": {"access_token": token, "token_type": "Bearer"},
},
)
except Exception as e:
raise UpstreamProviderError(f"Error initializing Jira client: {str(e)}")
raise UpstreamProviderError(
f"Error while initializing Jira client: {str(e)}"
)

return self.client

def setup_basic_client(self, user_email, org_domain, api_token):
try:
self.client = Jira(
url=org_domain,
username=user_email,
password=api_token,
)
except Exception as e:
raise UpstreamProviderError(
f"Error while initializing Jira client: {str(e)}"
)

return self.client


def get_client():
assert (auth_type := app.config.get("AUTH_TYPE")), "JIRA_AUTH_TYPE must be set"
jira_client = JiraClient()
jira_client.set_limit(
app.config.get("SEARCH_LIMIT", JiraClient.DEFAULT_SEARCH_LIMIT)
)
if auth_type == "basic":
assert (
user_email := app.config.get("USER_EMAIL")
), "JIRA_USER_EMAIL must be set"
assert (
org_domain := app.config.get("ORG_DOMAIN")
), "JIRA_ORG_DOMAIN must be set"
assert (api_token := app.config.get("API_TOKEN")), "JIRA_API_TOKEN must be set"
jira_client.setup_basic_client(user_email, org_domain, api_token)
elif auth_type == "oauth":
assert (
client_id := app.config.get("OAUTH_CLIENT_ID")
), "JIRA_OAUTH_CLIENT_ID must be set"
token = get_access_token()
jira_client.setup_oauth_client(client_id, token)
else:
raise UpstreamProviderError(f"Invalid auth type: {auth_type}")

return jira_client


return client
def get_access_token():
authorization_header = request.headers.get(AUTHORIZATION_HEADER, "")
if authorization_header.startswith(BEARER_PREFIX):
return authorization_header.removeprefix(BEARER_PREFIX)
return None
7 changes: 1 addition & 6 deletions jira/provider/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@

logger = logging.getLogger(__name__)

DEFAULT_SEARCH_LIMIT = 10


def serialize_result(issue):
data = {}
Expand All @@ -33,10 +31,7 @@ def serialize_result(issue):
def search(query):
client = get_client()

issues = client.jql(
'text ~ "' + query + '"',
limit=app.config.get("SEARCH_LIMIT", DEFAULT_SEARCH_LIMIT),
)["issues"]
issues = client.search(query)

results = []
for issue in issues:
Expand Down
1 change: 1 addition & 0 deletions jira/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ flask = "2.2.5"
connexion = {extras = ["swagger-ui"], version = "^2.14.2"}
python-dotenv = "^1.0.0"
gunicorn = "^21.2.0"
requests = "^2.31.0"


[build-system]
Expand Down