mirror of
				https://git.proxmox.com/git/ceph.git
				synced 2025-11-04 03:09:26 +00:00 
			
		
		
		
	This commit essentially contains all changes from the following commits (most recent last): *f35168f671*86a553d66e*ab5c03b44dThe series file and the patches' prefixed numbers have been updated correspondingly. A very minor adaptation has been made to the patch added by commitf35168f671in order to get it to apply correctly. Signed-off-by: Max Carrara <m.carrara@proxmox.com> (cherry picked from commitf9e87274b7) Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
		
			
				
	
	
		
			280 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Diff
		
	
	
	
	
	
			
		
		
	
	
			280 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Diff
		
	
	
	
	
	
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
 | 
						|
From: Max Carrara <m.carrara@proxmox.com>
 | 
						|
Date: Tue, 2 Jan 2024 13:02:51 +0000
 | 
						|
Subject: [PATCH] backport: mgr/dashboard: simplify authentication protocol
 | 
						|
 | 
						|
This is a backport of https://github.com/ceph/ceph/pull/54710 which
 | 
						|
fixes the Ceph Dashboard not being able to launch on Ceph Reef running
 | 
						|
on Debian Bookworm.
 | 
						|
 | 
						|
This is achieved by removing the dependency on `PyJWT` (Python) and thus
 | 
						|
transitively also removing the dependency on `cryptography` (Python).
 | 
						|
For more information, see the original pull request.
 | 
						|
 | 
						|
Note that the Ceph Dashboard still cannot be used if TLS is activated,
 | 
						|
because `pyOpenSSL` is used to verify certs during launch. Disabling
 | 
						|
TLS via `ceph config set mgr mgr/dashboard/ssl false` and using e.g.
 | 
						|
a reverse proxy can be used as a workaround.
 | 
						|
 | 
						|
A separate patch is required to allow the dashboard to run with TLS
 | 
						|
enabled.
 | 
						|
 | 
						|
Fixes: https://forum.proxmox.com/threads/ceph-warning-post-upgrade-to-v8.129371
 | 
						|
Signed-off-by: Daniel Persson <mailto.woden@gmail.com>
 | 
						|
Signed-off-by: Max Carrara <m.carrara@proxmox.com>
 | 
						|
---
 | 
						|
 ceph.spec.in                                  |  4 --
 | 
						|
 debian/control                                |  1 -
 | 
						|
 src/pybind/mgr/dashboard/constraints.txt      |  1 -
 | 
						|
 src/pybind/mgr/dashboard/exceptions.py        | 12 ++++
 | 
						|
 .../mgr/dashboard/requirements-lint.txt       |  1 +
 | 
						|
 .../mgr/dashboard/requirements-test.txt       |  1 +
 | 
						|
 src/pybind/mgr/dashboard/requirements.txt     |  1 -
 | 
						|
 src/pybind/mgr/dashboard/services/auth.py     | 70 ++++++++++++++++---
 | 
						|
 8 files changed, 75 insertions(+), 16 deletions(-)
 | 
						|
 | 
						|
diff --git a/ceph.spec.in b/ceph.spec.in
 | 
						|
index e4cbbef6943..457d7a6dc56 100644
 | 
						|
--- a/ceph.spec.in
 | 
						|
+++ b/ceph.spec.in
 | 
						|
@@ -403,7 +403,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
 | 
						|
@@ -416,7 +415,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
 | 
						|
@@ -608,7 +606,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}
 | 
						|
@@ -617,7 +614,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 e3f2073ce8f..ba34a163f54 100644
 | 
						|
--- a/debian/control
 | 
						|
+++ b/debian/control
 | 
						|
@@ -87,7 +87,6 @@ Build-Depends: automake,
 | 
						|
                python3-all-dev,
 | 
						|
                python3-cherrypy3,
 | 
						|
                python3-natsort,
 | 
						|
-               python3-jwt <pkg.ceph.check>,
 | 
						|
                python3-pecan <pkg.ceph.check>,
 | 
						|
                python3-bcrypt <pkg.ceph.check>,
 | 
						|
                tox <pkg.ceph.check>,
 | 
						|
diff --git a/src/pybind/mgr/dashboard/constraints.txt b/src/pybind/mgr/dashboard/constraints.txt
 | 
						|
index 55f81c92dec..fd614104880 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 96cbc523356..d396a38d2c3 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-lint.txt b/src/pybind/mgr/dashboard/requirements-lint.txt
 | 
						|
index d82fa1ace1d..5fe9957c32a 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.16.0
 | 
						|
+PyJWT~=2.0
 | 
						|
diff --git a/src/pybind/mgr/dashboard/requirements-test.txt b/src/pybind/mgr/dashboard/requirements-test.txt
 | 
						|
index 4e925e8616f..78fd1d5b742 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.16.0
 | 
						|
+PyJWT~=2.0
 | 
						|
diff --git a/src/pybind/mgr/dashboard/requirements.txt b/src/pybind/mgr/dashboard/requirements.txt
 | 
						|
index 8003d62a552..292971819c9 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 f13963abffd..3c600231252 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 @@ class JwtManager(object):
 | 
						|
             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 @@ class JwtManager(object):
 | 
						|
             '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 @@ class JwtManager(object):
 | 
						|
     @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 @@ class JwtManager(object):
 | 
						|
                 )
 | 
						|
             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']
 | 
						|
-- 
 | 
						|
2.39.2
 | 
						|
 |