Skip to content

Commit

Permalink
Add token authentication to confluence plugin
Browse files Browse the repository at this point in the history
It's now possible to use token authentication for connecting to
Confluence. Page list is now also available in markdown format.

Signed-off-by: Sandro Bonazzola <[email protected]>
  • Loading branch information
sandrobonazzola committed Jun 28, 2024
1 parent 017887f commit f60f59d
Showing 1 changed file with 106 additions and 7 deletions.
113 changes: 106 additions & 7 deletions did/plugins/confluence.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,35 @@
only valid for basic authentication, ``auth_password`` or
``auth_password_file`` must be provided, ``auth_password`` has a
higher priority.
Configuration example (token authentication)::
[redhat-confluence]
type = confluence
url = https://spaces.redhat.com/
auth_url = https://spaces.redhat.com/login.action
auth_type = token
token_file = ~/.did/confluence-token
token_name = did
token_expiration = 30
Notes:
Either ``token`` or ``token_file`` has to be defined.
token
Token string directly included in the config.
Has a higher priority over ``token_file``.
token_file
Path to the file where the token is stored.
token_expiration
Print warning if token with provided ``token_name`` expires within
specified number of ``days``.
token_name
Name of the token to check for expiration in ``token_expiration``
days. This has to match the name as seen in your Confluence profile.
"""

import distutils.util
Expand All @@ -41,7 +70,7 @@
from requests.packages.urllib3.exceptions import InsecureRequestWarning
from requests_gssapi import DISABLED, HTTPSPNEGOAuth

from did.base import Config, ReportError
from did.base import Config, ReportError, get_token
from did.stats import Stats, StatsGroup
from did.utils import listed, log, pretty

Expand All @@ -52,7 +81,7 @@
MAX_BATCHES = 100

# Supported authentication types
AUTH_TYPES = ["gss", "basic"]
AUTH_TYPES = ["gss", "basic", "token"]

# Enable ssl verify
SSL_VERIFY = True
Expand Down Expand Up @@ -95,29 +124,36 @@ def search(query, stats, expand=None):
class ConfluencePage(Confluence):
""" Confluence page results """

def __init__(self, page):
def __init__(self, page, url, myformat):
""" Initialize the page """
self.title = page['title']
self.url = f"{url}{page['_links']['webui']}"
self.format = myformat

def __str__(self):
""" Page title for displaying """
return "{}".format(self.title)
if self.format == "markdown":
return f"[{self.title}]({self.url})"
return f"{self.title}"


class ConfluenceComment(Confluence):
""" Confluence comment results """

def __init__(self, comment):
def __init__(self, comment, url, myformat):
""" Initialize issue """
# Remove the 'Re:' prefix
self.title = re.sub('^Re: ', '', comment['title'])
self.body = comment['body']['editor']['value']
# Remove html tags
self.body = re.sub('</p><p>', ' ', self.body)
self.body = re.sub('<[^<]+?>', '', self.body)
self.url = url
self.format = myformat

def __str__(self):
""" Confluence title & comment snippet for displaying """
# TODO: implement markdown output here
return "{}: {}".format(self.title, self.body)


Expand All @@ -135,8 +171,14 @@ def fetch(self):
"type=page AND creator = '{0}' "
"AND created >= {1} AND created < {2}".format(
self.user.login, self.options.since, self.options.until))
result = Confluence.search(query, self)
self.stats = [
ConfluencePage(page) for page in Confluence.search(query, self)]
ConfluencePage(
page,
self.parent.url,
self.options.format
) for page in result
]


class CommentAdded(Stats):
Expand All @@ -147,7 +189,11 @@ def fetch(self):
"AND created >= {1} AND created < {2}".format(
self.user.login, self.options.since, self.options.until))
self.stats = [
ConfluenceComment(comment) for comment in Confluence.search(
ConfluenceComment(
comment,
self.parent.url,
self.options.format
) for comment in Confluence.search(
query, self, expand="body.editor")]


Expand Down Expand Up @@ -211,6 +257,27 @@ def __init__(self, option, name=None, parent=None, user=None):
raise ReportError(
"`auth_password` and `auth_password_file` are only valid "
"for basic authentication (section [{0}])".format(option))
if self.auth_type == "token":
self.token = get_token(config)
if self.token is None:
raise ReportError(
"The `token` or `token_file` key must be set "
f"in the [{option}] section.")
if "token_expiration" in config or "token_name" in config:
try:
self.token_expiration = int(config["token_expiration"])
self.token_name = config["token_name"]
except KeyError:
raise ReportError(
"The ``token_name`` and ``token_expiration`` "
"must be set at the same time in "
"[{0}] section.".format(option))
except ValueError:
raise ReportError(
"The ``token_expiration`` must contain number, used in "
"[{0}] section.".format(option))
else:
self.token_expiration = self.token_name = None
# SSL verification
if "ssl_verify" in config:
try:
Expand Down Expand Up @@ -251,6 +318,11 @@ def session(self):
basic_auth = (self.auth_username, self.auth_password)
response = self._session.get(
self.auth_url, auth=basic_auth, verify=self.ssl_verify)
elif self.auth_type == "token":
self.session.headers["Authorization"] = f"Bearer {self.token}"
response = self._session.get(
"{0}/rest/api/content".format(self.url),
verify=self.ssl_verify)
else:
gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=DISABLED)
response = self._session.get(
Expand All @@ -261,4 +333,31 @@ def session(self):
log.error(error)
raise ReportError(
"Confluence authentication failed. Try kinit.")
if self.token_expiration:
response = self._session.get(
"{0}/rest/pat/latest/tokens".format(self.url),
verify=self.ssl_verify)
try:
response.raise_for_status()
token_found = None
for token in response.json():
if token["name"] == self.token_name:
token_found = token
break
if token_found is None:
raise ValueError(
f"Can't check validity for the '{self.token_name}' "
f"token as it doesn't exist.")
from datetime import datetime
expiring_at = datetime.strptime(
token_found["expiringAt"], r"%Y-%m-%dT%H:%M:%S.%f%z")
delta = (
expiring_at.astimezone() - datetime.now().astimezone())
if delta.days < self.token_expiration:
log.warning(
f"Confluence token '{self.token_name}' "
f"expires in {delta.days} days.")
except (requests.exceptions.HTTPError,
KeyError, ValueError) as error:
log.warning(error)
return self._session

0 comments on commit f60f59d

Please sign in to comment.