Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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]>
sandrobonazzola committed Sep 2, 2024

Verified

This commit was signed with the committer’s verified signature.
ekohl Ewoud Kohl van Wijngaarden
1 parent c068154 commit 1cd2064
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
@@ -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
@@ -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

@@ -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
@@ -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)


@@ -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):
@@ -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")]


@@ -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:
@@ -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(
@@ -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 1cd2064

Please sign in to comment.