From 17d6a993fc17d533460c5f40f3908c708e057c18 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Thu, 23 May 2024 18:45:17 +0200 Subject: Merging upstream version 18.2.3. Signed-off-by: Daniel Baumann --- src/pybind/mgr/dashboard/services/auth.py | 70 +++++++++++++-- src/pybind/mgr/dashboard/services/rgw_client.py | 110 +++++++++++++++++++++++- 2 files changed, 170 insertions(+), 10 deletions(-) (limited to 'src/pybind/mgr/dashboard/services') diff --git a/src/pybind/mgr/dashboard/services/auth.py b/src/pybind/mgr/dashboard/services/auth.py index f13963abf..3c6002312 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'] diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py index 5120806d8..aed702603 100644 --- a/src/pybind/mgr/dashboard/services/rgw_client.py +++ b/src/pybind/mgr/dashboard/services/rgw_client.py @@ -658,6 +658,30 @@ class RgwClient(RestClient): http_status_code=error.status_code, component='rgw') + @RestClient.api_get('/{bucket_name}?acl') + def get_acl(self, bucket_name, request=None): + # pylint: disable=unused-argument + try: + result = request(raw_content=True) # type: ignore + return result.decode("utf-8") + except RequestException as error: + msg = 'Error getting ACLs' + if error.status_code == 404: + msg = '{}: {}'.format(msg, str(error)) + raise DashboardException(msg=msg, + http_status_code=error.status_code, + component='rgw') + + @RestClient.api_put('/{bucket_name}?acl') + def set_acl(self, bucket_name, acl, request=None): + # pylint: disable=unused-argument + headers = {'x-amz-acl': acl} + try: + result = request(headers=headers) # type: ignore + except RequestException as e: + raise DashboardException(msg=str(e), component='rgw') + return result + @RestClient.api_get('/{bucket_name}?encryption') def get_bucket_encryption(self, bucket_name, request=None): # pylint: disable=unused-argument @@ -702,6 +726,19 @@ class RgwClient(RestClient): except RequestException as e: raise DashboardException(msg=str(e), component='rgw') + @RestClient.api_put('/{bucket_name}?tagging') + def set_tags(self, bucket_name, tags, request=None): + # pylint: disable=unused-argument + try: + ET.fromstring(tags) + except ET.ParseError: + return "Data must be properly formatted" + try: + result = request(data=tags) # type: ignore + except RequestException as e: + raise DashboardException(msg=str(e), component='rgw') + return result + @RestClient.api_get('/{bucket_name}?object-lock') def get_bucket_locking(self, bucket_name, request=None): # type: (str, Optional[object]) -> dict @@ -806,6 +843,9 @@ class RgwClient(RestClient): logger.warning('Error listing roles with code %d: %s', code, err) return [] + for role in roles: + if 'PermissionPolicies' not in role: + role['PermissionPolicies'] = [] return roles def create_role(self, role_name: str, role_path: str, role_assume_policy_doc: str) -> None: @@ -852,6 +892,74 @@ class RgwClient(RestClient): f' For more information about the format look at {link}') raise DashboardException(msg=msg, component='rgw') + def get_role(self, role_name: str): + rgw_get_role_command = ['role', 'get', '--role-name', role_name] + code, role, _err = mgr.send_rgwadmin_command(rgw_get_role_command) + if code != 0: + raise DashboardException(msg=f'Error getting role with code {code}: {_err}', + component='rgw') + return role + + def update_role(self, role_name: str, max_session_duration: str): + rgw_update_role_command = ['role', 'update', '--role-name', + role_name, '--max_session_duration', max_session_duration] + code, _, _err = mgr.send_rgwadmin_command(rgw_update_role_command, + stdout_as_json=False) + if code != 0: + raise DashboardException(msg=f'Error updating role with code {code}: {_err}', + component='rgw') + + def delete_role(self, role_name: str) -> None: + rgw_delete_role_command = ['role', 'delete', '--role-name', role_name] + code, _, _err = mgr.send_rgwadmin_command(rgw_delete_role_command, + stdout_as_json=False) + if code != 0: + raise DashboardException(msg=f'Error deleting role with code {code}: {_err}', + component='rgw') + + @RestClient.api_get('/{bucket_name}?policy') + def get_bucket_policy(self, bucket_name: str, request=None): + """ + Gets the bucket policy for a bucket. + :param bucket_name: The name of the bucket. + :type bucket_name: str + :rtype: None + """ + # pylint: disable=unused-argument + + try: + request = request() + return request + except RequestException as e: + if e.content: + content = json_str_to_object(e.content) + if content.get( + 'Code') == 'NoSuchBucketPolicy': + return None + raise e + + @RestClient.api_put('/{bucket_name}?policy') + def set_bucket_policy(self, bucket_name: str, policy: str, request=None): + """ + Sets the bucket policy for a bucket. + :param bucket_name: The name of the bucket. + :type bucket_name: str + :param policy: The bucket policy. + :type policy: JSON Structured Document + :return: The bucket policy. + :rtype: Dict + """ + # pylint: disable=unused-argument + try: + request = request(data=policy) + except RequestException as e: + if e.content: + content = json_str_to_object(e.content) + if content.get("Code") == "InvalidArgument": + msg = "Invalid JSON document" + raise DashboardException(msg=msg, component='rgw') + raise DashboardException(e) + def perform_validations(self, retention_period_days, retention_period_years, mode): try: retention_period_days = int(retention_period_days) if retention_period_days else 0 @@ -956,7 +1064,7 @@ class RgwMultisite: def create_realm(self, realm_name: str, default: bool): rgw_realm_create_cmd = ['realm', 'create'] cmd_create_realm_options = ['--rgw-realm', realm_name] - if default != 'false': + if default: cmd_create_realm_options.append('--default') rgw_realm_create_cmd += cmd_create_realm_options try: -- cgit v1.2.3