diff --git a/ceph-18.2.2-backport-mgr-dashboard-simplify-authentication-protocol.patch b/ceph-18.2.2-backport-mgr-dashboard-simplify-authentication-protocol.patch new file mode 100644 index 0000000..584a807 --- /dev/null +++ b/ceph-18.2.2-backport-mgr-dashboard-simplify-authentication-protocol.patch @@ -0,0 +1,321 @@ +From fcbd8fdae9d80d9a9bd61838aeaf41b504a5888a Mon Sep 17 00:00:00 2001 +From: Daniel Persson +Date: Wed, 29 Nov 2023 09:39:51 +0000 +Subject: [PATCH 1/3] mgr/dashboard: Simplify authentication protocol By + removing the dependency to PyJWT we also remove the dependency to the + cryptographic library which in the dashboard module will create a crash. In + newer implementations of the library PyO3 is used to run rust code in order + to encrypt with Elliptic Curves. This is never used in the dashboard + communication so a much simpler implementation where we only use the hmac + sha256 algorithm to create the signed JWT message could be used. + +Fixes: https://forum.proxmox.com/threads/ceph-warning-post-upgrade-to-v8.129371 +Signed-off-by: Daniel Persson +(cherry picked from commit c616a9d017b5fcc85bb5c1556bccf4c77cc3899e) +--- + src/pybind/mgr/dashboard/constraints.txt | 1 - + src/pybind/mgr/dashboard/exceptions.py | 12 ++++ + src/pybind/mgr/dashboard/requirements.txt | 1 - + src/pybind/mgr/dashboard/services/auth.py | 70 ++++++++++++++++++++--- + src/pybind/mgr/dashboard/tox.ini | 1 + + 5 files changed, 74 insertions(+), 11 deletions(-) + +diff --git a/src/pybind/mgr/dashboard/constraints.txt b/src/pybind/mgr/dashboard/constraints.txt +index 55f81c92dec06..fd6141048800a 100644 +--- a/src/pybind/mgr/dashboard/constraints.txt ++++ b/src/pybind/mgr/dashboard/constraints.txt +@@ -1,6 +1,5 @@ + CherryPy~=13.1 + more-itertools~=8.14 +-PyJWT~=2.0 + bcrypt~=3.1 + python3-saml~=1.4 + requests~=2.26 +diff --git a/src/pybind/mgr/dashboard/exceptions.py b/src/pybind/mgr/dashboard/exceptions.py +index 96cbc52335613..d396a38d2c3a2 100644 +--- a/src/pybind/mgr/dashboard/exceptions.py ++++ b/src/pybind/mgr/dashboard/exceptions.py +@@ -121,3 +121,15 @@ class GrafanaError(Exception): + + class PasswordPolicyException(Exception): + pass ++ ++ ++class ExpiredSignatureError(Exception): ++ pass ++ ++ ++class InvalidTokenError(Exception): ++ pass ++ ++ ++class InvalidAlgorithmError(Exception): ++ pass +diff --git a/src/pybind/mgr/dashboard/requirements.txt b/src/pybind/mgr/dashboard/requirements.txt +index 8003d62a5523f..292971819c9c6 100644 +--- a/src/pybind/mgr/dashboard/requirements.txt ++++ b/src/pybind/mgr/dashboard/requirements.txt +@@ -1,7 +1,6 @@ + bcrypt + CherryPy + more-itertools +-PyJWT + pyopenssl + requests + Routes +diff --git a/src/pybind/mgr/dashboard/services/auth.py b/src/pybind/mgr/dashboard/services/auth.py +index f13963abffdd4..3c6002312524d 100644 +--- a/src/pybind/mgr/dashboard/services/auth.py ++++ b/src/pybind/mgr/dashboard/services/auth.py +@@ -1,17 +1,19 @@ + # -*- coding: utf-8 -*- + ++import base64 ++import hashlib ++import hmac + import json + import logging + import os + import threading + import time + import uuid +-from base64 import b64encode + + import cherrypy +-import jwt + + from .. import mgr ++from ..exceptions import ExpiredSignatureError, InvalidAlgorithmError, InvalidTokenError + from .access_control import LocalAuthenticator, UserDoesNotExist + + cherrypy.config.update({ +@@ -33,7 +35,7 @@ class JwtManager(object): + @staticmethod + def _gen_secret(): + secret = os.urandom(16) +- return b64encode(secret).decode('utf-8') ++ return base64.b64encode(secret).decode('utf-8') + + @classmethod + def init(cls): +@@ -45,6 +47,54 @@ def init(cls): + mgr.set_store('jwt_secret', secret) + cls._secret = secret + ++ @classmethod ++ def array_to_base64_string(cls, message): ++ jsonstr = json.dumps(message, sort_keys=True).replace(" ", "") ++ string_bytes = base64.urlsafe_b64encode(bytes(jsonstr, 'UTF-8')) ++ return string_bytes.decode('UTF-8').replace("=", "") ++ ++ @classmethod ++ def encode(cls, message, secret): ++ header = {"alg": cls.JWT_ALGORITHM, "typ": "JWT"} ++ base64_header = cls.array_to_base64_string(header) ++ base64_message = cls.array_to_base64_string(message) ++ base64_secret = base64.urlsafe_b64encode(hmac.new( ++ bytes(secret, 'UTF-8'), ++ msg=bytes(base64_header + "." + base64_message, 'UTF-8'), ++ digestmod=hashlib.sha256 ++ ).digest()).decode('UTF-8').replace("=", "") ++ return base64_header + "." + base64_message + "." + base64_secret ++ ++ @classmethod ++ def decode(cls, message, secret): ++ split_message = message.split(".") ++ base64_header = split_message[0] ++ base64_message = split_message[1] ++ base64_secret = split_message[2] ++ ++ decoded_header = json.loads(base64.urlsafe_b64decode(base64_header)) ++ ++ if decoded_header['alg'] != cls.JWT_ALGORITHM: ++ raise InvalidAlgorithmError() ++ ++ incoming_secret = base64.urlsafe_b64encode(hmac.new( ++ bytes(secret, 'UTF-8'), ++ msg=bytes(base64_header + "." + base64_message, 'UTF-8'), ++ digestmod=hashlib.sha256 ++ ).digest()).decode('UTF-8').replace("=", "") ++ ++ if base64_secret != incoming_secret: ++ raise InvalidTokenError() ++ ++ # We add ==== as padding to ignore the requirement to have correct padding in ++ # the urlsafe_b64decode method. ++ decoded_message = json.loads(base64.urlsafe_b64decode(base64_message + "====")) ++ now = int(time.time()) ++ if decoded_message['exp'] < now: ++ raise ExpiredSignatureError() ++ ++ return decoded_message ++ + @classmethod + def gen_token(cls, username): + if not cls._secret: +@@ -59,13 +109,13 @@ def gen_token(cls, username): + 'iat': now, + 'username': username + } +- return jwt.encode(payload, cls._secret, algorithm=cls.JWT_ALGORITHM) # type: ignore ++ return cls.encode(payload, cls._secret) # type: ignore + + @classmethod + def decode_token(cls, token): + if not cls._secret: + cls.init() +- return jwt.decode(token, cls._secret, algorithms=cls.JWT_ALGORITHM) # type: ignore ++ return cls.decode(token, cls._secret) # type: ignore + + @classmethod + def get_token_from_header(cls): +@@ -99,8 +149,8 @@ def get_username(cls): + @classmethod + def get_user(cls, token): + try: +- dtoken = JwtManager.decode_token(token) +- if not JwtManager.is_blocklisted(dtoken['jti']): ++ dtoken = cls.decode_token(token) ++ if not cls.is_blocklisted(dtoken['jti']): + user = AuthManager.get_user(dtoken['username']) + if user.last_update <= dtoken['iat']: + return user +@@ -110,10 +160,12 @@ def get_user(cls, token): + ) + else: + cls.logger.debug('Token is block-listed') # type: ignore +- except jwt.ExpiredSignatureError: ++ except ExpiredSignatureError: + cls.logger.debug("Token has expired") # type: ignore +- except jwt.InvalidTokenError: ++ except InvalidTokenError: + cls.logger.debug("Failed to decode token") # type: ignore ++ except InvalidAlgorithmError: ++ cls.logger.debug("Only the HS256 algorithm is supported.") # type: ignore + except UserDoesNotExist: + cls.logger.debug( # type: ignore + "Invalid token: user %s does not exist", dtoken['username'] +diff --git a/src/pybind/mgr/dashboard/tox.ini b/src/pybind/mgr/dashboard/tox.ini +index 47756e946e125..271df286ec5e8 100644 +--- a/src/pybind/mgr/dashboard/tox.ini ++++ b/src/pybind/mgr/dashboard/tox.ini +@@ -20,6 +20,7 @@ addopts = + deps = + -rrequirements.txt + -cconstraints.txt ++ PyJWT + + [base-test] + deps = + +From d456590743aa26d7d253b20294f5aa33544ab8a7 Mon Sep 17 00:00:00 2001 +From: Daniel Persson +Date: Sun, 3 Dec 2023 08:03:47 +0000 +Subject: [PATCH 2/3] mgr/dashboard: Changes suggested after review by + @epuertat. + +Move the JWT requirement to the test requirements file. Also remove JWT from ceph specification and debian build. + +Signed-off-by: Daniel Persson +(cherry picked from commit c1ea66fe12f86e7a63681cba860fb91b1ea86e12) +--- + ceph.spec.in | 4 ---- + debian/control | 1 - + src/pybind/mgr/dashboard/requirements-test.txt | 1 + + src/pybind/mgr/dashboard/tox.ini | 1 - + 4 files changed, 1 insertion(+), 6 deletions(-) + +diff --git a/ceph.spec.in b/ceph.spec.in +index ff8aa5aafbff8..c4281abc5bfbb 100644 +--- a/ceph.spec.in ++++ b/ceph.spec.in +@@ -412,7 +412,6 @@ BuildRequires: xmlsec1-nss + BuildRequires: xmlsec1-openssl + BuildRequires: xmlsec1-openssl-devel + BuildRequires: python%{python3_pkgversion}-cherrypy +-BuildRequires: python%{python3_pkgversion}-jwt + BuildRequires: python%{python3_pkgversion}-routes + BuildRequires: python%{python3_pkgversion}-scipy + BuildRequires: python%{python3_pkgversion}-werkzeug +@@ -425,7 +424,6 @@ BuildRequires: libxmlsec1-1 + BuildRequires: libxmlsec1-nss1 + BuildRequires: libxmlsec1-openssl1 + BuildRequires: python%{python3_pkgversion}-CherryPy +-BuildRequires: python%{python3_pkgversion}-PyJWT + BuildRequires: python%{python3_pkgversion}-Routes + BuildRequires: python%{python3_pkgversion}-Werkzeug + BuildRequires: python%{python3_pkgversion}-numpy-devel +@@ -617,7 +615,6 @@ Requires: ceph-prometheus-alerts = %{_epoch_prefix}%{version}-%{release} + Requires: python%{python3_pkgversion}-setuptools + %if 0%{?fedora} || 0%{?rhel} + Requires: python%{python3_pkgversion}-cherrypy +-Requires: python%{python3_pkgversion}-jwt + Requires: python%{python3_pkgversion}-routes + Requires: python%{python3_pkgversion}-werkzeug + %if 0%{?weak_deps} +@@ -626,7 +623,6 @@ Recommends: python%{python3_pkgversion}-saml + %endif + %if 0%{?suse_version} + Requires: python%{python3_pkgversion}-CherryPy +-Requires: python%{python3_pkgversion}-PyJWT + Requires: python%{python3_pkgversion}-Routes + Requires: python%{python3_pkgversion}-Werkzeug + Recommends: python%{python3_pkgversion}-python3-saml +diff --git a/debian/control b/debian/control +index 837a55a371670..e7b123ec381e7 100644 +--- a/debian/control ++++ b/debian/control +@@ -91,7 +91,6 @@ Build-Depends: automake, + python3-all-dev, + python3-cherrypy3, + python3-natsort, +- python3-jwt , + python3-pecan , + python3-bcrypt , + tox , +diff --git a/src/pybind/mgr/dashboard/requirements-test.txt b/src/pybind/mgr/dashboard/requirements-test.txt +index da283d0b64aaa..aa80b3336b540 100644 +--- a/src/pybind/mgr/dashboard/requirements-test.txt ++++ b/src/pybind/mgr/dashboard/requirements-test.txt +@@ -2,3 +2,4 @@ pytest-cov + pytest-instafail + pyfakefs==4.5.0 + jsonschema~=4.0 ++PyJWT~=2.0 +diff --git a/src/pybind/mgr/dashboard/tox.ini b/src/pybind/mgr/dashboard/tox.ini +index 271df286ec5e8..47756e946e125 100644 +--- a/src/pybind/mgr/dashboard/tox.ini ++++ b/src/pybind/mgr/dashboard/tox.ini +@@ -20,7 +20,6 @@ addopts = + deps = + -rrequirements.txt + -cconstraints.txt +- PyJWT + + [base-test] + deps = + +From 04b3792228415fa0320eb2e28c60e00fac68d3d8 Mon Sep 17 00:00:00 2001 +From: Daniel Persson +Date: Sun, 3 Dec 2023 09:46:56 +0000 +Subject: [PATCH 3/3] mgr/dashboard: Updated test dependencies + +Seemed that the test dependencies was separated in two different requirements files +one for the testing and one for linting. Added the JWT dependency in the linting file +as well. + +Signed-off-by: Daniel Persson +(cherry picked from commit 06765e648acb1676d5d563c631b8d8fc08b5323c) +--- + src/pybind/mgr/dashboard/requirements-lint.txt | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/src/pybind/mgr/dashboard/requirements-lint.txt b/src/pybind/mgr/dashboard/requirements-lint.txt +index 57e5191574083..571c92a4ebfbc 100644 +--- a/src/pybind/mgr/dashboard/requirements-lint.txt ++++ b/src/pybind/mgr/dashboard/requirements-lint.txt +@@ -9,3 +9,4 @@ autopep8==1.5.7 + pyfakefs==4.5.0 + isort==5.5.3 + jsonschema~=4.0 ++PyJWT~=2.0