diff --git a/.gitignore b/.gitignore index 454c605..8f5236b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -SOURCES/sos-4.6.1.tar.gz +SOURCES/sos-4.7.0.tar.gz SOURCES/sos-audit-0.3.tgz diff --git a/.sos.metadata b/.sos.metadata index ebf25d3..67aeec0 100644 --- a/.sos.metadata +++ b/.sos.metadata @@ -1,2 +1,2 @@ -b6999d34ade3b3d0b88390ab525d31c6a8dc2950 SOURCES/sos-4.6.1.tar.gz +7d1629848263be2d613983fb15cd418dccdf1c76 SOURCES/sos-4.7.0.tar.gz 9d478b9f0085da9178af103078bbf2fd77b0175a SOURCES/sos-audit-0.3.tgz diff --git a/SOURCES/sos-RHEL-21178-device-auth.patch b/SOURCES/sos-RHEL-21178-device-auth.patch deleted file mode 100644 index 0cc9474..0000000 --- a/SOURCES/sos-RHEL-21178-device-auth.patch +++ /dev/null @@ -1,502 +0,0 @@ -From c1a08482f9f724395102be22d94382cbda14dbce Mon Sep 17 00:00:00 2001 -From: Jose Castillo -Date: Mon, 9 Oct 2023 16:28:15 +0100 -Subject: [PATCH] [redhat] Change authentication method for RHEL - -The authentication method for RHEL uploads to the -customer portal is changing in 2024 to Device Auth -tokens, from user/password basic authorization. -To accomplish this, one new class is created: -DeviceAuth (deviceauth.py), that takes care of -managing OID token authentication. - -Closes: RH: SUPDEV-63 - -Signed-off-by: Jose Castillo ---- - sos/policies/auth/__init__.py | 210 +++++++++++++++++++++++++++++++++ - sos/policies/distros/redhat.py | 121 ++++++++++++++----- - 2 files changed, 300 insertions(+), 31 deletions(-) - create mode 100644 sos/policies/auth/__init__.py - -diff --git a/sos/policies/auth/__init__.py b/sos/policies/auth/__init__.py -new file mode 100644 -index 000000000..5b62a4953 ---- /dev/null -+++ b/sos/policies/auth/__init__.py -@@ -0,0 +1,210 @@ -+# Copyright (C) 2023 Red Hat, Inc., Jose Castillo -+ -+# 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 logging -+try: -+ import requests -+ REQUESTS_LOADED = True -+except ImportError: -+ REQUESTS_LOADED = False -+import time -+from datetime import datetime, timedelta -+ -+DEVICE_AUTH_CLIENT_ID = "sos-tools" -+GRANT_TYPE_DEVICE_CODE = "urn:ietf:params:oauth:grant-type:device_code" -+ -+logger = logging.getLogger("sos") -+ -+ -+class DeviceAuthorizationClass: -+ """ -+ Device Authorization Class -+ """ -+ -+ def __init__(self, client_identifier_url, token_endpoint): -+ -+ self._access_token = None -+ self._access_expires_at = None -+ self.__device_code = None -+ -+ self.client_identifier_url = client_identifier_url -+ self.token_endpoint = token_endpoint -+ self._use_device_code_grant() -+ -+ def _use_device_code_grant(self): -+ """ -+ Start the device auth flow. In the future we will -+ store the tokens in an in-memory keyring. -+ -+ """ -+ -+ self._request_device_code() -+ print( -+ "Please visit the following URL to authenticate this" -+ f" device: {self._verification_uri_complete}" -+ ) -+ self.poll_for_auth_completion() -+ -+ def _request_device_code(self): -+ """ -+ Initialize new Device Authorization Grant attempt by -+ requesting a new device code. -+ -+ """ -+ data = "client_id={}".format(DEVICE_AUTH_CLIENT_ID) -+ headers = {'content-type': 'application/x-www-form-urlencoded'} -+ if not REQUESTS_LOADED: -+ raise Exception("python3-requests is not installed and is required" -+ " for obtaining device auth token.") -+ try: -+ res = requests.post( -+ self.client_identifier_url, -+ data=data, -+ headers=headers) -+ res.raise_for_status() -+ response = res.json() -+ self._user_code = response.get("user_code") -+ self._verification_uri = response.get("verification_uri") -+ self._interval = response.get("interval") -+ self.__device_code = response.get("device_code") -+ self._verification_uri_complete = response.get( -+ "verification_uri_complete") -+ except requests.HTTPError as e: -+ raise requests.HTTPError("HTTP request failed " -+ "while attempting to acquire the tokens." -+ f"Error returned was {res.status_code} " -+ f"{e}") -+ -+ def poll_for_auth_completion(self): -+ """ -+ Continuously poll OIDC token endpoint until the user is successfully -+ authenticated or an error occurs. -+ -+ """ -+ token_data = {'grant_type': GRANT_TYPE_DEVICE_CODE, -+ 'client_id': DEVICE_AUTH_CLIENT_ID, -+ 'device_code': self.__device_code} -+ -+ if not REQUESTS_LOADED: -+ raise Exception("python3-requests is not installed and is required" -+ " for obtaining device auth token.") -+ while self._access_token is None: -+ time.sleep(self._interval) -+ try: -+ check_auth_completion = requests.post(self.token_endpoint, -+ data=token_data) -+ -+ status_code = check_auth_completion.status_code -+ -+ if status_code == 200: -+ logger.info("The SSO authentication is successful") -+ self._set_token_data(check_auth_completion.json()) -+ if status_code not in [200, 400]: -+ raise Exception(status_code, check_auth_completion.text) -+ if status_code == 400 and \ -+ check_auth_completion.json()['error'] not in \ -+ ("authorization_pending", "slow_down"): -+ raise Exception(status_code, check_auth_completion.text) -+ except requests.exceptions.RequestException as e: -+ logger.error(f"Error was found while posting a request: {e}") -+ -+ def _set_token_data(self, token_data): -+ """ -+ Set the class attributes as per the input token_data received. -+ In the future we will persist the token data in a local, -+ in-memory keyring, to avoid visting the browser frequently. -+ :param token_data: Token data containing access_token, refresh_token -+ and their expiry etc. -+ """ -+ self._access_token = token_data.get("access_token") -+ self._access_expires_at = datetime.utcnow() + \ -+ timedelta(seconds=token_data.get("expires_in")) -+ self._refresh_token = token_data.get("refresh_token") -+ self._refresh_expires_in = token_data.get("refresh_expires_in") -+ if self._refresh_expires_in == 0: -+ self._refresh_expires_at = datetime.max -+ else: -+ self._refresh_expires_at = datetime.utcnow() + \ -+ timedelta(seconds=self._refresh_expires_in) -+ -+ def get_access_token(self): -+ """ -+ Get the valid access_token at any given time. -+ :return: Access_token -+ :rtype: string -+ """ -+ if self.is_access_token_valid(): -+ return self._access_token -+ else: -+ if self.is_refresh_token_valid(): -+ self._use_refresh_token_grant() -+ return self._access_token -+ else: -+ self._use_device_code_grant() -+ return self._access_token -+ -+ def is_access_token_valid(self): -+ """ -+ Check the validity of access_token. We are considering it invalid 180 -+ sec. prior to it's exact expiry time. -+ :return: True/False -+ -+ """ -+ return self._access_token and self._access_expires_at and \ -+ self._access_expires_at - timedelta(seconds=180) > \ -+ datetime.utcnow() -+ -+ def is_refresh_token_valid(self): -+ """ -+ Check the validity of refresh_token. We are considering it invalid -+ 180 sec. prior to it's exact expiry time. -+ -+ :return: True/False -+ -+ """ -+ return self._refresh_token and self._refresh_expires_at and \ -+ self._refresh_expires_at - timedelta(seconds=180) > \ -+ datetime.utcnow() -+ -+ def _use_refresh_token_grant(self, refresh_token=None): -+ """ -+ Fetch the new access_token and refresh_token using the existing -+ refresh_token and persist it. -+ :param refresh_token: optional param for refresh_token -+ -+ """ -+ if not REQUESTS_LOADED: -+ raise Exception("python3-requests is not installed and is required" -+ " for obtaining device auth token.") -+ refresh_token_data = {'client_id': DEVICE_AUTH_CLIENT_ID, -+ 'grant_type': 'refresh_token', -+ 'refresh_token': self._refresh_token if not -+ refresh_token else refresh_token} -+ -+ refresh_token_res = requests.post(self.token_endpoint, -+ data=refresh_token_data) -+ -+ if refresh_token_res.status_code == 200: -+ self._set_token_data(refresh_token_res.json()) -+ -+ elif refresh_token_res.status_code == 400 and 'invalid' in\ -+ refresh_token_res.json()['error']: -+ logger.warning("Problem while fetching the new tokens from refresh" -+ " token grant - {} {}." -+ " New Device code will be requested !".format -+ (refresh_token_res.status_code, -+ refresh_token_res.json()['error'])) -+ self._use_device_code_grant() -+ else: -+ raise Exception( -+ "Something went wrong while using the " -+ "Refresh token grant for fetching tokens:" -+ f" Returned status code {refresh_token_res.status_code}" -+ f" and error {refresh_token_res.json()['error']}") -diff --git a/sos/policies/distros/redhat.py b/sos/policies/distros/redhat.py -index bdbe8f952..02cc4cc2f 100644 ---- a/sos/policies/distros/redhat.py -+++ b/sos/policies/distros/redhat.py -@@ -12,6 +12,7 @@ - import os - import sys - import re -+from sos.policies.auth import DeviceAuthorizationClass - - from sos.report.plugins import RedHatPlugin - from sos.presets.redhat import (RHEL_PRESETS, ATOMIC_PRESETS, RHV, RHEL, -@@ -51,6 +52,10 @@ class RedHatPolicy(LinuxPolicy): - default_container_runtime = 'podman' - sos_pkg_name = 'sos' - sos_bin_path = '/usr/sbin' -+ 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" - - def __init__(self, sysroot=None, init=None, probe_runtime=True, - remote_exec=None): -@@ -228,6 +233,7 @@ class RHELPolicy(RedHatPolicy): - """ + disclaimer_text + "%(vendor_text)s\n") - _upload_url = RH_SFTP_HOST - _upload_method = 'post' -+ _device_token = None - - def __init__(self, sysroot=None, init=None, probe_runtime=True, - remote_exec=None): -@@ -266,24 +272,23 @@ def check(cls, remote=''): - - def prompt_for_upload_user(self): - if self.commons['cmdlineopts'].upload_user: -- return -- # Not using the default, so don't call this prompt for RHCP -- if self.commons['cmdlineopts'].upload_url: -- super(RHELPolicy, self).prompt_for_upload_user() -- return -- if not self.get_upload_user(): -- if self.case_id: -- self.upload_user = input(_( -- "Enter your Red Hat Customer Portal username for " -- "uploading [empty for anonymous SFTP]: ") -- ) -- else: # no case id provided => failover to SFTP -- self.upload_url = RH_SFTP_HOST -- self.ui_log.info("No case id provided, uploading to SFTP") -- self.upload_user = input(_( -- "Enter your Red Hat Customer Portal username for " -- "uploading to SFTP [empty for anonymous]: ") -- ) -+ self.ui_log.info( -+ _("The option --upload-user has been deprecated in favour" -+ " of device authorization in RHEL") -+ ) -+ if not self.case_id: -+ # no case id provided => failover to SFTP -+ self.upload_url = 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") -+ ) -+ return - - def get_upload_url(self): - if self.upload_url: -@@ -292,10 +297,42 @@ def get_upload_url(self): - return self.commons['cmdlineopts'].upload_url - elif self.commons['cmdlineopts'].upload_protocol == 'sftp': - return RH_SFTP_HOST -+ elif not self.commons['cmdlineopts'].case_id: -+ self.ui_log.info("No case id provided, uploading to SFTP") -+ return RH_SFTP_HOST - else: - rh_case_api = "/support/v1/cases/%s/attachments" - return RH_API_HOST + rh_case_api % self.case_id - -+ def _get_upload_https_auth(self): -+ str_auth = "Bearer {}".format(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) -+ - def _get_upload_headers(self): - if self.get_upload_url().startswith(RH_API_HOST): - return {'isPrivate': 'false', 'cache-control': 'no-cache'} -@@ -332,15 +369,38 @@ def upload_sftp(self): - " 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 = RH_API_HOST + '/support/v2/sftp/token' -- # we have a username and password, but we need to reset the password -- # to be the token returned from the auth endpoint -- if self.get_upload_user() and self.get_upload_password(): -- auth = self.get_upload_https_auth() -- ret = requests.post(url, auth=auth, timeout=10) -+ 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 = self.get_upload_user() -+ _user = json.loads(ret.text)['username'] - _token = json.loads(ret.text)['token'] - else: - self.ui_log.debug( -@@ -351,8 +411,7 @@ def upload_sftp(self): - "Unable to retrieve Red Hat auth token using provided " - "credentials. Will try anonymous." - ) -- # we either do not have a username or password/token, or both -- if not _token: -+ else: - adata = {"isAnonymous": True} - anon = requests.post(url, data=json.dumps(adata), timeout=10) - if anon.status_code == 200: -@@ -368,7 +427,6 @@ def upload_sftp(self): - f"DEBUG: anonymous request failed (status: " - f"{anon.status_code}): {anon.json()}" - ) -- - if _user and _token: - return super(RHELPolicy, self).upload_sftp(user=_user, - password=_token) -@@ -380,17 +438,18 @@ def upload_archive(self, archive): - """ - try: - if self.upload_url and self.upload_url.startswith(RH_API_HOST) and\ -- (not self.get_upload_user() or not self.get_upload_password()): -+ (not self.get_upload_user() or -+ not self.get_upload_password()): - self.upload_url = RH_SFTP_HOST - uploaded = super(RHELPolicy, self).upload_archive(archive) -- except Exception: -+ except Exception as e: - uploaded = False - if not self.upload_url.startswith(RH_API_HOST): - raise - else: - self.ui_log.error( -- _(f"Upload to Red Hat Customer Portal failed. Trying " -- f"{RH_SFTP_HOST}") -+ _(f"Upload to Red Hat Customer Portal failed due to " -+ f"{e}. Trying {RH_SFTP_HOST}") - ) - self.upload_url = RH_SFTP_HOST - uploaded = super(RHELPolicy, self).upload_archive(archive) -From d338a232cd7c829ca8ca5e5febef51035d1f7da5 Mon Sep 17 00:00:00 2001 -From: Pavel Moravec -Date: Wed, 10 Jan 2024 16:47:44 +0100 -Subject: [PATCH] [build] Bump version to 4.6.1 - -Signed-off-by: Pavel Moravec ---- - docs/conf.py | 4 ++-- - sos.spec | 5 ++++- - sos/__init__.py | 2 +- - 3 files changed, 7 insertions(+), 4 deletions(-) - -diff --git a/docs/conf.py b/docs/conf.py -index 5f105373e..57d1b9297 100644 ---- a/docs/conf.py -+++ b/docs/conf.py -@@ -59,9 +59,9 @@ - # built documents. - # - # The short X.Y version. --version = '4.6.0' -+version = '4.6.1' - # The full version, including alpha/beta/rc tags. --release = '4.6.0' -+release = '4.6.1' - - # The language for content autogenerated by Sphinx. Refer to documentation - # for a list of supported languages. -diff --git a/sos.spec b/sos.spec -index b575b5232..a08e2857b 100644 ---- a/sos.spec -+++ b/sos.spec -@@ -1,6 +1,6 @@ - Summary: A set of tools to gather troubleshooting information from a system - Name: sos --Version: 4.6.0 -+Version: 4.6.1 - Release: 1%{?dist} - Source0: https://github.com/sosreport/sos/archive/%{name}-%{version}.tar.gz - License: GPL-2.0-or-later -@@ -90,6 +90,9 @@ rm -rf %{buildroot}/usr/config/ - %config(noreplace) %{_sysconfdir}/sos/sos.conf - - %changelog -+* Wed Jan 10 2024 Pavel Moravec = 4.6.1 -+- New upstream release -+ - * Thu Aug 17 2023 Jake Hunsaker = 4.6.0 - - New upstream release - -diff --git a/sos/__init__.py b/sos/__init__.py -index 78e452676..18d18c4c7 100644 ---- a/sos/__init__.py -+++ b/sos/__init__.py -@@ -14,7 +14,7 @@ - This module houses the i18n setup and message function. The default is to use - gettext to internationalize messages. - """ --__version__ = "4.6.0" -+__version__ = "4.6.1" - - import os - import sys diff --git a/SPECS/sos.spec b/SPECS/sos.spec index 4b71994..f011c80 100644 --- a/SPECS/sos.spec +++ b/SPECS/sos.spec @@ -4,7 +4,7 @@ Summary: A set of tools to gather troubleshooting information from a system Name: sos -Version: 4.6.1 +Version: 4.7.0 Release: 1%{?dist} Group: Applications/System Source0: https://github.com/sosreport/sos/archive/%{version}/sos-%{version}.tar.gz @@ -22,7 +22,6 @@ Recommends: python3-pexpect Recommends: python3-pyyaml Conflicts: vdsm < 4.40 Obsoletes: sos-collector <= 1.9 -Patch1: sos-RHEL-21178-device-auth.patch %description Sos is a set of tools that gathers information about system @@ -33,7 +32,6 @@ support technicians and developers. %prep %setup -qn %{name}-%{version} %setup -T -D -a1 -q -%patch1 -p1 %build %py3_build @@ -106,6 +104,10 @@ of the system. Currently storage and filesystem commands are audited. %changelog +* Tue Feb 20 2024 Jan Jansky = 4.7.0-1 +- rebase to upstream 4.7.0 + Resolves: RHEL-26115 + * Thu Jan 11 2024 Pavel Moravec = 4.6.1-1 - rebase to upstream 4.6.1 Resolves: RHEL-21174