]> git.proxmox.com Git - ceph.git/blame - patches/0012-backport-mgr-dashboard-simplify-authentication-proto.patch
mgr/dashboard: add backport that allows the dashboard to work again
[ceph.git] / patches / 0012-backport-mgr-dashboard-simplify-authentication-proto.patch
CommitLineData
f35168f6
MC
1From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
2From: Max Carrara <m.carrara@proxmox.com>
3Date: Tue, 2 Jan 2024 13:02:51 +0000
4Subject: [PATCH] backport: mgr/dashboard: simplify authentication protocol
5
6This is a backport of https://github.com/ceph/ceph/pull/54710 which
7fixes the Ceph Dashboard not being able to launch on Ceph Reef running
8on Debian Bookworm.
9
10This is achieved by removing the dependency on `PyJWT` (Python) and thus
11transitively also removing the dependency on `cryptography` (Python).
12For more information, see the original pull request.
13
14Note that the Ceph Dashboard still cannot be used if TLS is activated,
15because `pyOpenSSL` is used to verify certs during launch. Disabling
16TLS via `ceph config set mgr mgr/dashboard/ssl false` and using e.g.
17a reverse proxy can be used as a workaround.
18
19A separate patch is required to allow the dashboard to run with TLS
20enabled.
21
22Fixes: https://forum.proxmox.com/threads/ceph-warning-post-upgrade-to-v8.129371
23Signed-off-by: Daniel Persson <mailto.woden@gmail.com>
24Signed-off-by: Max Carrara <m.carrara@proxmox.com>
25---
26 ceph.spec.in | 4 --
27 debian/control | 1 -
28 src/pybind/mgr/dashboard/constraints.txt | 1 -
29 src/pybind/mgr/dashboard/exceptions.py | 12 ++++
30 .../mgr/dashboard/requirements-lint.txt | 1 +
31 .../mgr/dashboard/requirements-test.txt | 1 +
32 src/pybind/mgr/dashboard/requirements.txt | 1 -
33 src/pybind/mgr/dashboard/services/auth.py | 70 ++++++++++++++++---
34 8 files changed, 75 insertions(+), 16 deletions(-)
35
36diff --git a/ceph.spec.in b/ceph.spec.in
37index f0dd8e8a941..6fb61aed8d2 100644
38--- a/ceph.spec.in
39+++ b/ceph.spec.in
40@@ -412,7 +412,6 @@ BuildRequires: xmlsec1-nss
41 BuildRequires: xmlsec1-openssl
42 BuildRequires: xmlsec1-openssl-devel
43 BuildRequires: python%{python3_pkgversion}-cherrypy
44-BuildRequires: python%{python3_pkgversion}-jwt
45 BuildRequires: python%{python3_pkgversion}-routes
46 BuildRequires: python%{python3_pkgversion}-scipy
47 BuildRequires: python%{python3_pkgversion}-werkzeug
48@@ -425,7 +424,6 @@ BuildRequires: libxmlsec1-1
49 BuildRequires: libxmlsec1-nss1
50 BuildRequires: libxmlsec1-openssl1
51 BuildRequires: python%{python3_pkgversion}-CherryPy
52-BuildRequires: python%{python3_pkgversion}-PyJWT
53 BuildRequires: python%{python3_pkgversion}-Routes
54 BuildRequires: python%{python3_pkgversion}-Werkzeug
55 BuildRequires: python%{python3_pkgversion}-numpy-devel
56@@ -617,7 +615,6 @@ Requires: ceph-prometheus-alerts = %{_epoch_prefix}%{version}-%{release}
57 Requires: python%{python3_pkgversion}-setuptools
58 %if 0%{?fedora} || 0%{?rhel}
59 Requires: python%{python3_pkgversion}-cherrypy
60-Requires: python%{python3_pkgversion}-jwt
61 Requires: python%{python3_pkgversion}-routes
62 Requires: python%{python3_pkgversion}-werkzeug
63 %if 0%{?weak_deps}
64@@ -626,7 +623,6 @@ Recommends: python%{python3_pkgversion}-saml
65 %endif
66 %if 0%{?suse_version}
67 Requires: python%{python3_pkgversion}-CherryPy
68-Requires: python%{python3_pkgversion}-PyJWT
69 Requires: python%{python3_pkgversion}-Routes
70 Requires: python%{python3_pkgversion}-Werkzeug
71 Recommends: python%{python3_pkgversion}-python3-saml
72diff --git a/debian/control b/debian/control
73index 32e7bb45ce4..289b28877a8 100644
74--- a/debian/control
75+++ b/debian/control
76@@ -91,7 +91,6 @@ Build-Depends: automake,
77 python3-all-dev,
78 python3-cherrypy3,
79 python3-natsort,
80- python3-jwt <pkg.ceph.check>,
81 python3-pecan <pkg.ceph.check>,
82 python3-bcrypt <pkg.ceph.check>,
83 tox <pkg.ceph.check>,
84diff --git a/src/pybind/mgr/dashboard/constraints.txt b/src/pybind/mgr/dashboard/constraints.txt
85index 55f81c92dec..fd614104880 100644
86--- a/src/pybind/mgr/dashboard/constraints.txt
87+++ b/src/pybind/mgr/dashboard/constraints.txt
88@@ -1,6 +1,5 @@
89 CherryPy~=13.1
90 more-itertools~=8.14
91-PyJWT~=2.0
92 bcrypt~=3.1
93 python3-saml~=1.4
94 requests~=2.26
95diff --git a/src/pybind/mgr/dashboard/exceptions.py b/src/pybind/mgr/dashboard/exceptions.py
96index 96cbc523356..d396a38d2c3 100644
97--- a/src/pybind/mgr/dashboard/exceptions.py
98+++ b/src/pybind/mgr/dashboard/exceptions.py
99@@ -121,3 +121,15 @@ class GrafanaError(Exception):
100
101 class PasswordPolicyException(Exception):
102 pass
103+
104+
105+class ExpiredSignatureError(Exception):
106+ pass
107+
108+
109+class InvalidTokenError(Exception):
110+ pass
111+
112+
113+class InvalidAlgorithmError(Exception):
114+ pass
115diff --git a/src/pybind/mgr/dashboard/requirements-lint.txt b/src/pybind/mgr/dashboard/requirements-lint.txt
116index d82fa1ace1d..5fe9957c32a 100644
117--- a/src/pybind/mgr/dashboard/requirements-lint.txt
118+++ b/src/pybind/mgr/dashboard/requirements-lint.txt
119@@ -9,3 +9,4 @@ autopep8==1.5.7
120 pyfakefs==4.5.0
121 isort==5.5.3
122 jsonschema==4.16.0
123+PyJWT~=2.0
124diff --git a/src/pybind/mgr/dashboard/requirements-test.txt b/src/pybind/mgr/dashboard/requirements-test.txt
125index d2566bab59f..5066c7a59b6 100644
126--- a/src/pybind/mgr/dashboard/requirements-test.txt
127+++ b/src/pybind/mgr/dashboard/requirements-test.txt
128@@ -2,3 +2,4 @@ pytest-cov
129 pytest-instafail
130 pyfakefs==4.5.0
131 jsonschema
132+PyJWT~=2.0
133diff --git a/src/pybind/mgr/dashboard/requirements.txt b/src/pybind/mgr/dashboard/requirements.txt
134index 8003d62a552..292971819c9 100644
135--- a/src/pybind/mgr/dashboard/requirements.txt
136+++ b/src/pybind/mgr/dashboard/requirements.txt
137@@ -1,7 +1,6 @@
138 bcrypt
139 CherryPy
140 more-itertools
141-PyJWT
142 pyopenssl
143 requests
144 Routes
145diff --git a/src/pybind/mgr/dashboard/services/auth.py b/src/pybind/mgr/dashboard/services/auth.py
146index f13963abffd..3c600231252 100644
147--- a/src/pybind/mgr/dashboard/services/auth.py
148+++ b/src/pybind/mgr/dashboard/services/auth.py
149@@ -1,17 +1,19 @@
150 # -*- coding: utf-8 -*-
151
152+import base64
153+import hashlib
154+import hmac
155 import json
156 import logging
157 import os
158 import threading
159 import time
160 import uuid
161-from base64 import b64encode
162
163 import cherrypy
164-import jwt
165
166 from .. import mgr
167+from ..exceptions import ExpiredSignatureError, InvalidAlgorithmError, InvalidTokenError
168 from .access_control import LocalAuthenticator, UserDoesNotExist
169
170 cherrypy.config.update({
171@@ -33,7 +35,7 @@ class JwtManager(object):
172 @staticmethod
173 def _gen_secret():
174 secret = os.urandom(16)
175- return b64encode(secret).decode('utf-8')
176+ return base64.b64encode(secret).decode('utf-8')
177
178 @classmethod
179 def init(cls):
180@@ -45,6 +47,54 @@ class JwtManager(object):
181 mgr.set_store('jwt_secret', secret)
182 cls._secret = secret
183
184+ @classmethod
185+ def array_to_base64_string(cls, message):
186+ jsonstr = json.dumps(message, sort_keys=True).replace(" ", "")
187+ string_bytes = base64.urlsafe_b64encode(bytes(jsonstr, 'UTF-8'))
188+ return string_bytes.decode('UTF-8').replace("=", "")
189+
190+ @classmethod
191+ def encode(cls, message, secret):
192+ header = {"alg": cls.JWT_ALGORITHM, "typ": "JWT"}
193+ base64_header = cls.array_to_base64_string(header)
194+ base64_message = cls.array_to_base64_string(message)
195+ base64_secret = base64.urlsafe_b64encode(hmac.new(
196+ bytes(secret, 'UTF-8'),
197+ msg=bytes(base64_header + "." + base64_message, 'UTF-8'),
198+ digestmod=hashlib.sha256
199+ ).digest()).decode('UTF-8').replace("=", "")
200+ return base64_header + "." + base64_message + "." + base64_secret
201+
202+ @classmethod
203+ def decode(cls, message, secret):
204+ split_message = message.split(".")
205+ base64_header = split_message[0]
206+ base64_message = split_message[1]
207+ base64_secret = split_message[2]
208+
209+ decoded_header = json.loads(base64.urlsafe_b64decode(base64_header))
210+
211+ if decoded_header['alg'] != cls.JWT_ALGORITHM:
212+ raise InvalidAlgorithmError()
213+
214+ incoming_secret = base64.urlsafe_b64encode(hmac.new(
215+ bytes(secret, 'UTF-8'),
216+ msg=bytes(base64_header + "." + base64_message, 'UTF-8'),
217+ digestmod=hashlib.sha256
218+ ).digest()).decode('UTF-8').replace("=", "")
219+
220+ if base64_secret != incoming_secret:
221+ raise InvalidTokenError()
222+
223+ # We add ==== as padding to ignore the requirement to have correct padding in
224+ # the urlsafe_b64decode method.
225+ decoded_message = json.loads(base64.urlsafe_b64decode(base64_message + "===="))
226+ now = int(time.time())
227+ if decoded_message['exp'] < now:
228+ raise ExpiredSignatureError()
229+
230+ return decoded_message
231+
232 @classmethod
233 def gen_token(cls, username):
234 if not cls._secret:
235@@ -59,13 +109,13 @@ class JwtManager(object):
236 'iat': now,
237 'username': username
238 }
239- return jwt.encode(payload, cls._secret, algorithm=cls.JWT_ALGORITHM) # type: ignore
240+ return cls.encode(payload, cls._secret) # type: ignore
241
242 @classmethod
243 def decode_token(cls, token):
244 if not cls._secret:
245 cls.init()
246- return jwt.decode(token, cls._secret, algorithms=cls.JWT_ALGORITHM) # type: ignore
247+ return cls.decode(token, cls._secret) # type: ignore
248
249 @classmethod
250 def get_token_from_header(cls):
251@@ -99,8 +149,8 @@ class JwtManager(object):
252 @classmethod
253 def get_user(cls, token):
254 try:
255- dtoken = JwtManager.decode_token(token)
256- if not JwtManager.is_blocklisted(dtoken['jti']):
257+ dtoken = cls.decode_token(token)
258+ if not cls.is_blocklisted(dtoken['jti']):
259 user = AuthManager.get_user(dtoken['username'])
260 if user.last_update <= dtoken['iat']:
261 return user
262@@ -110,10 +160,12 @@ class JwtManager(object):
263 )
264 else:
265 cls.logger.debug('Token is block-listed') # type: ignore
266- except jwt.ExpiredSignatureError:
267+ except ExpiredSignatureError:
268 cls.logger.debug("Token has expired") # type: ignore
269- except jwt.InvalidTokenError:
270+ except InvalidTokenError:
271 cls.logger.debug("Failed to decode token") # type: ignore
272+ except InvalidAlgorithmError:
273+ cls.logger.debug("Only the HS256 algorithm is supported.") # type: ignore
274 except UserDoesNotExist:
275 cls.logger.debug( # type: ignore
276 "Invalid token: user %s does not exist", dtoken['username']
277--
2782.39.2
279