Skip to content

Commit

Permalink
repo: backport mgr/dashboard fix for python-cryptography issues
Browse files Browse the repository at this point in the history
Removes the (limited) runtime usages of python-jwt, and therefore,
python-cryptography from the mgr dashboard module.

This should restore dashboard functionality, if used alongside a
patched pyo3 for python-bcrypt.

Upstream-Ref: ceph/ceph@33d8bef
References: ceph/ceph#55689
References: https://tracker.ceph.com/issues/63529
References: #20
References: https://git.st8l.com/luxolus/pyo3/commit/44d6919168621b46e4e884130ca43338655b020c
  • Loading branch information
bazaah committed Mar 17, 2024
1 parent f6cfb97 commit 495f8a7
Showing 1 changed file with 323 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
From fcbd8fdae9d80d9a9bd61838aeaf41b504a5888a Mon Sep 17 00:00:00 2001
From: Daniel Persson <mailto.woden@gmail.com>
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 <mailto.woden@gmail.com>
(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 <mailto.woden@gmail.com>
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 <mailto.woden@gmail.com>
(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 <pkg.ceph.check>,
python3-pecan <pkg.ceph.check>,
python3-bcrypt <pkg.ceph.check>,
tox <pkg.ceph.check>,
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
+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 <mailto.woden@gmail.com>
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 <mailto.woden@gmail.com>
(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.16.0
+jsonschema~=4.0
+PyJWT~=2.0

0 comments on commit 495f8a7

Please sign in to comment.