Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions ceph.spec.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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}
Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down
1 change: 0 additions & 1 deletion src/pybind/mgr/dashboard/constraints.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
CherryPy~=13.1
more-itertools~=8.14
PyJWT~=2.0
bcrypt~=3.1
python3-saml~=1.4
requests~=2.26
Expand Down
12 changes: 12 additions & 0 deletions src/pybind/mgr/dashboard/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,15 @@ class GrafanaError(Exception):

class PasswordPolicyException(Exception):
pass


class ExpiredSignatureError(Exception):
pass


class InvalidTokenError(Exception):
pass


class InvalidAlgorithmError(Exception):
pass
1 change: 1 addition & 0 deletions src/pybind/mgr/dashboard/requirements-lint.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ autopep8==1.5.7
pyfakefs==4.5.0
isort==5.5.3
jsonschema~=4.0
PyJWT~=2.0
1 change: 1 addition & 0 deletions src/pybind/mgr/dashboard/requirements-test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pytest-cov
pytest-instafail
pyfakefs==4.5.0
jsonschema~=4.0
PyJWT~=2.0
1 change: 0 additions & 1 deletion src/pybind/mgr/dashboard/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
bcrypt
CherryPy
more-itertools
PyJWT
pyopenssl
requests
Routes
Expand Down
70 changes: 61 additions & 9 deletions src/pybind/mgr/dashboard/services/auth.py
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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):
Expand All @@ -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:
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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']
Expand Down