Skip to content

Commit a7f55d0

Browse files
authored
feat(jira): add basic auth method (#7233)
1 parent 97da78d commit a7f55d0

File tree

3 files changed

+224
-14
lines changed

3 files changed

+224
-14
lines changed

prowler/lib/outputs/jira/exceptions/exceptions.py

+22
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ class JiraBaseException(ProwlerException):
8282
"message": "The project key is invalid.",
8383
"remediation": "Please check the project key and try again.",
8484
},
85+
(9019, "JiraBasicAuthError"): {
86+
"message": "Failed to authenticate with Jira using basic authentication.",
87+
"remediation": "Please check the user mail and API token and try again.",
88+
},
89+
(9020, "JiraInvalidParameterError"): {
90+
"message": "Missing parameters on Jira Init function.",
91+
"remediation": "Please check the parameters and try again.",
92+
},
8593
}
8694

8795
def __init__(self, code, file=None, original_exception=None, message=None):
@@ -229,3 +237,17 @@ def __init__(self, file=None, original_exception=None, message=None):
229237
super().__init__(
230238
9018, file=file, original_exception=original_exception, message=message
231239
)
240+
241+
242+
class JiraBasicAuthError(JiraBaseException):
243+
def __init__(self, file=None, original_exception=None, message=None):
244+
super().__init__(
245+
9019, file=file, original_exception=original_exception, message=message
246+
)
247+
248+
249+
class JiraInvalidParameterError(JiraBaseException):
250+
def __init__(self, file=None, original_exception=None, message=None):
251+
super().__init__(
252+
9020, file=file, original_exception=original_exception, message=message
253+
)

prowler/lib/outputs/jira/jira.py

