diff --git a/kdb-bot/src/bot/startup_migration_extension.py b/kdb-bot/src/bot/startup_migration_extension.py index 4f149463..415c8344 100644 --- a/kdb-bot/src/bot/startup_migration_extension.py +++ b/kdb-bot/src/bot/startup_migration_extension.py @@ -4,6 +4,7 @@ from cpl_core.dependency_injection import ServiceCollectionABC from cpl_core.environment import ApplicationEnvironmentABC from bot_data.abc.migration_abc import MigrationABC +from bot_data.migration.api_key_migration import ApiKeyMigration from bot_data.migration.api_migration import ApiMigration from bot_data.migration.auto_role_fix1_migration import AutoRoleFix1Migration from bot_data.migration.auto_role_migration import AutoRoleMigration @@ -32,3 +33,4 @@ class StartupMigrationExtension(StartupExtensionABC): services.add_transient(MigrationABC, StatsMigration) # 09.11.2022 #46 - 0.3.0 services.add_transient(MigrationABC, AutoRoleFix1Migration) # 30.12.2022 #151 - 0.3.0 services.add_transient(MigrationABC, UserMessageCountPerHourMigration) # 11.01.2023 #168 - 0.3.1 + services.add_transient(MigrationABC, ApiKeyMigration) # 09.02.2023 #162 - 1.0.0 diff --git a/kdb-bot/src/bot/translation/de.json b/kdb-bot/src/bot/translation/de.json index 17524fc7..25c1301e 100644 --- a/kdb-bot/src/bot/translation/de.json +++ b/kdb-bot/src/bot/translation/de.json @@ -283,7 +283,17 @@ "technician": { "restart_message": "Bin gleich wieder da :D", "shutdown_message": "Trauert nicht um mich, es war eine logische Entscheidung. Das Wohl von Vielen, es wiegt schwerer als das Wohl von Wenigen oder eines Einzelnen. Ich war es und ich werde es immer sein, Euer Freund. Lebt lange und in Frieden :)", - "log_message": "Hier sind deine Logdateien! :)" + "log_message": "Hier sind deine Logdateien! :)", + "api_key": { + "get": "API-Schlüssel für {}: {}", + "add": { + "success": "API-Schlüssel für {} wurde erstellt: {}" + }, + "remove": { + "not_found": "API-Schlüssel konnte nicht gefunden werden!", + "success": "API-Schlüssel wurde entfernt :D" + } + } } }, "api": { 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/abc/api_key_repository_abc.py b/kdb-bot/src/bot_data/abc/api_key_repository_abc.py new file mode 100644 index 00000000..c21d22df --- /dev/null +++ b/kdb-bot/src/bot_data/abc/api_key_repository_abc.py @@ -0,0 +1,35 @@ +from abc import ABC, abstractmethod + +from cpl_query.extension import List + +from bot_data.model.api_key import ApiKey + + +class ApiKeyRepositoryABC(ABC): + @abstractmethod + def __init__(self): + pass + + @abstractmethod + def get_api_keys(self) -> List[ApiKey]: + pass + + @abstractmethod + def get_api_key(self, identifier: str, key: str) -> ApiKey: + pass + + @abstractmethod + def get_api_key_by_key(self, key: str) -> ApiKey: + pass + + @abstractmethod + def add_api_key(self, api_key: ApiKey): + pass + + @abstractmethod + def update_api_key(self, api_key: ApiKey): + pass + + @abstractmethod + def delete_api_key(self, api_key: ApiKey): + pass diff --git a/kdb-bot/src/bot_data/data_module.py b/kdb-bot/src/bot_data/data_module.py index 87152541..6e8c8000 100644 --- a/kdb-bot/src/bot_data/data_module.py +++ b/kdb-bot/src/bot_data/data_module.py @@ -5,6 +5,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.api_key_repository_abc import ApiKeyRepositoryABC from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC from bot_data.abc.auto_role_repository_abc import AutoRoleRepositoryABC from bot_data.abc.client_repository_abc import ClientRepositoryABC @@ -20,6 +21,7 @@ from bot_data.abc.user_message_count_per_hour_repository_abc import ( UserMessageCountPerHourRepositoryABC, ) from bot_data.abc.user_repository_abc import UserRepositoryABC +from bot_data.service.api_key_repository_service import ApiKeyRepositoryService from bot_data.service.auth_user_repository_service import AuthUserRepositoryService from bot_data.service.auto_role_repository_service import AutoRoleRepositoryService from bot_data.service.client_repository_service import ClientRepositoryService @@ -48,6 +50,7 @@ class DataModule(ModuleABC): pass def configure_services(self, services: ServiceCollectionABC, env: ApplicationEnvironmentABC): + services.add_transient(ApiKeyRepositoryABC, ApiKeyRepositoryService) services.add_transient(AuthUserRepositoryABC, AuthUserRepositoryService) services.add_transient(ServerRepositoryABC, ServerRepositoryService) services.add_transient(UserRepositoryABC, UserRepositoryService) diff --git a/kdb-bot/src/bot_data/migration/api_key_migration.py b/kdb-bot/src/bot_data/migration/api_key_migration.py new file mode 100644 index 00000000..76004f13 --- /dev/null +++ b/kdb-bot/src/bot_data/migration/api_key_migration.py @@ -0,0 +1,38 @@ +from bot_core.logging.database_logger import DatabaseLogger +from bot_data.abc.migration_abc import MigrationABC +from bot_data.db_context import DBContext + + +class ApiKeyMigration(MigrationABC): + name = "1.0_ApiKeyMigration" + + def __init__(self, logger: DatabaseLogger, db: DBContext): + MigrationABC.__init__(self) + self._logger = logger + self._db = db + self._cursor = db.cursor + + def upgrade(self): + self._logger.debug(__name__, "Running upgrade") + + self._cursor.execute( + str( + f""" + CREATE TABLE IF NOT EXISTS `ApiKeys` ( + `Id` BIGINT NOT NULL AUTO_INCREMENT, + `Identifier` VARCHAR(255) NOT NULL, + `Key` VARCHAR(255) NOT NULL, + `CreatorId` BIGINT NULL, + `CreatedAt` DATETIME(6), + `LastModifiedAt` DATETIME(6), + PRIMARY KEY(`Id`), + FOREIGN KEY (`CreatorId`) REFERENCES `Users`(`UserId`), + CONSTRAINT UC_Identifier_Key UNIQUE (`Identifier`,`Key`), + CONSTRAINT UC_Key UNIQUE (`Key`) + ); + """ + ) + ) + + def downgrade(self): + self._cursor.execute("DROP TABLE `ApiKeys`;") diff --git a/kdb-bot/src/bot_data/migration/auto_role_fix1_migration.py b/kdb-bot/src/bot_data/migration/auto_role_fix1_migration.py index 7a5766c2..70a8e30e 100644 --- a/kdb-bot/src/bot_data/migration/auto_role_fix1_migration.py +++ b/kdb-bot/src/bot_data/migration/auto_role_fix1_migration.py @@ -4,7 +4,7 @@ from bot_data.db_context import DBContext class AutoRoleFix1Migration(MigrationABC): - name = "0.3.0_AutoRoleMigration" + name = "0.3.0_AutoRoleFixMigration" def __init__(self, logger: DatabaseLogger, db: DBContext): MigrationABC.__init__(self) diff --git a/kdb-bot/src/bot_data/model/api_key.py b/kdb-bot/src/bot_data/model/api_key.py new file mode 100644 index 00000000..d27037cb --- /dev/null +++ b/kdb-bot/src/bot_data/model/api_key.py @@ -0,0 +1,102 @@ +from datetime import datetime +from typing import Optional + +from cpl_core.database import TableABC + +from bot_data.model.user import User + + +class ApiKey(TableABC): + def __init__( + self, + identifier: str, + key: str, + creator: Optional[User], + created_at: datetime = None, + modified_at: datetime = None, + id=0, + ): + self._id = id + self._identifier = identifier + self._key = key + self._creator = creator + + TableABC.__init__(self) + self._created_at = created_at if created_at is not None else self._created_at + self._modified_at = modified_at if modified_at is not None else self._modified_at + + @property + def identifier(self) -> str: + return self._identifier + + @property + def key(self) -> str: + return self._key + + @property + def creator(self) -> Optional[User]: + return self._creator + + @staticmethod + def get_select_all_string() -> str: + return str( + f""" + SELECT * FROM `ApiKeys`; + """ + ) + + @staticmethod + def get_select_string(identifier: str, key: str) -> str: + return str( + f""" + SELECT * FROM `ApiKeys` + WHERE `Identifier` = '{identifier}' + AND `Key` = '{key}'; + """ + ) + + @staticmethod + def get_select_by_key(key: str) -> str: + return str( + f""" + SELECT * FROM `ApiKeys` + WHERE `Key` = '{key}'; + """ + ) + + @property + def insert_string(self) -> str: + return str( + f""" + INSERT INTO `ApiKeys` ( + `Identifier`, `Key`, `CreatorId`, `CreatedAt`, `LastModifiedAt` + ) VALUES ( + '{self._identifier}', + '{self._key}', + {"NULL" if self._creator is None else self._creator.user_id}, + '{self._created_at}', + '{self._modified_at}' + ); + """ + ) + + @property + def udpate_string(self) -> str: + return str( + f""" + UPDATE `ApiKeys` + SET `Identifier` = '{self._identifier}', + `Key` = '{self._key}', + `LastModifiedAt` = '{self._modified_at}' + WHERE `Id` = {self._id}; + """ + ) + + @property + def delete_string(self) -> str: + return str( + f""" + DELETE FROM `ApiKeys` + WHERE `Id` = {self._id}; + """ + ) 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 new file mode 100644 index 00000000..e7919373 --- /dev/null +++ b/kdb-bot/src/bot_data/service/api_key_repository_service.py @@ -0,0 +1,73 @@ +from typing import Optional + +from cpl_core.database.context import DatabaseContextABC +from cpl_query.extension import List + +from bot_core.logging.database_logger import DatabaseLogger +from bot_data.abc.api_key_repository_abc import ApiKeyRepositoryABC +from bot_data.abc.user_repository_abc import UserRepositoryABC +from bot_data.model.api_key import ApiKey + + +class ApiKeyRepositoryService(ApiKeyRepositoryABC): + def __init__( + self, + logger: DatabaseLogger, + db_context: DatabaseContextABC, + users: UserRepositoryABC, + ): + self._logger = logger + self._context = db_context + + self._users = users + + ApiKeyRepositoryABC.__init__(self) + + @staticmethod + def _get_value_from_result(value: any) -> Optional[any]: + if isinstance(value, str) and "NULL" in value: + return None + + 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]), + 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]), + ) + + return api_key + + def get_api_keys(self) -> List[ApiKey]: + api_keys = List(ApiKey) + self._logger.trace(__name__, f"Send SQL command: {ApiKey.get_select_all_string()}") + results = self._context.select(ApiKey.get_select_all_string()) + for result in results: + api_keys.append(self._api_key_from_result(result)) + + return api_keys + + def get_api_key(self, identifier: str, key: str) -> ApiKey: + self._logger.trace(__name__, f"Send SQL command: {ApiKey.get_select_string(identifier, key)}") + return self._api_key_from_result(self._context.select(ApiKey.get_select_string(identifier, key))[0]) + + def get_api_key_by_key(self, key: str) -> ApiKey: + self._logger.trace(__name__, f"Send SQL command: {ApiKey.get_select_by_key(key)}") + return self._api_key_from_result(self._context.select(ApiKey.get_select_by_key(key))[0]) + + def add_api_key(self, api_key: ApiKey): + self._logger.trace(__name__, f"Send SQL command: {api_key.insert_string}") + self._context.cursor.execute(api_key.insert_string) + + def update_api_key(self, api_key: ApiKey): + self._logger.trace(__name__, f"Send SQL command: {api_key.udpate_string}") + self._context.cursor.execute(api_key.udpate_string) + + def delete_api_key(self, api_key: ApiKey): + self._logger.trace(__name__, f"Send SQL command: {api_key.delete_string}") + self._context.cursor.execute(api_key.delete_string) 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/command/api_key_group.py b/kdb-bot/src/modules/technician/command/api_key_group.py new file mode 100644 index 00000000..1f7f0c0d --- /dev/null +++ b/kdb-bot/src/modules/technician/command/api_key_group.py @@ -0,0 +1,139 @@ +import hashlib +import uuid +from typing import List as TList + +import discord +from cpl_core.database.context import DatabaseContextABC +from cpl_discord.command import DiscordCommandABC +from cpl_discord.service import DiscordBotServiceABC +from cpl_translation import TranslatePipe +from discord import app_commands +from discord.ext import commands +from discord.ext.commands import Context + +from bot_api.configuration.authentication_settings import AuthenticationSettings +from bot_core.abc.client_utils_abc import ClientUtilsABC +from bot_core.abc.message_service_abc import MessageServiceABC +from bot_core.helper.command_checks import CommandChecks +from bot_core.logging.command_logger import CommandLogger +from bot_data.abc.api_key_repository_abc import ApiKeyRepositoryABC +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 modules.permission.abc.permission_service_abc import PermissionServiceABC + + +class ApiKeyGroup(DiscordCommandABC): + def __init__( + self, + logger: CommandLogger, + auth_settings: AuthenticationSettings, + message_service: MessageServiceABC, + bot: DiscordBotServiceABC, + client_utils: ClientUtilsABC, + permission_service: PermissionServiceABC, + translate: TranslatePipe, + db: DatabaseContextABC, + servers: ServerRepositoryABC, + users: UserRepositoryABC, + api_keys: ApiKeyRepositoryABC, + ): + DiscordCommandABC.__init__(self) + + self._logger = logger + self._auth_settings = auth_settings + self._message_service = message_service + self._bot = bot + self._client_utils = client_utils + self._permissions = permission_service + self._t = translate + self._db = db + self._servers = servers + self._users = users + self._api_keys = api_keys + + 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() + + @commands.hybrid_group(name="api-key") + @commands.guild_only() + async def api_key(self, ctx: Context): + pass + + @api_key.command() + @commands.guild_only() + @CommandChecks.check_is_ready() + @CommandChecks.check_is_member_technician() + async def get(self, ctx: Context, key: str, wait: int = None): + self._logger.debug(__name__, f"Received command api-key get {ctx}: {key},{wait}") + + api_key = self._api_keys.get_api_key_by_key(key) + await self._message_service.send_ctx_msg( + ctx, + self._t.transform("modules.technician.api_key.get").format( + api_key.identifier, self._get_api_key_str(api_key) + ), + ) + self._logger.trace(__name__, f"Finished command api-key get") + + @get.autocomplete("key") + async def get_autocomplete(self, interaction: discord.Interaction, current: str) -> TList[app_commands.Choice[str]]: + keys = self._api_keys.get_api_keys() + + return [ + app_commands.Choice(name=f"{key.identifier}: {key.key}", value=key.key) + for key in self._client_utils.get_auto_complete_list(keys, current, lambda x: x.key) + ] + + @api_key.command() + @commands.guild_only() + @CommandChecks.check_is_ready() + @CommandChecks.check_is_member_moderator() + async def add(self, ctx: Context, identifier: str): + self._logger.debug(__name__, f"Received command api-key add {ctx}: {identifier}") + + server = self._servers.get_server_by_discord_id(ctx.guild.id) + user = self._users.get_user_by_discord_id_and_server_id(ctx.author.id, server.server_id) + api_key = ApiKey(identifier, str(uuid.uuid4()), user) + self._api_keys.add_api_key(api_key) + self._db.save_changes() + await self._message_service.send_ctx_msg( + ctx, + self._t.transform("modules.technician.api_key.add.success").format( + identifier, self._get_api_key_str(api_key) + ), + ) + + self._logger.trace(__name__, f"Finished command api-key add") + + @api_key.command() + @commands.guild_only() + @CommandChecks.check_is_ready() + @CommandChecks.check_is_member_moderator() + async def remove(self, ctx: Context, key: str): + self._logger.debug(__name__, f"Received command api-key remove {ctx}: {key}") + + keys = self._api_keys.get_api_keys().where(lambda x: x.key == key) + if keys.count() < 1: + await self._message_service.send_ctx_msg( + ctx, + self._t.transform("modules.technician.api_key.remove.not_found"), + ) + + api_key = keys.single() + self._api_keys.delete_api_key(api_key) + self._db.save_changes() + await self._message_service.send_ctx_msg(ctx, self._t.transform("modules.technician.api_key.remove.success")) + + self._logger.trace(__name__, f"Finished command api-key remove") + + @remove.autocomplete("key") + async def set_autocomplete(self, interaction: discord.Interaction, current: str) -> TList[app_commands.Choice[str]]: + keys = self._api_keys.get_api_keys() + + return [ + app_commands.Choice(name=f"{key.identifier}: {key.key}", value=key.key) + for key in self._client_utils.get_auto_complete_list(keys, current, lambda x: x.key) + ] diff --git a/kdb-bot/src/modules/technician/technician_module.py b/kdb-bot/src/modules/technician/technician_module.py index 9a9ca7ba..9aa5089c 100644 --- a/kdb-bot/src/modules/technician/technician_module.py +++ b/kdb-bot/src/modules/technician/technician_module.py @@ -5,11 +5,14 @@ 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 from modules.technician.command.shutdown_command import ShutdownCommand -from modules.base.service.base_helper_service import BaseHelperService class TechnicianModule(ModuleABC): @@ -20,9 +23,11 @@ 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) self._dc.add_command(ShutdownCommand) self._dc.add_command(LogCommand) + self._dc.add_command(ApiKeyGroup) # events