diff --git a/kdb-bot/src/bot/main.py b/kdb-bot/src/bot/main.py index d2bbdb5a..9747634b 100644 --- a/kdb-bot/src/bot/main.py +++ b/kdb-bot/src/bot/main.py @@ -11,6 +11,7 @@ from bot.startup_discord_extension import StartupDiscordExtension from bot.startup_migration_extension import StartupMigrationExtension from bot.startup_module_extension import StartupModuleExtension from bot.startup_settings_extension import StartupSettingsExtension +from bot_api.app_api_extension import AppApiExtension from modules.boot_log.boot_log_extension import BootLogExtension from modules.database.database_extension import DatabaseExtension @@ -29,6 +30,7 @@ class Program: .use_extension(StartupMigrationExtension) \ .use_extension(BootLogExtension) \ .use_extension(DatabaseExtension) \ + .use_extension(AppApiExtension) \ .use_startup(Startup) self.app: Application = await app_builder.build_async() await self.app.run_async() diff --git a/kdb-bot/src/bot_api/abc/auth_service_abc.py b/kdb-bot/src/bot_api/abc/auth_service_abc.py index 28d13749..3a006a0d 100644 --- a/kdb-bot/src/bot_api/abc/auth_service_abc.py +++ b/kdb-bot/src/bot_api/abc/auth_service_abc.py @@ -9,6 +9,7 @@ from bot_api.model.email_string_dto import EMailStringDTO from bot_api.model.reset_password_dto import ResetPasswordDTO from bot_api.model.token_dto import TokenDTO from bot_api.model.update_auth_user_dto import UpdateAuthUserDTO +from bot_data.model.auth_user import AuthUser class AuthServiceABC(ABC): @@ -16,6 +17,12 @@ class AuthServiceABC(ABC): @abstractmethod def __init__(self): pass + @abstractmethod + def generate_token(self, user: AuthUser) -> str: pass + + @abstractmethod + def decode_token(self, token: str) -> dict: pass + @abstractmethod async def get_all_auth_users_async(self) -> List[AuthUserDTO]: pass @@ -43,6 +50,9 @@ class AuthServiceABC(ABC): @abstractmethod async def delete_auth_user_async(self, user_dto: AuthUserDTO): pass + @abstractmethod + async def verify_login(self, token_str: str) -> bool: pass + @abstractmethod async def login_async(self, user_dto: AuthUserDTO) -> TokenDTO: pass diff --git a/kdb-bot/src/bot_api/app_api_extension.py b/kdb-bot/src/bot_api/app_api_extension.py new file mode 100644 index 00000000..c0b5fd12 --- /dev/null +++ b/kdb-bot/src/bot_api/app_api_extension.py @@ -0,0 +1,26 @@ +from cpl_core.application import ApplicationExtensionABC +from cpl_core.configuration import ConfigurationABC +from cpl_core.dependency_injection import ServiceProviderABC + +from bot_api.abc.auth_service_abc import AuthServiceABC +from bot_api.configuration.authentication_settings import AuthenticationSettings +from bot_api.route.route import Route +from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum +from bot_core.configuration.feature_flags_settings import FeatureFlagsSettings +from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC + + +class AppApiExtension(ApplicationExtensionABC): + + def __init__(self): + ApplicationExtensionABC.__init__(self) + + async def run(self, config: ConfigurationABC, services: ServiceProviderABC): + feature_flags: FeatureFlagsSettings = config.get_configuration(FeatureFlagsSettings) + if not feature_flags.get_flag(FeatureFlagsEnum.api_module): + return + + auth_settings: AuthenticationSettings = config.get_configuration(AuthenticationSettings) + auth_users: AuthUserRepositoryABC = services.get_service(AuthUserRepositoryABC) + auth: AuthServiceABC = services.get_service(AuthServiceABC) + Route.init_authorize(auth_users, auth) diff --git a/kdb-bot/src/bot_api/controller/auth_controller.py b/kdb-bot/src/bot_api/controller/auth_controller.py index c5e10d1c..ee64bf65 100644 --- a/kdb-bot/src/bot_api/controller/auth_controller.py +++ b/kdb-bot/src/bot_api/controller/auth_controller.py @@ -39,11 +39,13 @@ class AuthController: self._auth_service = auth_service @Route.get(f'{BasePath}/users') + @Route.authorize async def get_all_users(self) -> Response: result = await self._auth_service.get_all_auth_users_async() return jsonify(result.select(lambda x: x.to_dict())) @Route.post(f'{BasePath}/users/get/filtered') + @Route.authorize async def get_filtered_users(self) -> Response: dto: AuthUserSelectCriteria = JSONProcessor.process(AuthUserSelectCriteria, request.get_json(force=True, silent=True)) result = await self._auth_service.get_filtered_auth_users_async(dto) @@ -51,11 +53,13 @@ class AuthController: return jsonify(result.to_dict()) @Route.get(f'{BasePath}/users/get/') + @Route.authorize async def get_user_from_email(self, email: str) -> Response: result = await self._auth_service.get_auth_user_by_email_async(email) return jsonify(result.to_dict()) @Route.get(f'{BasePath}/users/find/') + @Route.authorize async def find_user_from_email(self, email: str) -> Response: result = await self._auth_service.find_auth_user_by_email_async(email) return jsonify(result.to_dict()) @@ -83,36 +87,42 @@ class AuthController: return '', 200 @Route.post(f'{BasePath}/update-user') + @Route.authorize async def update_user(self): dto: UpdateAuthUserDTO = JSONProcessor.process(UpdateAuthUserDTO, request.get_json(force=True, silent=True)) await self._auth_service.update_user_async(dto) return '', 200 @Route.post(f'{BasePath}/update-user-as-admin') + @Route.authorize async def update_user_as_admin(self): dto: UpdateAuthUserDTO = JSONProcessor.process(UpdateAuthUserDTO, request.get_json(force=True, silent=True)) await self._auth_service.update_user_async(dto) return '', 200 @Route.post(f'{BasePath}/refresh') + @Route.authorize async def refresh(self) -> Response: dto: TokenDTO = JSONProcessor.process(TokenDTO, request.get_json(force=True, silent=True)) result = await self._auth_service.refresh_async(dto) return jsonify(result.to_dict()) @Route.post(f'{BasePath}/revoke') + @Route.authorize async def revoke(self): dto: TokenDTO = JSONProcessor.process(TokenDTO, request.get_json(force=True, silent=True)) await self._auth_service.revoke_async(dto) return '', 200 @Route.post(f'{BasePath}/delete-user') + @Route.authorize async def delete_user(self): dto: AuthUserDTO = JSONProcessor.process(AuthUserDTO, request.get_json(force=True, silent=True)) await self._auth_service.delete_auth_user_async(dto) return '', 200 @Route.post(f'{BasePath}/delete-user-by-mail/') + @Route.authorize async def delete_user_by_mail(self, email: str): await self._auth_service.delete_auth_user_by_email_async(email) return '', 200 diff --git a/kdb-bot/src/bot_api/controller/gui_controller.py b/kdb-bot/src/bot_api/controller/gui_controller.py index a2fa883a..dccc2099 100644 --- a/kdb-bot/src/bot_api/controller/gui_controller.py +++ b/kdb-bot/src/bot_api/controller/gui_controller.py @@ -40,6 +40,7 @@ class GuiController: return VersionDTO(version.major, version.minor, version.micro).to_dict() @Route.get(f'{BasePath}/settings') + @Route.authorize async def settings(self): # TODO: Authentication import bot_api @@ -61,6 +62,7 @@ class GuiController: ).to_dict() @Route.get(f'{BasePath}/send-test-mail/') + @Route.authorize async def send_test_mail(self, email: str): # TODO: Authentication mail = EMail() diff --git a/kdb-bot/src/bot_api/route/route.py b/kdb-bot/src/bot_api/route/route.py index 74fbbc67..429b24a3 100644 --- a/kdb-bot/src/bot_api/route/route.py +++ b/kdb-bot/src/bot_api/route/route.py @@ -1,9 +1,48 @@ +from functools import wraps +from typing import Optional + +from flask import request from flask_cors import cross_origin +from bot_api.abc.auth_service_abc import AuthServiceABC +from bot_api.exception.service_error_code_enum import ServiceErrorCode +from bot_api.exception.service_exception import ServiceException +from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC + class Route: registered_routes = {} + _auth_users: Optional[AuthUserRepositoryABC] = None + _auth: Optional[AuthServiceABC] = None + + @classmethod + def init_authorize(cls, auth_users: AuthUserRepositoryABC, auth: AuthServiceABC): + cls._auth_users = auth_users + cls._auth = auth + + @classmethod + def authorize(cls, f): + @wraps(f) + async def decorator(*args, **kwargs): + token = None + if 'Authorization' in request.headers: + bearer = request.headers.get('Authorization') + token = bearer.split()[1] + + if not token: + raise ServiceException(ServiceErrorCode.InvalidData, f'Token not set') + + if cls._auth_users is None or cls._auth is None: + raise ServiceException(ServiceErrorCode.InvalidDependencies, f'Authorize is not initialized') + + if not cls._auth.verify_login(token): + raise ServiceException(ServiceErrorCode.InvalidUser, f'Token expired') + + return await f(*args, **kwargs) + + return decorator + @classmethod def route(cls, path=None, **kwargs): # simple decorator for class based views diff --git a/kdb-bot/src/bot_api/service/auth_service.py b/kdb-bot/src/bot_api/service/auth_service.py index ede6f15f..0a78a9c4 100644 --- a/kdb-bot/src/bot_api/service/auth_service.py +++ b/kdb-bot/src/bot_api/service/auth_service.py @@ -70,7 +70,7 @@ class AuthService(AuthServiceABC): return False - def _generate_token(self, user: AuthUser) -> str: + def generate_token(self, user: AuthUser) -> str: token = jwt.encode( payload={ 'user_id': user.id, @@ -85,7 +85,7 @@ class AuthService(AuthServiceABC): return token - def _decode_token(self, token: str) -> dict: + def decode_token(self, token: str) -> dict: return jwt.decode( token, key=self._auth_settings.secret_key, @@ -292,6 +292,21 @@ class AuthService(AuthServiceABC): self._logger.error(__name__, f'Cannot delete user', e) raise ServiceException(ServiceErrorCode.UnableToDelete, f'Cannot delete user by mail {user_dto.email}') + def verify_login(self, token_str: str) -> bool: + try: + token = self.decode_token(token_str) + if token is None or 'email' not in token: + raise ServiceException(ServiceErrorCode.InvalidData, 'Token invalid') + + user = self._auth_users.find_auth_user_by_email(token['email']) + if user is None: + raise ServiceException(ServiceErrorCode.InvalidData, 'Token expired') + except Exception as e: + self._logger.error(__name__, f'Refreshing token failed', e) + return False + + return True + async def login_async(self, user_dto: AuthUser) -> TokenDTO: if user_dto is None: raise ServiceException(ServiceErrorCode.InvalidData, 'User not set') @@ -304,7 +319,7 @@ class AuthService(AuthServiceABC): if db_user.password != user_dto.password: raise ServiceException(ServiceErrorCode.InvalidUser, 'Wrong password') - token = self._generate_token(db_user) + token = self.generate_token(db_user) refresh_token = self._create_and_save_refresh_token(db_user) if db_user.forgot_password_id is not None: db_user.forgot_password_id = None @@ -316,16 +331,16 @@ class AuthService(AuthServiceABC): if token_dto is None: raise ServiceException(ServiceErrorCode.InvalidData, f'Token not set') - token = self._decode_token(token_dto.token) - if token is None or 'email' not in token: - raise ServiceException(ServiceErrorCode.InvalidData, 'Token invalid') - try: + token = self.decode_token(token_dto.token) + if token is None or 'email' not in token: + raise ServiceException(ServiceErrorCode.InvalidData, 'Token invalid') + user = self._auth_users.get_auth_user_by_email(token['email']) if user is None or user.refresh_token != token_dto.refresh_token or user.refresh_token_expire_time <= datetime.now(): raise ServiceException(ServiceErrorCode.InvalidData, 'Token expired') - return TokenDTO(self._generate_token(user), self._create_and_save_refresh_token(user)) + return TokenDTO(self.generate_token(user), self._create_and_save_refresh_token(user)) except Exception as e: self._logger.error(__name__, f'Refreshing token failed', e) return TokenDTO('', '') @@ -334,8 +349,9 @@ class AuthService(AuthServiceABC): if token_dto is None or token_dto.token is None or token_dto.refresh_token is None: raise ServiceException(ServiceErrorCode.InvalidData, 'Token not set') - token = self._decode_token(token_dto.token) try: + token = self.decode_token(token_dto.token) + user = self._auth_users.get_auth_user_by_email(token['email']) if user is None or user.refresh_token != token_dto.refresh_token or user.refresh_token_expire_time <= datetime.now(): raise ServiceException(ServiceErrorCode.InvalidData, 'Token expired')