+115-14
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from prowler.lib.outputs.finding import Finding
1111
from prowler.lib.outputs.jira.exceptions.exceptions import (
1212
JiraAuthenticationError,
13+
JiraBasicAuthError,
1314
JiraCreateIssueError,
1415
JiraGetAccessTokenError,
1516
JiraGetAuthResponseError,
@@ -21,6 +22,7 @@
2122
JiraGetProjectsError,
2223
JiraGetProjectsResponseError,
2324
JiraInvalidIssueTypeError,
25+
JiraInvalidParameterError,
2426
JiraInvalidProjectKeyError,
2527
JiraNoProjectsError,
2628
JiraNoTokenError,
@@ -84,6 +86,8 @@ class Jira:
8486
- JiraCreateIssueError: Failed to create an issue in Jira
8587
- JiraSendFindingsResponseError: Failed to send the findings to Jira
8688
- JiraTestConnectionError: Failed to test the connection
89+
- JiraBasicAuthError: Failed to authenticate using basic auth
90+
- JiraInvalidParameterError: The provided parameters in Init are invalid
8791
8892
Usage:
8993
jira = Jira(
@@ -98,6 +102,10 @@ class Jira:
98102
_client_id: str = None
99103
_client_secret: str = None
100104
_access_token: str = None
105+
_user_mail: str = None
106+
_api_token: str = None
107+
_domain: str = None
108+
_using_basic_auth: bool = False
101109
_refresh_token: str = None
102110
_expiration_date: int = None
103111
_cloud_id: str = None
@@ -120,14 +128,31 @@ def __init__(
120128
redirect_uri: str = None,
121129
client_id: str = None,
122130
client_secret: str = None,
131+
user_mail: str = None,
132+
api_token: str = None,
133+
domain: str = None,
123134
):
124135
self._redirect_uri = redirect_uri
125136
self._client_id = client_id
126137
self._client_secret = client_secret
138+
self._user_mail = user_mail
139+
self._api_token = api_token
140+
self._domain = domain
127141
self._scopes = ["read:jira-user", "read:jira-work", "write:jira-work"]
128-
auth_url = self.auth_code_url()
129-
authorization_code = self.input_authorization_code(auth_url)
130-
self.get_auth(authorization_code)
142+
# If the client mail, API token and site name are present, use basic auth
143+
if user_mail and api_token and domain:
144+
self._using_basic_auth = True
145+
self.get_basic_auth()
146+
# If the redirect URI, client ID and client secret are present, use auth code flow
147+
elif redirect_uri and client_id and client_secret:
148+
auth_url = self.auth_code_url()
149+
authorization_code = self.input_authorization_code(auth_url)
150+
self.get_auth(authorization_code)
151+
else:
152+
init_error = "Failed to initialize Jira object, missing parameters."
153+
raise JiraInvalidParameterError(
154+
message=init_error, file=os.path.basename(__file__)
155+
)
131156

132157
@property
133158
def redirect_uri(self):
@@ -157,6 +182,10 @@ def cloud_id(self, value):
157182
def scopes(self):
158183
return self._scopes
159184

185+
@property
186+
def using_basic_auth(self):
187+
return self._using_basic_auth
188+
160189
def get_params(self, state_encoded):
161190
return {
162191
**self.PARAMS_TEMPLATE,
@@ -209,6 +238,29 @@ def get_timestamp_from_seconds(seconds: int) -> datetime:
209238
"""
210239
return (datetime.now() + timedelta(seconds=seconds)).isoformat()
211240

241+
def get_basic_auth(self) -> None:
242+
"""Get the access token using the mail and API token.
243+
244+
Returns:
245+
- None
246+
247+
Raises:
248+
- JiraBasicAuthError: Failed to authenticate using basic auth
249+
"""
250+
try:
251+
user_string = f"{self._user_mail}:{self._api_token}"
252+
self._access_token = base64.b64encode(user_string.encode("utf-8")).decode(
253+
"utf-8"
254+
)
255+
self._cloud_id = self.get_cloud_id(self._access_token, domain=self._domain)
256+
except Exception as e:
257+
message_error = f"Failed to get auth using basic auth: {e}"
258+
logger.error(message_error)
259+
raise JiraBasicAuthError(
260+
message=message_error,
261+
file=os.path.basename(__file__),
262+
)
263+
212264
def get_auth(self, auth_code: str = None) -> None:
213265
"""Get the access token and refresh token
214266
@@ -269,11 +321,12 @@ def get_auth(self, auth_code: str = None) -> None:
269321
file=os.path.basename(__file__),
270322
)
271323

272-
def get_cloud_id(self, access_token: str = None) -> str:
324+
def get_cloud_id(self, access_token: str = None, domain: str = None) -> str:
273325
"""Get the cloud ID from Jira
274326
275327
Args:
276328
- access_token: The access token from Jira
329+
- domain: The site name from Jira
277330
278331
Returns:
279332
- str: The cloud ID
@@ -284,8 +337,17 @@ def get_cloud_id(self, access_token: str = None) -> str:
284337
- JiraGetCloudIDError: Failed to get the cloud ID from Jira
285338
"""
286339
try:
287-
headers = {"Authorization": f"Bearer {access_token}"}
288-
response = requests.get(self.API_TOKEN_URL, headers=headers)
340+
if self._using_basic_auth:
341+
headers = {"Authorization": f"Basic {access_token}"}
342+
response = requests.get(
343+
f"https://{domain}.atlassian.net/_edge/tenant_info",
344+
headers=headers,
345+
)
346+
response = response.json()
347+
return response.get("cloudId")
348+
else:
349+
headers = {"Authorization": f"Bearer {access_token}"}
350+
response = requests.get(self.API_TOKEN_URL, headers=headers)
289351

290352
if response.status_code == 200:
291353
resources = response.json()
@@ -326,6 +388,10 @@ def get_access_token(self) -> str:
326388
- JiraGetAccessTokenError: Failed to get the access token
327389
"""
328390
try:
391+
# If using basic auth, return the access token
392+
if self._using_basic_auth:
393+
return self._access_token
394+
329395
if self.auth_expiration and datetime.now() < datetime.fromisoformat(
330396
self.auth_expiration
331397
):
@@ -392,6 +458,9 @@ def test_connection(
392458
redirect_uri: str = None,
393459
client_id: str = None,
394460
client_secret: str = None,
461+
user_mail: str = None,
462+
api_token: str = None,
463+
domain: str = None,
395464
raise_on_exception: bool = True,
396465
) -> Connection:
397466
"""Test the connection to Jira
@@ -400,6 +469,9 @@ def test_connection(
400469
- redirect_uri: The redirect URI
401470
- client_id: The client ID
402471
- client_secret: The client secret
472+
- user_mail: The client mail
473+
- api_token: The API token
474+
- domain: The site name
403475
- raise_on_exception: Whether to raise an exception or not
404476
405477
Returns:
@@ -417,13 +489,20 @@ def test_connection(
417489
redirect_uri=redirect_uri,
418490
client_id=client_id,
419491
client_secret=client_secret,
492+
user_mail=user_mail,
493+
api_token=api_token,
494+
domain=domain,
420495
)
421496
access_token = jira.get_access_token()
422497

423498
if not access_token:
424499
return ValueError("Failed to get access token")
425500

426-
headers = {"Authorization": f"Bearer {access_token}"}
501+
if jira.using_basic_auth:
502+
headers = {"Authorization": f"Basic {access_token}"}
503+
else:
504+
headers = {"Authorization": f"Bearer {access_token}"}
505+
427506
response = requests.get(
428507
f"https://api.atlassian.com/ex/jira/{jira.cloud_id}/rest/api/3/myself",
429508
headers=headers,
@@ -461,6 +540,13 @@ def test_connection(
461540
if raise_on_exception:
462541
raise auth_error
463542
return Connection(error=auth_error)
543+
except JiraBasicAuthError as basic_auth_error:
544+
logger.error(
545+
f"{basic_auth_error.__class__.__name__}[{basic_auth_error.__traceback__.tb_lineno}]: {basic_auth_error}"
546+
)
547+
if raise_on_exception:
548+
raise basic_auth_error
549+
return Connection(error=basic_auth_error)
464550
except Exception as error:
465551
logger.error(f"Failed to test connection: {error}")
466552
if raise_on_exception:
@@ -489,7 +575,11 @@ def get_projects(self) -> Dict[str, str]:
489575
if not access_token:
490576
return ValueError("Failed to get access token")
491577

492-
headers = {"Authorization": f"Bearer {access_token}"}
578+
if self._using_basic_auth:
579+
headers = {"Authorization": f"Basic {access_token}"}
580+
else:
581+
headers = {"Authorization": f"Bearer {access_token}"}
582+
493583
response = requests.get(
494584
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/project",
495585
headers=headers,
@@ -500,7 +590,7 @@ def get_projects(self) -> Dict[str, str]:
500590
projects = {
501591
project["key"]: project["name"] for project in response.json()
502592
}
503-
if len(projects) == 0:
593+
if projects == {}: # If no projects are found
504594
logger.error("No projects found")
505595
raise JiraNoProjectsError(
506596
message="No projects found in Jira",
@@ -555,7 +645,11 @@ def get_available_issue_types(self, project_key: str = None) -> list[str]:
555645
file=os.path.basename(__file__),
556646
)
557647

558-
headers = {"Authorization": f"Bearer {access_token}"}
648+
if self._using_basic_auth:
649+
headers = {"Authorization": f"Basic {access_token}"}
650+
else:
651+
headers = {"Authorization": f"Bearer {access_token}"}
652+
559653
response = requests.get(
560654
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/issue/createmeta?projectKeys={project_key}&expand=projects.issuetypes.fields",
561655
headers=headers,
@@ -1109,10 +1203,17 @@ def send_findings(
11091203
raise JiraInvalidIssueTypeError(
11101204
message="The issue type is invalid", file=os.path.basename(__file__)
11111205
)
1112-
headers = {
1113-
"Authorization": f"Bearer {access_token}",
1114-
"Content-Type": "application/json",
1115-
}
1206+
1207+
if self._using_basic_auth:
1208+
headers = {
1209+
"Authorization": f"Basic {access_token}",
1210+
"Content-Type": "application/json",
1211+
}
1212+
else:
1213+
headers = {
1214+
"Authorization": f"Bearer {access_token}",
1215+
"Content-Type": "application/json",
1216+
}
11161217

11171218
for finding in findings:
11181219
status_color = self.get_color_from_status(finding.status.value)

0 commit comments

Comments
 (0)