all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH] mgr/dashboard: add backport that allows the Dashboard to be used again
@ 2024-01-02 17:37 Max Carrara
  2024-01-05 14:11 ` Max Carrara
  0 siblings, 1 reply; 2+ messages in thread
From: Max Carrara @ 2024-01-02 17:37 UTC (permalink / raw)
  To: pve-devel

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 <m.carrara@proxmox.com>
---
 ...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 <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.
+
+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 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 <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
++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





^ permalink raw reply	[flat|nested] 2+ messages in thread

* Re: [pve-devel] [PATCH] mgr/dashboard: add backport that allows the Dashboard to be used again
  2024-01-02 17:37 [pve-devel] [PATCH] mgr/dashboard: add backport that allows the Dashboard to be used again Max Carrara
@ 2024-01-05 14:11 ` Max Carrara
  0 siblings, 0 replies; 2+ messages in thread
From: Max Carrara @ 2024-01-05 14:11 UTC (permalink / raw)
  To: pve-devel

This patch is to be ignored; just sent in a new series:
https://lists.proxmox.com/pipermail/pve-devel/2024-January/061242.html




^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2024-01-05 14:11 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2024-01-02 17:37 [pve-devel] [PATCH] mgr/dashboard: add backport that allows the Dashboard to be used again Max Carrara
2024-01-05 14:11 ` Max Carrara

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal