# Copyright 2024 Red Hat, Inc. Jose Castillo <jcastillo@redhat.com>
# This file is part of the sos project: https://github.com/sosreport/sos
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# version 2 of the GNU General Public License.
#
# See the LICENSE file in the source distribution for further information.
import os
import json
from sos.upload.targets import UploadTarget
from sos.utilities import convert_bytes, TIMEOUT_DEFAULT
from sos.policies.auth import DeviceAuthorizationClass
from sos.policies.distros.redhat import RHELPolicy
from sos import _sos as _
try:
import requests
REQUESTS_LOADED = True
except ImportError:
REQUESTS_LOADED = False
class RHELUploadTarget(UploadTarget):
client_identifier_url = "https://sso.redhat.com/auth/"\
"realms/redhat-external/protocol/openid-connect/auth/device"
token_endpoint = "https://sso.redhat.com/auth/realms/"\
"redhat-external/protocol/openid-connect/token"
upload_target_name = 'Red Hat Upload Target'
upload_target_id = "redhat"
def __init__(self, parser=None, args=None, cmdline=None):
super().__init__(parser=parser, args=args, cmdline=cmdline)
RH_API_HOST = "https://api.access.redhat.com"
RH_SFTP_HOST = "sftp://sftp.access.redhat.com"
_upload_url = RH_SFTP_HOST
_upload_method = 'post'
_device_token = None
# Max size for an http single request is 1Gb
_max_size_request = 1073741824
def check_distribution(self):
"""Return true if we are running in a RHEL system"""
return isinstance(self.commons['policy'], RHELPolicy)
def pre_work(self, hook_commons):
super().pre_work(hook_commons)
self.upload_directory = self.commons['cmdlineopts'].upload_directory
def prompt_for_upload_user(self):
if self.commons['cmdlineopts'].upload_user:
self.ui_log.info(
_("The option --upload-user has been deprecated in favour"
" of device authorization in RHEL")
)
if not self.commons['cmdlineopts'].case_id:
# no case id provided => failover to SFTP
self.upload_url = self.RH_SFTP_HOST
self.ui_log.info("No case id provided, uploading to SFTP")
def prompt_for_upload_password(self):
# With OIDC we don't ask for user/pass anymore
if self.commons['cmdlineopts'].upload_pass:
self.ui_log.info(
_("The option --upload-pass has been deprecated in favour"
" of device authorization in RHEL")
)
def get_upload_url(self):
rh_case_api = "/support/v1/cases/"\
f"{self.commons['cmdlineopts'].case_id}/attachments"
try:
if self.upload_url:
return self.upload_url
if self.commons['cmdlineopts'].upload_url:
return self.commons['cmdlineopts'].upload_url
if self.commons['cmdlineopts'].upload_protocol == 'sftp':
return self.RH_SFTP_HOST
if not self.commons['cmdlineopts'].case_id and not\
self.commons['policy'].prompt_for_case_id(
self.commons['cmdlineopts']):
return self.RH_SFTP_HOST
except Exception as e:
self.ui_log.info(
"There was a problem while setting the "
f"remote upload target: {e}"
)
return f"{self.RH_API_HOST}{rh_case_api}"
def _get_upload_https_auth(self):
str_auth = f"Bearer {self._device_token}"
return {'Authorization': str_auth}
def _upload_https_post(self, archive, verify=True):
"""If upload_https() needs to use requests.post(), use this method.
Policies should override this method instead of the base upload_https()
:param archive: The open archive file object
"""
files = {
'file': (archive.name.split('/')[-1], archive,
self._get_upload_headers())
}
# Get the access token at this point. With this,
# we cover the cases where report generation takes
# longer than the token timeout
RHELAuth = DeviceAuthorizationClass(
self.client_identifier_url,
self.token_endpoint
)
self._device_token = RHELAuth.get_access_token()
self.ui_log.info("Device authorized correctly. Uploading file to "
f"{self.get_upload_url_string()}")
return requests.post(self.get_upload_url(), files=files,
headers=self._get_upload_https_auth(),
verify=verify, timeout=TIMEOUT_DEFAULT)
def _get_upload_headers(self):
if self.get_upload_url().startswith(self.RH_API_HOST):
return {'isPrivate': 'false', 'cache-control': 'no-cache'}
return {}
def get_upload_url_string(self):
if self.get_upload_url().startswith(self.RH_API_HOST):
return "Red Hat Customer Portal"
if self.get_upload_url().startswith(self.RH_SFTP_HOST):
return "Red Hat Secure FTP"
return self._get_obfuscated_upload_url(self.upload_url)
def _get_sftp_upload_name(self):
"""The RH SFTP server will only automatically connect file uploads to
cases if the filename _starts_ with the case number
"""
fname = self.upload_archive_name.split('/')[-1]
if self.commons['cmdlineopts'].case_id:
fname = f"{self.commons['cmdlineopts'].case_id}_{fname}"
if self.upload_directory:
fname = os.path.join(self.upload_directory, fname)
return fname
# pylint: disable=too-many-branches
def upload_sftp(self, user=None, password=None):
"""Override the base upload_sftp to allow for setting an on-demand
generated anonymous login for the RH SFTP server if a username and
password are not given
"""
if self.RH_SFTP_HOST.split('//')[1] not in self.get_upload_url():
return super().upload_sftp()
if not REQUESTS_LOADED:
raise Exception("python3-requests is not installed and is required"
" for obtaining SFTP auth token.")
_token = None
_user = None
# We may have a device token already if we attempted
# to upload via http but the upload failed. So
# lets check first if there isn't one.
if not self._device_token:
try:
RHELAuth = DeviceAuthorizationClass(
self.client_identifier_url,
self.token_endpoint
)
except Exception as e:
# We end up here if the user cancels the device
# authentication in the web interface
if "end user denied" in str(e):
self.ui_log.info(
"Device token authorization "
"has been cancelled by the user."
)
else:
self._device_token = RHELAuth.get_access_token()
if self._device_token:
self.ui_log.info("Device authorized correctly. Uploading file to"
f" {self.get_upload_url_string()}")
url = self.RH_API_HOST + '/support/v2/sftp/token'
ret = None
if self._device_token:
headers = self._get_upload_https_auth()
ret = requests.post(url, headers=headers, timeout=10)
if ret.status_code == 200:
# credentials are valid
_user = json.loads(ret.text)['username']
_token = json.loads(ret.text)['token']
else:
self.ui_log.debug(
f"DEBUG: auth attempt failed (status: {ret.status_code}): "
f"{ret.json()}"
)
self.ui_log.error(
"Unable to retrieve Red Hat auth token using provided "
"credentials. Will try anonymous."
)
else:
adata = {"isAnonymous": True}
anon = requests.post(url, data=json.dumps(adata), timeout=10)
if anon.status_code == 200:
resp = json.loads(anon.text)
_user = resp['username']
_token = resp['token']
self.ui_log.info(
_(f"User {_user} used for anonymous upload. Please inform "
f"your support engineer so they may retrieve the data.")
)
else:
self.ui_log.debug(
f"DEBUG: anonymous request failed (status: "
f"{anon.status_code}): {anon.json()}"
)
if _user and _token:
return super().upload_sftp(user=_user, password=_token)
raise Exception("Could not retrieve valid or anonymous credentials")
def check_file_too_big(self, archive):
size = os.path.getsize(archive)
# Lets check if the size is bigger than the limit.
# There's really no need to transform the size to Gb,
# so we don't need to call any size converter implemented
# in tools.py
if size >= self._max_size_request:
self.ui_log.warning(
_("Size of archive is bigger than Red Hat Customer Portal "
"limit for uploads of "
f"{convert_bytes(self._max_size_request)} "
" via sos http upload. \n")
)
self.upload_url = self.RH_SFTP_HOST
def upload_archive(self, archive):
"""Override the base upload_archive to provide for automatic failover
from RHCP failures to the public RH dropbox
"""
try:
if self.get_upload_url().startswith(self.RH_API_HOST):
self.check_file_too_big(archive)
uploaded = super().upload_archive(archive)
except Exception as e:
uploaded = False
if not self.upload_url.startswith(self.RH_API_HOST):
raise
self.ui_log.error(
_(f"Upload to Red Hat Customer Portal failed due to "
f"{e}. Trying {self.RH_SFTP_HOST}")
)
self.upload_url = self.RH_SFTP_HOST
uploaded = super().upload_archive(archive)
return uploaded
# vim: set et ts=4 sw=4 :
Anons79 File Manager Version 1.0, Coded By Anons79
Email: [email protected]