graphql part3/3 #162-3 #194

Merged
edraft merged 4 commits from #162-3 into 1.0.0 2023-02-11 10:39:49 +01:00
12 changed files with 112 additions and 19 deletions
Showing only changes of commit a6df06f13a - Show all commits

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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")

View File

@ -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")

View File

@ -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`),

View File

@ -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}'
);

View File

@ -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]),

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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)