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 34c1b5f2..7d526078 100644 --- a/kdb-bot/src/bot_api/abc/auth_service_abc.py +++ b/kdb-bot/src/bot_api/abc/auth_service_abc.py @@ -83,6 +83,10 @@ class AuthServiceABC(ABC): async def verify_login(self, token_str: str) -> bool: pass + @abstractmethod + async def verify_api_key(self, api_key: str) -> bool: + pass + @abstractmethod async def login_async(self, user_dto: AuthUserDTO) -> TokenDTO: pass diff --git a/kdb-bot/src/bot_api/api_module.py b/kdb-bot/src/bot_api/api_module.py index 8c3d6b3b..8f40f95e 100644 --- a/kdb-bot/src/bot_api/api_module.py +++ b/kdb-bot/src/bot_api/api_module.py @@ -13,7 +13,7 @@ from bot_api.api import Api from bot_api.api_thread import ApiThread from bot_api.controller.auth_controller import AuthController from bot_api.controller.auth_discord_controller import AuthDiscordController -from bot_api.controller.grahpql_controller import GraphQLController +from bot_api.controller.graphql_controller import GraphQLController from bot_api.controller.gui_controller import GuiController from bot_api.event.bot_api_on_ready_event import BotApiOnReadyEvent from bot_api.service.auth_service import AuthService diff --git a/kdb-bot/src/bot_api/controller/grahpql_controller.py b/kdb-bot/src/bot_api/controller/graphql_controller.py similarity index 97% rename from kdb-bot/src/bot_api/controller/grahpql_controller.py rename to kdb-bot/src/bot_api/controller/graphql_controller.py index 1cbc9877..d9072029 100644 --- a/kdb-bot/src/bot_api/controller/grahpql_controller.py +++ b/kdb-bot/src/bot_api/controller/graphql_controller.py @@ -33,7 +33,7 @@ class GraphQLController: return PLAYGROUND_HTML, 200 @Route.post(f"{BasePath}") - @Route.authorize + @Route.authorize(by_api_key=True) async def graphql(self): data = request.get_json() diff --git a/kdb-bot/src/bot_api/route/route.py b/kdb-bot/src/bot_api/route/route.py index f437fdb6..39a6044d 100644 --- a/kdb-bot/src/bot_api/route/route.py +++ b/kdb-bot/src/bot_api/route/route.py @@ -30,15 +30,32 @@ class Route: cls._env = env.environment_name @classmethod - def authorize(cls, f: Callable = None, role: AuthRoleEnum = None, skip_in_dev=False): + def authorize(cls, f: Callable = None, role: AuthRoleEnum = None, skip_in_dev=False, by_api_key=False): if f is None: - return functools.partial(cls.authorize, role=role, skip_in_dev=skip_in_dev) + return functools.partial(cls.authorize, role=role, skip_in_dev=skip_in_dev, by_api_key=by_api_key) @wraps(f) async def decorator(*args, **kwargs): if skip_in_dev and cls._env == "development": return await f(*args, **kwargs) + if "Authorization" not in request.headers and by_api_key and "API-Key" in request.headers: + valid = False + try: + valid = cls._auth.verify_api_key(request.headers["API-Key"]) + except ServiceException as e: + error = ErrorDTO(e.error_code, e.message) + return jsonify(error.to_dict()), 403 + except Exception as e: + return jsonify(e), 500 + + if not valid: + ex = ServiceException(ServiceErrorCode.Unauthorized, f"API-Key invalid") + error = ErrorDTO(ex.error_code, ex.message) + return jsonify(error.to_dict()), 401 + + return await f(*args, **kwargs) + token = None if "Authorization" in request.headers: bearer = request.headers.get("Authorization") diff --git a/kdb-bot/src/bot_api/service/auth_service.py b/kdb-bot/src/bot_api/service/auth_service.py index 1ba01206..7193b434 100644 --- a/kdb-bot/src/bot_api/service/auth_service.py +++ b/kdb-bot/src/bot_api/service/auth_service.py @@ -31,9 +31,11 @@ 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_api.transformer.auth_user_transformer import AuthUserTransformer as AUT +from bot_data.abc.api_key_repository_abc import ApiKeyRepositoryABC from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC from bot_data.abc.server_repository_abc import ServerRepositoryABC from bot_data.abc.user_repository_abc import UserRepositoryABC +from bot_data.model.api_key import ApiKey from bot_data.model.auth_role_enum import AuthRoleEnum from bot_data.model.auth_user import AuthUser from bot_data.model.auth_user_users_relation import AuthUserUsersRelation @@ -49,9 +51,9 @@ class AuthService(AuthServiceABC): bot: DiscordBotServiceABC, db: DatabaseContextABC, auth_users: AuthUserRepositoryABC, + api_keys: ApiKeyRepositoryABC, users: UserRepositoryABC, servers: ServerRepositoryABC, - # mailer: MailThread, mailer: EMailClientABC, t: TranslatePipe, auth_settings: AuthenticationSettings, @@ -64,6 +66,7 @@ class AuthService(AuthServiceABC): self._bot = bot self._db = db self._auth_users = auth_users + self._api_keys = api_keys self._users = users self._servers = servers self._mailer = mailer @@ -82,6 +85,11 @@ class AuthService(AuthServiceABC): return False + def _get_api_key_str(self, api_key: ApiKey) -> str: + return hashlib.sha256( + f"{api_key.identifier}:{api_key.key}+{self._auth_settings.secret_key}".encode("utf-8") + ).hexdigest() + def generate_token(self, user: AuthUser) -> str: token = jwt.encode( payload={ @@ -221,7 +229,12 @@ class AuthService(AuthServiceABC): raise ServiceException(ServiceErrorCode.InvalidUser, "User already exists") user = AUT.to_db(user_dto) - if self._auth_users.get_all_auth_users().count() == 0: + if ( + self._auth_users.get_all_auth_users() + .where(lambda x: x.name != "internal" and x.email != "internal@localhost") + .count() + == 0 + ): user.auth_role = AuthRoleEnum.admin user.password_salt = uuid.uuid4() @@ -478,6 +491,18 @@ class AuthService(AuthServiceABC): return True + def verify_api_key(self, api_key: str) -> bool: + try: + keys = self._api_keys.get_api_keys().select(self._get_api_key_str) + + if not keys.contains(api_key): + raise ServiceException(ServiceErrorCode.InvalidData, "API-Key invalid") + except Exception as e: + self._logger.error(__name__, f"Token invalid", 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") diff --git a/kdb-bot/src/bot_data/migration/api_key_migration.py b/kdb-bot/src/bot_data/migration/api_key_migration.py index df4c603a..76004f13 100644 --- a/kdb-bot/src/bot_data/migration/api_key_migration.py +++ b/kdb-bot/src/bot_data/migration/api_key_migration.py @@ -22,7 +22,7 @@ class ApiKeyMigration(MigrationABC): `Id` BIGINT NOT NULL AUTO_INCREMENT, `Identifier` VARCHAR(255) NOT NULL, `Key` VARCHAR(255) NOT NULL, - `CreatorId` BIGINT, + `CreatorId` BIGINT NULL, `CreatedAt` DATETIME(6), `LastModifiedAt` DATETIME(6), PRIMARY KEY(`Id`), diff --git a/kdb-bot/src/bot_data/model/api_key.py b/kdb-bot/src/bot_data/model/api_key.py index e0e0de9d..d27037cb 100644 --- a/kdb-bot/src/bot_data/model/api_key.py +++ b/kdb-bot/src/bot_data/model/api_key.py @@ -1,4 +1,5 @@ from datetime import datetime +from typing import Optional from cpl_core.database import TableABC @@ -10,7 +11,7 @@ class ApiKey(TableABC): self, identifier: str, key: str, - creator: User, + creator: Optional[User], created_at: datetime = None, modified_at: datetime = None, id=0, @@ -33,7 +34,7 @@ class ApiKey(TableABC): return self._key @property - def creator(self) -> User: + def creator(self) -> Optional[User]: return self._creator @staticmethod @@ -72,7 +73,7 @@ class ApiKey(TableABC): ) VALUES ( '{self._identifier}', '{self._key}', - '{self._creator.user_id}', + {"NULL" if self._creator is None else self._creator.user_id}, '{self._created_at}', '{self._modified_at}' ); diff --git a/kdb-bot/src/bot_data/service/api_key_repository_service.py b/kdb-bot/src/bot_data/service/api_key_repository_service.py index 1825dae8..e7919373 100644 --- a/kdb-bot/src/bot_data/service/api_key_repository_service.py +++ b/kdb-bot/src/bot_data/service/api_key_repository_service.py @@ -31,10 +31,11 @@ class ApiKeyRepositoryService(ApiKeyRepositoryABC): return value def _api_key_from_result(self, sql_result: tuple) -> ApiKey: + creator = self._get_value_from_result(sql_result[3]) api_key = ApiKey( self._get_value_from_result(sql_result[1]), self._get_value_from_result(sql_result[2]), - self._users.get_user_by_id(int(self._get_value_from_result(sql_result[3]))), + None if creator is None else self._users.get_user_by_id(int(creator)), self._get_value_from_result(sql_result[4]), self._get_value_from_result(sql_result[5]), id=self._get_value_from_result(sql_result[0]), diff --git a/kdb-bot/src/bot_data/service/seeder_service.py b/kdb-bot/src/bot_data/service/seeder_service.py index 04e999cc..feada479 100644 --- a/kdb-bot/src/bot_data/service/seeder_service.py +++ b/kdb-bot/src/bot_data/service/seeder_service.py @@ -1,6 +1,5 @@ from cpl_core.database.context import DatabaseContextABC from cpl_core.dependency_injection import ServiceProviderABC -from cpl_query.extension import List from bot_core.logging.database_logger import DatabaseLogger from bot_data.abc.data_seeder_abc import DataSeederABC @@ -18,12 +17,10 @@ class SeederService: self._db = db - self._seeder = List(type, DataSeederABC.__subclasses__()) - async def seed(self): self._logger.info(__name__, f"Seed data") - for seeder in self._seeder: - seeder_as_service: DataSeederABC = self._services.get_service(seeder) - self._logger.debug(__name__, f"Starting seeder {seeder.__name__}") - await seeder_as_service.seed() + for seeder in self._services.get_services(list[DataSeederABC]): + seeder: DataSeederABC = seeder + self._logger.debug(__name__, f"Starting seeder {type(seeder).__name__}") + await seeder.seed() self._db.save_changes() diff --git a/kdb-bot/src/modules/level/level_module.py b/kdb-bot/src/modules/level/level_module.py index 1e2295a9..be99518c 100644 --- a/kdb-bot/src/modules/level/level_module.py +++ b/kdb-bot/src/modules/level/level_module.py @@ -8,6 +8,7 @@ from cpl_discord.service.discord_collection_abc import DiscordCollectionABC from bot_core.abc.module_abc import ModuleABC from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum +from bot_data.abc.data_seeder_abc import DataSeederABC from modules.level.command.level_group import LevelGroup from modules.level.events.level_on_member_join_event import LevelOnMemberJoinEvent from modules.level.events.level_on_message_event import LevelOnMessageEvent @@ -29,7 +30,7 @@ class LevelModule(ModuleABC): env.set_working_directory(cwd) def configure_services(self, services: ServiceCollectionABC, env: ApplicationEnvironmentABC): - services.add_transient(LevelSeeder) + services.add_transient(DataSeederABC, LevelSeeder) services.add_transient(LevelService) # commands diff --git a/kdb-bot/src/modules/technician/api_key_seeder.py b/kdb-bot/src/modules/technician/api_key_seeder.py new file mode 100644 index 00000000..f8f1305c --- /dev/null +++ b/kdb-bot/src/modules/technician/api_key_seeder.py @@ -0,0 +1,44 @@ +from cpl_core.configuration import ConfigurationABC +from cpl_core.database.context import DatabaseContextABC +from cpl_discord.service import DiscordBotServiceABC + +from bot_core.logging.database_logger import DatabaseLogger +from bot_data.abc.api_key_repository_abc import ApiKeyRepositoryABC +from bot_data.abc.data_seeder_abc import DataSeederABC +from bot_data.abc.user_repository_abc import UserRepositoryABC +from bot_data.model.api_key import ApiKey + + +class ApiKeySeeder(DataSeederABC): + def __init__( + self, + logger: DatabaseLogger, + config: ConfigurationABC, + bot: DiscordBotServiceABC, + db: DatabaseContextABC, + users: UserRepositoryABC, + api_keys: ApiKeyRepositoryABC, + ): + DataSeederABC.__init__(self) + + self._logger = logger + self._config = config + self._bot = bot + self._db = db + self._users = users + self._api_keys = api_keys + + async def seed(self): + self._logger.debug(__name__, f"API-Key seeder started") + + if self._api_keys.get_api_keys().count() > 0: + self._logger.debug(__name__, f"Skip API-Key seeder") + return + + try: + frontend_key = ApiKey("frontend", "87f529fd-a32e-40b3-a1d1-7a1583cf3ff5", None) + self._api_keys.add_api_key(frontend_key) + self._db.save_changes() + self._logger.info(__name__, f"Created frontend API-Key") + except Exception as e: + self._logger.fatal(__name__, "Cannot create frontend API-Key", e) diff --git a/kdb-bot/src/modules/technician/technician_module.py b/kdb-bot/src/modules/technician/technician_module.py index ab3e2210..9aa5089c 100644 --- a/kdb-bot/src/modules/technician/technician_module.py +++ b/kdb-bot/src/modules/technician/technician_module.py @@ -5,8 +5,10 @@ from cpl_discord.service.discord_collection_abc import DiscordCollectionABC from bot_core.abc.module_abc import ModuleABC from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum +from bot_data.abc.data_seeder_abc import DataSeederABC from modules.base.abc.base_helper_abc import BaseHelperABC from modules.base.service.base_helper_service import BaseHelperService +from modules.technician.api_key_seeder import ApiKeySeeder from modules.technician.command.api_key_group import ApiKeyGroup from modules.technician.command.log_command import LogCommand from modules.technician.command.restart_command import RestartCommand @@ -21,6 +23,7 @@ class TechnicianModule(ModuleABC): pass def configure_services(self, services: ServiceCollectionABC, env: ApplicationEnvironmentABC): + services.add_transient(DataSeederABC, ApiKeySeeder) services.add_transient(BaseHelperABC, BaseHelperService) # commands self._dc.add_command(RestartCommand)