From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 0BBA4BEDF1 for ; Tue, 2 Jan 2024 18:37:12 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id DD0EB1A934 for ; Tue, 2 Jan 2024 18:37:11 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Tue, 2 Jan 2024 18:37:10 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 13CF442FB7 for ; Tue, 2 Jan 2024 18:37:10 +0100 (CET) From: Max Carrara To: pve-devel@lists.proxmox.com Date: Tue, 2 Jan 2024 18:37:03 +0100 Message-Id: <20240102173703.829892-1-m.carrara@proxmox.com> X-Mailer: git-send-email 2.39.2 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.059 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record T_SCC_BODY_TEXT_LINE -0.01 - Subject: [pve-devel] [PATCH] mgr/dashboard: add backport that allows the Dashboard to be used again X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Tue, 02 Jan 2024 17:37:12 -0000 After upgrading from PVE 7 to PVE 8, some users noted that the Ceph Dashboard does not work anymore. [0] A user from our community provided a pull request [1] which removes a dependency to `PyJWT` (Python). This commit adds a backport of this PR as a single patch. As stated in the patch itself, this allows the Dashboard to be used again, as long as TLS for the dashboard is deactivated. This can be done via `ceph config set mgr mgr/dashboard/ssl false`. As a workaround, users may e.g. use a reverse proxy that does TLS termination for the dashboard. [0]: https://forum.proxmox.com/threads/ceph-warning-post-upgrade-to-v8.129371/ [1]: https://github.com/ceph/ceph/pull/54710 Signed-off-by: Max Carrara --- ...hboard-Simplify-authentication-proto.patch | 276 ++++++++++++++++++ patches/series | 1 + 2 files changed, 277 insertions(+) create mode 100644 patches/0012-backport-mgr-dashboard-Simplify-authentication-proto.patch diff --git a/patches/0012-backport-mgr-dashboard-Simplify-authentication-proto.patch b/patches/0012-backport-mgr-dashboard-Simplify-authentication-proto.patch new file mode 100644 index 000000000..387211200 --- /dev/null +++ b/patches/0012-backport-mgr-dashboard-Simplify-authentication-proto.patch @@ -0,0 +1,276 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Max Carrara +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. + +Fixes: https://forum.proxmox.com/threads/ceph-warning-post-upgrade-to-v8.129371 +Signed-off-by: Daniel Persson +Signed-off-by: Max Carrara +--- + 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 f0dd8e8a941..6fb61aed8d2 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 0c1929948af..7ef20a9958e 100644 +--- a/debian/control ++++ b/debian/control +@@ -92,7 +92,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/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 ++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 8ae897cebd8..20d4be39f59 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 + diff --git a/patches/series b/patches/series index 8804134cc..918c25024 100644 --- a/patches/series +++ b/patches/series @@ -9,3 +9,4 @@ 0009-fix-4759-run-ceph-crash-daemon-with-www-data-group-f.patch 0010-debian-add-missing-bcrypt-to-manager-.requires.patch 0011-fix-compatibility-with-CPUs-not-supporting-SSE-4.1-i.patch +0012-backport-mgr-dashboard-Simplify-authentication-proto.patch -- 2.39.2