ceph/patches/0012-backport-mgr-dashboard-simplify-authentication-proto.patch
Max Carrara f35168f671 mgr/dashboard: add backport that allows the dashboard to work again
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.

This patch by itself however does not yet allow the dashboard to run
with TLS enabled.

[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>
2024-01-15 16:48:12 +01:00

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 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 32e7bb45ce4..289b28877a8 100644
--- a/debian/control
+++ b/debian/control
@@ -91,7 +91,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 d2566bab59f..5066c7a59b6 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 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