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

[Integration][Jira] Added support for oauth2 for live events #1429

Merged
merged 3 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
9 changes: 9 additions & 0 deletions integrations/jira/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

<!-- towncrier release notes start -->

## 0.3.2 (2025-02-24)


### Improvements

- Added support for OAuth live events for Jira using the webhooks api.
- https://developer.atlassian.com/cloud/jira/platform/webhooks/#registering-a-webhook-using-the-rest-api--for-connect-and-oauth-2-0-apps-


## 0.3.1 (2025-02-23)


Expand Down
57 changes: 48 additions & 9 deletions integrations/jira/jira/client.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import asyncio
import uuid
from typing import Any, AsyncGenerator, Generator

import httpx
from httpx import Auth, BasicAuth, Request, Response, Timeout
from loguru import logger
from port_ocean.clients.auth.oauth_client import (
OAuthClient,
)

from port_ocean.clients.auth.oauth_client import OAuthClient
from port_ocean.context.ocean import ocean
from port_ocean.utils import http_async_client

Expand All @@ -30,6 +30,12 @@
"user_deleted",
]

OAUTH2_WEBHOOK_EVENTS = [
"jira:issue_created",
"jira:issue_updated",
"jira:issue_deleted",
]


class BearerAuth(Auth):
def __init__(self, token: str):
Expand All @@ -51,20 +57,24 @@ def __init__(self, jira_url: str, jira_email: str, jira_token: str) -> None:
self.jira_token = jira_token

# If the Jira URL is directing to api.atlassian.com, we use OAuth2 Bearer Auth
if "api.atlassian.com" in self.jira_url:
if self.is_oauth_host():
self.jira_api_auth = self._get_bearer()
self.webhooks_url = f"{self.jira_rest_url}/api/3/webhook"
else:
self.jira_api_auth = BasicAuth(self.jira_email, self.jira_token)
self.webhooks_url = f"{self.jira_rest_url}/webhooks/1.0/webhook"

self.api_url = f"{self.jira_rest_url}/api/3"
self.teams_base_url = f"{self.jira_url}/gateway/api/public/teams/v1/org"
self.webhooks_url = f"{self.jira_rest_url}/webhooks/1.0/webhook"

self.client = http_async_client
self.client.auth = self.jira_api_auth
self.client.timeout = Timeout(30)
self._semaphore = asyncio.Semaphore(MAX_CONCURRENT_REQUESTS)

def is_oauth_host(self) -> bool:
return "api.atlassian.com" in self.jira_url

def _get_bearer(self) -> BearerAuth:
try:
return BearerAuth(self.external_access_token)
Expand Down Expand Up @@ -169,12 +179,41 @@ def _generate_base_req_params(
"startAt": startAt,
}

async def _get_webhooks(self) -> list[dict[str, Any]]:
return await self._send_api_request("GET", url=self.webhooks_url)
async def _create_events_webhook_oauth(self, app_host: str) -> None:
webhook_target_app_host = f"{app_host}/integration/webhook"
webhooks = (await self._send_api_request("GET", url=self.webhooks_url))[
"values"
]

if webhooks:
logger.info("Ocean real time reporting webhook already exists")
return

# We search a random project to get data from all projects
random_project = str(uuid.uuid4())

body = {
"url": webhook_target_app_host,
"webhooks": [
{
"jqlFilter": f"project not in ({random_project})",
"events": OAUTH2_WEBHOOK_EVENTS,
}
],
}

await self._send_api_request("POST", self.webhooks_url, json=body)
logger.info("Ocean real time reporting webhook created")

async def create_webhooks(self, app_host: str) -> None:
if self.is_oauth_host():
await self._create_events_webhook_oauth(app_host)
else:
await self._create_events_webhook(app_host)

async def create_events_webhook(self, app_host: str) -> None:
async def _create_events_webhook(self, app_host: str) -> None:
webhook_target_app_host = f"{app_host}/integration/webhook"
webhooks = await self._get_webhooks()
webhooks = await self._send_api_request("GET", url=self.webhooks_url)

for webhook in webhooks:
if webhook.get("url") == webhook_target_app_host:
Expand Down
2 changes: 1 addition & 1 deletion integrations/jira/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async def setup_application() -> None:
return

client = create_jira_client()
await client.create_events_webhook(base_url)
await client.create_webhooks(base_url)


@ocean.on_resync(Kinds.PROJECT)
Expand Down
2 changes: 1 addition & 1 deletion integrations/jira/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "jira"
version = "0.3.1"
version = "0.3.2"
description = "Integration to bring information from Jira into Port"
authors = ["Mor Paz <[email protected]>"]

Expand Down
4 changes: 2 additions & 2 deletions integrations/jira/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ async def test_create_events_webhook(mock_jira_client: JiraClient) -> None:
{"id": "new_webhook"}, # Creation response
]

await mock_jira_client.create_events_webhook(app_host)
await mock_jira_client.create_webhooks(app_host)

# Verify webhook creation call
create_call = mock_request.call_args_list[1]
Expand All @@ -288,5 +288,5 @@ async def test_create_events_webhook(mock_jira_client: JiraClient) -> None:
) as mock_request:
mock_request.return_value = [{"url": webhook_url}]

await mock_jira_client.create_events_webhook(app_host)
await mock_jira_client.create_webhooks(app_host)
mock_request.assert_called_once() # Only checks for existence
Loading