diff --git a/auth_backend/auth_plugins/auth_method.py b/auth_backend/auth_plugins/auth_method.py index 91fad77a..6c01e304 100644 --- a/auth_backend/auth_plugins/auth_method.py +++ b/auth_backend/auth_plugins/auth_method.py @@ -42,148 +42,11 @@ class Session(Base): AUTH_METHODS: dict[str, type[AuthMethodMeta]] = {} -class MethodMeta(metaclass=ABCMeta): - """Параметры метода аввторизации пользователя - Args: - `__fields__: frozenset - required` - множество параметров данного метода авторизации - - `__required_fields__: frozenset - required` - множество обязательных парамтеров данного метода авторизации - - `__auth_method__: str - required` - __repr__ соотвествуещго метода авторизации - - Пример: - ``` - class YourAuthParams(MethodMeta): - __auth_method__ = "your_auth" ##YourAuth.__repr__ === "your_auth" - - __fields__ = frozenset(frozenset(("very_important_field", "not_important_field",))("very_important_field", "not_important_field",)) - - __required_fields__ = frozenset(("very_important_field",)) - ``` - """ - - __auth_method__: str = None - - __fields__ = frozenset() - __required_fields__ = frozenset() - __user: User - - def __init__(self, user: User, methods: list[AuthMethod] = None): - assert self.__fields__ and self.__required_fields__, "__fields__ or __required_fields__ not defined" - if methods is None: - methods = [] - self.__user = user - for method in methods: - assert method.param in self.__fields__ - setattr(self, method.param, method) - - async def create(self, param: str, value: str) -> AuthMethod: - """ - Создает AuthMethod у данного юзера, auth_method берется из - self.__auth_method__ - - Args: - param: str - параметр AuthMethod - - value: str - значение, которое будет задано по этому параметру - - Returns: - AuthMethod - созданный метод - - Raises: - AssertionError - если param не нахяодятся в __fields__ - - AlreadyExists - если метод по такому ключу уже существует - - Пример: - ``` - user.auth_methods.email.create("email", value) - ``` - """ - assert param in self.__fields__, "You cant create auth_method which not declared in __fields__" - if (attr := getattr(self, param)) and getattr(attr, "is_deleted") is not True: - raise AlreadyExists(AuthMethod, attr.id) - _method = AuthMethod( - user_id=self.__user.id, param=param, value=value, auth_method=self.__class__.get_auth_method_name() - ) - assert param in self.__fields__, "You cant create auth_method which not daclared" - db.session.add(_method) - db.session.flush() - db.session.refresh(self.__user) - setattr(self, param, _method) - return _method - - async def bulk_create(self, map: dict[str, str]) -> list[AuthMethod]: - """Создает несколько AuthMethod'ов по мапе param-value, - auth_method берется из self.__auth_method__ - - Args: - map: dict[str, str] - словарь, по которому будуут создаваться AuthMthods - - Returns: - list[AuthMethod] - созданные методы - - Raises: - AssertionError - если ключи словаря не нахяодятся в ___fields__ - - AlreadyExists - если метод по такому ключу уже существует - - Пример: - ``` - user.auth_method.email.bulk_create({"email": val1, "salt": val2}) - ``` - """ - for k in map.keys(): - assert k in self.__fields__, "You cant create auth_method which not declared in __fields__" - if (attr := getattr(self, k)) and getattr(attr, "is_deleted") is not True: - raise AlreadyExists(AuthMethod, attr.id) - methods: list[AuthMethod] = [] - for k, v in map.items(): - methods.append( - method := AuthMethod( - user_id=self.__user.id, param=k, value=v, auth_method=self.__class__.get_auth_method_name() - ) - ) - db.session.add(method) - db.session.flush() - db.session.refresh(self.__user) - return methods - - def __bool__(self) -> bool: - """Определен ли для польщователя этот метод аутентификации - Args: - None - Returns: - Если у юзера удалено/не определено хотя бы одно из полей из - __required_fields__ -> False, иначе True - - """ - for field in self.__required_fields__: - if not getattr(self, field): - return False - return True - - @classmethod - def get_auth_method_name(cls) -> str: - """Имя соответствующего метода аутентфикации - - Args: - None - - Returns: - Имя метода аутентификации, к которому - приилагается данный класс - """ - return re.sub(r"(? str: return re.sub(r"(? UserLoginKey: @staticmethod async def _create_session( - user: User, scopes_list_names: list[TypeScope] | None, session_name: str = None, *, db_session: DbSession + user: User, scopes_list_names: list[TypeScope] | None, session_name: str | None = None, *, db_session: DbSession ) -> Session: """Создает сессию пользователя""" return await create_session(user, scopes_list_names, db_session=db_session, session_name=session_name) diff --git a/auth_backend/auth_plugins/email.py b/auth_backend/auth_plugins/email.py index 1bd4a318..36ef6fe9 100644 --- a/auth_backend/auth_plugins/email.py +++ b/auth_backend/auth_plugins/email.py @@ -15,10 +15,11 @@ from auth_backend.models.db import AuthMethod, User, UserSession from auth_backend.schemas.types.scopes import Scope from auth_backend.settings import get_settings +from auth_backend.utils.auth_params import get_auth_params from auth_backend.utils.security import UnionAuth from auth_backend.utils.smtp import SendEmailMessage -from .auth_method import AuthMethodMeta, MethodMeta, Session, random_string +from .auth_method import AuthMethodMeta, Session, random_string settings = get_settings() @@ -101,37 +102,12 @@ class ResetForgottenPassword(Base): new_password: constr(min_length=1) -class EmailParams(MethodMeta): - __auth_method__ = "Email" - __fields__ = frozenset( - ( - "email", - "hashed_password", - "salt", - "confirmed", - "confirmation_token", - "tmp_email", - "reset_token", - "tmp_email_confirmation_token", - ) - ) - - __required_fields__ = frozenset(("email", "hashed_password", "salt", "confirmed", "confirmation_token")) - - email: AuthMethod = None - hashed_password: AuthMethod = None - salt: AuthMethod = None - confirmed: AuthMethod = None - confirmation_token: AuthMethod = None - tmp_email: AuthMethod = None - reset_token: AuthMethod = None - tmp_email_confirmation_token: AuthMethod = None - - class Email(AuthMethodMeta): prefix = "/email" - fields = EmailParams + @staticmethod + def _get_email_params(user_id: int) -> dict[str, AuthMethod]: + return get_auth_params(user_id, "email", db.session) def __init__(self): super().__init__() @@ -173,18 +149,19 @@ async def _login(cls, user_inp: EmailLogin, background_tasks: BackgroundTasks) - ) if not query: raise AuthFailed("Incorrect login or password", "Некорректный логин или пароль") - if query.user.auth_methods.email.confirmed.value.lower() == "false": + auth_params = cls._get_email_params(query.user_id) + if auth_params["confirmed"].value.lower() == "false": raise AuthFailed( "Registration wasn't completed. Try to registrate again and do not forget to approve your email", "Регистрация не была завершена. Попробуйте зарегистрироваться снова и не забудьте подтвердить почту", ) - if query.user.auth_methods.email.email.value.lower() != user_inp.email.lower() or not Email._validate_password( + if auth_params["email"].value.lower() != user_inp.email.lower() or not Email._validate_password( user_inp.password, - query.user.auth_methods.email.hashed_password.value, - query.user.auth_methods.email.salt.value, + auth_params["hashed_password"].value, + auth_params["salt"].value, ): raise AuthFailed("Incorrect login or password", "Некорректный логин или пароль") - userdata = await Email._convert_data_to_userdata_format({"email": query.user.auth_methods.email.email.value}) + userdata = await Email._convert_data_to_userdata_format({"email": auth_params["email"].value}) await get_kafka_producer().produce( settings.KAFKA_USER_LOGIN_TOPIC_NAME, Email.generate_kafka_key(query.user.id), @@ -206,14 +183,16 @@ async def _add_to_db(user_inp: EmailRegister, confirmation_token: str, user: Use "confirmed": str(False), "confirmation_token": confirmation_token, } - await user.auth_methods.email.bulk_create(map) + for k, v in map.items(): + AuthMethod.create(user_id=user.id, auth_method="email", param=k, value=v, session=db.session) @staticmethod async def _change_confirmation_link(user: User, confirmation_token: str) -> None: - if user.auth_methods.email.confirmed.value == "true": + auth_params = Email._get_email_params(user.id) + if auth_params["confirmed"].value == "true": raise AlreadyExists(User, user.id) else: - user.auth_methods.email.confirmation_token.value = confirmation_token + auth_params["confirmation_token"].value = confirmation_token @classmethod async def _register( @@ -224,7 +203,7 @@ async def _register( user_session: UserSession = Depends(UnionAuth(scopes=[], allow_none=True, auto_error=True)), ) -> StatusResponseModel: confirmation_token: str = random_string() - auth_method: AuthMethod = ( + auth_method: AuthMethod | None = ( AuthMethod.query(session=db.session) .filter( AuthMethod.param == "email", @@ -297,10 +276,9 @@ async def _approve_email(token: str, background_tasks: BackgroundTasks) -> Statu status="Error", message="Incorrect link", ru="Некорректная ссылка" ).model_dump(), ) - auth_method.user.auth_methods.email.confirmed.value = "true" - userdata = await Email._convert_data_to_userdata_format( - {"email": auth_method.user.auth_methods.email.email.value} - ) + auth_params = Email._get_email_params(auth_method.user.id) + auth_params["confirmed"].value = "true" + userdata = await Email._convert_data_to_userdata_format({"email": auth_params["email"].value}) await get_kafka_producer().produce( settings.KAFKA_USER_LOGIN_TOPIC_NAME, Email.generate_kafka_key(auth_method.user.id), @@ -317,14 +295,15 @@ async def _request_reset_email( background_tasks: BackgroundTasks, user_session: UserSession = Depends(UnionAuth(scopes=[], allow_none=False, auto_error=True)), ) -> StatusResponseModel: - if not user_session.user.auth_methods.email: + auth_params = Email._get_email_params(user_session.user_id) + if "email" not in auth_params: raise IncorrectUserAuthType() - if user_session.user.auth_methods.email.confirmed.value == "false": + if auth_params["confirmed"].value == "false": raise AuthFailed( "Registration wasn't completed. Try to registrate again and do not forget to approve your email", "Регистрация не была завершена. Паоробуйте зарегистрироваться снова и не забудьте подтвердить почту", ) - if user_session.user.auth_methods.email.email.value == scheme.email: + if auth_params["email"].value == scheme.email: raise HTTPException( status_code=401, detail=StatusResponseModel( @@ -332,13 +311,21 @@ async def _request_reset_email( ).model_dump(), ) token = random_string(length=settings.TOKEN_LENGTH) - if user_session.user.auth_methods.email.tmp_email is not None: - user_session.user.auth_methods.email.tmp_email.is_deleted = True - user_session.user.auth_methods.email.tmp_email_confirmation_token.is_deleted = True + if "tmp_email" in auth_params: + auth_params["tmp_email"].is_deleted = True + auth_params["tmp_email_confirmation_token"].is_deleted = True db.session.flush() - await user_session.user.auth_methods.email.bulk_create( - {"tmp_email_confirmation_token": token, "tmp_email": scheme.email} + AuthMethod.create( + user_id=user_session.user_id, + auth_method="email", + param="tmp_email_confirmation_token", + value=token, + session=db.session, ) + AuthMethod.create( + user_id=user_session.user_id, auth_method="email", param="tmp_email", value=scheme.email, session=db.session + ) + SendEmailMessage.send( to_email=scheme.email, ip=request.client.host, @@ -355,7 +342,7 @@ async def _request_reset_email( @staticmethod async def _reset_email(token: str, background_tasks: BackgroundTasks) -> StatusResponseModel: - auth: AuthMethod = ( + auth: AuthMethod | None = ( AuthMethod.query(session=db.session) .filter( AuthMethod.param == 'tmp_email_confirmation_token', @@ -370,16 +357,17 @@ async def _reset_email(token: str, background_tasks: BackgroundTasks) -> StatusR status="Error", message="Incorrect confirmation token", ru="Неправильный токен подтверждения" ).model_dump(), ) + auth_params = Email._get_email_params(auth.user_id) user: User = auth.user - if user.auth_methods.email.confirmed.value == "false": + if auth_params["confirmed"].value == "false": raise AuthFailed( "Registration wasn't completed. Try to registrate again and do not forget to approve your email", "Регистрация не была завершена. Паоробуйте зарегистрироваться снова и не забудьте подтвердить почту", ) - user.auth_methods.email.email.value = user.auth_methods.email.tmp_email.value - user.auth_methods.email.tmp_email_confirmation_token.is_deleted = True - user.auth_methods.email.tmp_email.is_deleted = True - userdata = await Email._convert_data_to_userdata_format({"email": user.auth_methods.email.email.value}) + auth_params["email"].value = auth_params["tmp_email"].value + auth_params["tmp_email_confirmation_token"].is_deleted = True + auth_params["tmp_email"].is_deleted = True + userdata = await Email._convert_data_to_userdata_format({"email": auth_params["email"].value}) await get_kafka_producer().produce( settings.KAFKA_USER_LOGIN_TOPIC_NAME, Email.generate_kafka_key(user.id), userdata, bg_tasks=background_tasks ) @@ -393,7 +381,8 @@ async def _request_reset_password( background_tasks: BackgroundTasks, user_session: UserSession = Depends(UnionAuth(scopes=[], allow_none=False, auto_error=True)), ) -> StatusResponseModel: - if not user_session.user.auth_methods.email: + auth_params = Email._get_email_params(user_session.user.id) + if "email" not in auth_params: raise HTTPException( status_code=401, detail=StatusResponseModel( @@ -405,14 +394,14 @@ async def _request_reset_password( salt = random_string() if not Email._validate_password( schema.password, - user_session.user.auth_methods.email.hashed_password.value, - user_session.user.auth_methods.email.salt.value, + auth_params["hashed_password"].value, + auth_params["salt"].value, ): raise AuthFailed("Incorrect password", "Неправильный пароль") - user_session.user.auth_methods.email.hashed_password.value = Email._hash_password(schema.new_password, salt) - user_session.user.auth_methods.email.salt.value = salt + auth_params["hashed_password"].value = Email._hash_password(schema.new_password, salt) + auth_params["salt"].value = salt SendEmailMessage.send( - to_email=user_session.user.auth_methods.email.email.value, + to_email=auth_params["email"].value, ip=request.client.host, message_file_name="password_change_notification.html", subject="Смена пароля Твой ФФ!", @@ -428,7 +417,7 @@ async def _request_reset_password( async def _request_reset_forgotten_password( request: Request, schema: RequestResetForgottenPassword, background_tasks: BackgroundTasks ) -> StatusResponseModel: - auth_method_email: AuthMethod = ( + auth_method_email: AuthMethod | None = ( AuthMethod.query(session=db.session) .filter( AuthMethod.auth_method == Email.get_name(), @@ -444,7 +433,8 @@ async def _request_reset_forgotten_password( status="Error", message="Email not found", ru="Почта не найдена" ).model_dump(), ) - if not auth_method_email.user.auth_methods.email: + auth_params = Email._get_email_params(auth_method_email.user.id) + if "email" not in auth_params: raise HTTPException( status_code=401, detail=StatusResponseModel( @@ -453,25 +443,30 @@ async def _request_reset_forgotten_password( ru="Метод аутентификации не установлен для пользователя", ).model_dump(), ) - if auth_method_email.user.auth_methods.email.confirmed.value.lower() == "false": + if auth_params["confirmed"].value.lower() == "false": raise AuthFailed( "Registration wasn't completed. Try to registrate again and do not forget to approve your email", "Регистрация не была завершена. Паоробуйте зарегистрироваться снова и не забудьте подтвердить почту", ) - if auth_method_email.user.auth_methods.email.reset_token is not None: - auth_method_email.user.auth_methods.email.reset_token.is_deleted = True + if "reset_token" in auth_params: + auth_params["reset_token"].is_deleted = True db.session.flush() - await auth_method_email.user.auth_methods.email.create( - "reset_token", random_string(length=settings.TOKEN_LENGTH) + AuthMethod.create( + user_id=auth_method_email.user.id, + auth_method="email", + param="reset_token", + value=random_string(length=settings.TOKEN_LENGTH), + session=db.session, ) + auth_params = Email._get_email_params(auth_method_email.user.id) SendEmailMessage.send( - to_email=auth_method_email.user.auth_methods.email.email.value, + to_email=auth_params["email"].value, ip=request.client.host, message_file_name="password_change_confirmation.html", subject="Смена пароля Твой ФФ!", dbsession=db.session, background_tasks=background_tasks, - url=f"{settings.APPLICATION_HOST}/auth/reset/password?token={auth_method_email.user.auth_methods.email.reset_token.value}", + url=f"{settings.APPLICATION_HOST}/auth/reset/password?token={auth_params['reset_token'].value}", ) return StatusResponseModel( status="Success", message="Reset link has been successfully mailed", ru="Ссылка отправлена на почту" @@ -497,10 +492,11 @@ async def _reset_forgotten_password( status="Error", message="Invalid reset token", ru="Неправильный токен сброса" ).model_dump(), ) + auth_params = Email._get_email_params(auth_method.user.id) salt = random_string() - auth_method.user.auth_methods.email.hashed_password.value = Email._hash_password(schema.new_password, salt) - auth_method.user.auth_methods.email.salt.value = salt - auth_method.user.auth_methods.email.reset_token.is_deleted = True + auth_params["hashed_password"].value = Email._hash_password(schema.new_password, salt) + auth_params["salt"].value = salt + auth_params["reset_token"].is_deleted = True db.session.commit() return StatusResponseModel( status="Success", message="Password has been successfully changed", ru="Пароль изменен" diff --git a/auth_backend/auth_plugins/github.py b/auth_backend/auth_plugins/github.py index 80b882b6..2d54aef1 100644 --- a/auth_backend/auth_plugins/github.py +++ b/auth_backend/auth_plugins/github.py @@ -17,7 +17,7 @@ from auth_backend.settings import Settings from auth_backend.utils.security import UnionAuth -from .auth_method import MethodMeta, OauthMeta, Session +from .auth_method import OauthMeta, Session logger = logging.getLogger(__name__) @@ -29,21 +29,11 @@ class GithubSettings(Settings): GITHUB_CLIENT_SECRET: str | None = None -class GithubAuthParams(MethodMeta): - __auth_method__ = "GithubAuth" - __fields__ = frozenset(("user_id",)) - __required_fields__ = frozenset(("user_id",)) - - user_id: AuthMethod = None - - class GithubAuth(OauthMeta): """Вход в приложение по аккаунту GitHub""" prefix = '/github' tags = ['github'] - - fields = GithubAuthParams settings = GithubSettings() class OauthResponseSchema(BaseModel): diff --git a/auth_backend/auth_plugins/google.py b/auth_backend/auth_plugins/google.py index 17c8a44e..e86e9c22 100644 --- a/auth_backend/auth_plugins/google.py +++ b/auth_backend/auth_plugins/google.py @@ -14,12 +14,12 @@ from auth_backend.exceptions import AlreadyExists, OauthAuthFailed, OauthCredentialsIncorrect from auth_backend.kafka.kafka import get_kafka_producer -from auth_backend.models.db import AuthMethod, User, UserSession +from auth_backend.models.db import User, UserSession from auth_backend.schemas.types.scopes import Scope from auth_backend.settings import Settings from auth_backend.utils.security import UnionAuth -from .auth_method import MethodMeta, OauthMeta, Session +from .auth_method import OauthMeta, Session logger = logging.getLogger(__name__) @@ -37,20 +37,11 @@ class GoogleSettings(Settings): GOOGLE_BLACKLIST_DOMAINS: list[str] | None = ['physics.msu.ru'] -class GoogleAuthParams(MethodMeta): - __auth_method__ = "GoogleAuth" - __fields__ = frozenset(("unique_google_id",)) - __required_fields__ = frozenset(("unique_google_id",)) - - unique_google_id: AuthMethod = None - - class GoogleAuth(OauthMeta): """Вход в приложение по аккаунту гугл""" prefix = '/google' tags = ['Google'] - fields = GoogleAuthParams settings = GoogleSettings() class OauthResponseSchema(BaseModel): diff --git a/auth_backend/auth_plugins/keycloak.py b/auth_backend/auth_plugins/keycloak.py index 6d3e140e..b9968544 100644 --- a/auth_backend/auth_plugins/keycloak.py +++ b/auth_backend/auth_plugins/keycloak.py @@ -17,7 +17,7 @@ from auth_backend.settings import Settings from auth_backend.utils.security import UnionAuth -from .auth_method import MethodMeta, OauthMeta, Session +from .auth_method import OauthMeta, Session logger = logging.getLogger(__name__) @@ -30,21 +30,11 @@ class KeycloakSettings(Settings): KEYCLOAK_CLIENT_SECRET: str | None = None -class KeycloakAuthParams(MethodMeta): - __auth_method__ = "KeycloakAuth" - __fields__ = frozenset(("user_id",)) - __required_fields__ = frozenset(("user_id",)) - - user_id: AuthMethod = None - - class KeycloakAuth(OauthMeta): """Вход в приложение по аккаунту Keycloak""" prefix = '/keycloak' tags = ['keycloak'] - - fields = KeycloakAuthParams settings = KeycloakSettings() class OauthResponseSchema(BaseModel): diff --git a/auth_backend/auth_plugins/lkmsu.py b/auth_backend/auth_plugins/lkmsu.py index ce86c38a..90c335ab 100644 --- a/auth_backend/auth_plugins/lkmsu.py +++ b/auth_backend/auth_plugins/lkmsu.py @@ -18,7 +18,7 @@ from auth_backend.utils.security import UnionAuth from auth_backend.utils.string import concantenate_strings -from .auth_method import MethodMeta, OauthMeta, Session +from .auth_method import OauthMeta, Session logger = logging.getLogger(__name__) @@ -31,21 +31,11 @@ class LkmsuSettings(Settings): LKMSU_FACULTY_NAME: str = 'Физический факультет' -class LkmsuAuthParams(MethodMeta): - __auth_method__ = "LkmsuAuth" - __fields__ = frozenset(("user_id",)) - __required_fields__ = frozenset(("user_id",)) - - user_id: AuthMethod = None - - class LkmsuAuth(OauthMeta): """Вход в приложение по аккаунту гугл""" prefix = '/lk-msu' tags = ['lk_msu'] - - fields = LkmsuAuthParams settings = LkmsuSettings() class OauthResponseSchema(BaseModel): diff --git a/auth_backend/auth_plugins/methods_dict.py b/auth_backend/auth_plugins/methods_dict.py deleted file mode 100644 index 0aa8a6a1..00000000 --- a/auth_backend/auth_plugins/methods_dict.py +++ /dev/null @@ -1,80 +0,0 @@ -from auth_backend.models.db import AuthMethod, User - -from .auth_method import MethodMeta -from .email import Email -from .github import GithubAuth -from .google import GoogleAuth -from .keycloak import KeycloakAuth -from .lkmsu import LkmsuAuth -from .mymsu import MyMsuAuth -from .physics import PhysicsAuth -from .telegram import TelegramAuth -from .vk import VkAuth -from .yandex import YandexAuth - - -class MethodsDict: - """Доступные методы авторизации пользователя - - Как это использовать? Когда создаете новый метод авторизации, - определите около класса метода класс с именем `{название класса метода авторизации}Params`, - наследника MethodMeta. Например: - ``` - class EmailParams: - pass - ``` - - Подробно про MethodMeta можно посмотреть в документации MethodMeta. - - Определите в классе авторизации ссылку `fields = тому классу, который вы создали`. - Далее добавляете в MethodsDict поле, имя которого это название метода авторизации, - определите его с дефлотным значением None, - поставьте тайп хинт `{__repr__ класса метода авторизации}.fields}. Например: - ``` - class MethodsDict: - ... - your_auth: YourAuth.fields = None - ``` - - Примеры использования: - ``` - email = user.auth_methods.email.email.value - - user.auth_methods.email.tmp_token.is_deleted = True - session.commit() - - user.auth_methods.email.create('tmp_token', random_string()) - - user.auth_methods.email.bulk_create({"email": value}) - ``` - - """ - - __user: User - email: Email.fields = None - google_auth: GoogleAuth.fields = None - physics_auth: PhysicsAuth.fields = None - lkmsu_auth: LkmsuAuth.fields = None - my_msu_auth: MyMsuAuth.fields = None - telegram_auth: TelegramAuth.fields = None - vk_auth: VkAuth.fields = None - github_auth: GithubAuth.fields = None - yandex_auth: YandexAuth.fields = None - keycloak_auth: KeycloakAuth.fields = None - - def __new__(cls, methods: list[AuthMethod], user: User, *args, **kwargs): - obj = super(MethodsDict, cls).__new__(cls) - obj.__user = user - _methods_dict: dict[str, list[AuthMethod]] = {} - for method in methods: - if method.auth_method not in _methods_dict.keys(): - _methods_dict[method.auth_method] = [] - _methods_dict[method.auth_method].append(method) - for Method in MethodMeta.__subclasses__(): - if Method.get_auth_method_name() not in _methods_dict.keys(): - _obj = Method(user=obj.__user) - setattr(obj, Method.get_auth_method_name(), _obj) - continue - _obj = Method(methods=_methods_dict[Method.get_auth_method_name()], user=obj.__user) - setattr(obj, Method.get_auth_method_name(), _obj) - return obj diff --git a/auth_backend/auth_plugins/mymsu.py b/auth_backend/auth_plugins/mymsu.py index 4f0ed35d..67c1e676 100644 --- a/auth_backend/auth_plugins/mymsu.py +++ b/auth_backend/auth_plugins/mymsu.py @@ -2,7 +2,7 @@ from auth_backend.settings import Settings -from .yandex import YandexAuth, YandexAuthParams +from .yandex import YandexAuth class MyMsuSettings(Settings): @@ -16,13 +16,8 @@ class MyMsuSettings(Settings): YANDEX_BLACKLIST_DOMAINS: list[str] | None = None -class MyMsuAuthParams(YandexAuthParams): - __auth_method__ = "MyMsuAuth" - - class MyMsuAuth(YandexAuth): """Вход в приложение по почте @my.msu.ru""" prefix = '/my-msu' - fields = MyMsuAuthParams settings = MyMsuSettings() diff --git a/auth_backend/auth_plugins/physics.py b/auth_backend/auth_plugins/physics.py index c5ccff55..7ffd36f1 100644 --- a/auth_backend/auth_plugins/physics.py +++ b/auth_backend/auth_plugins/physics.py @@ -2,7 +2,7 @@ from auth_backend.settings import Settings -from .google import GoogleAuth, GoogleAuthParams +from .google import GoogleAuth class PhysicsSettings(Settings): @@ -23,13 +23,8 @@ class PhysicsSettings(Settings): GOOGLE_BLACKLIST_DOMAINS: list[str] | None = None -class PhysicsAuthParams(GoogleAuthParams): - __auth_method__ = "PhysicsAuth" - - class PhysicsAuth(GoogleAuth): """Вход в приложение по почте @physics.msu.ru""" prefix = '/physics-msu' settings = PhysicsSettings() - fields = PhysicsAuthParams diff --git a/auth_backend/auth_plugins/telegram.py b/auth_backend/auth_plugins/telegram.py index 89f8b154..148a0806 100644 --- a/auth_backend/auth_plugins/telegram.py +++ b/auth_backend/auth_plugins/telegram.py @@ -11,7 +11,7 @@ from fastapi_sqlalchemy import db from pydantic import BaseModel, Field -from auth_backend.auth_plugins.auth_method import MethodMeta, OauthMeta, Session +from auth_backend.auth_plugins.auth_method import OauthMeta, Session from auth_backend.exceptions import AlreadyExists, OauthAuthFailed from auth_backend.kafka.kafka import get_kafka_producer from auth_backend.models.db import AuthMethod, User, UserSession @@ -29,18 +29,9 @@ class TelegramSettings(Settings): TELEGRAM_BOT_TOKEN: str | None = None -class TelegramAuthParams(MethodMeta): - __auth_method__ = "TelegramAuth" - __fields__ = frozenset(("user_id",)) - __required_fields__ = frozenset(("user_id",)) - - user_id: AuthMethod = None - - class TelegramAuth(OauthMeta): prefix = '/telegram' tags = ['Telegram'] - fields = TelegramAuthParams settings = TelegramSettings() class OauthResponseSchema(BaseModel): diff --git a/auth_backend/auth_plugins/vk.py b/auth_backend/auth_plugins/vk.py index dad0d655..6e05a1b4 100644 --- a/auth_backend/auth_plugins/vk.py +++ b/auth_backend/auth_plugins/vk.py @@ -18,7 +18,7 @@ from auth_backend.utils.string import concantenate_strings from ..schemas.types.scopes import Scope -from .auth_method import MethodMeta, OauthMeta, Session +from .auth_method import OauthMeta, Session logger = logging.getLogger(__name__) @@ -44,19 +44,9 @@ class VkSettings(Settings): ] # Другие данные https://dev.vk.com/ru/reference/objects/user -class VkAuthParams(MethodMeta): - __auth_method__ = "VkAuth" - __fields__ = frozenset(("user_id",)) - __required_fields__ = frozenset(("user_id",)) - - user_id: AuthMethod = None - - class VkAuth(OauthMeta): prefix = '/vk' tags = ['vk'] - - fields = VkAuthParams settings = VkSettings() class OauthResponseSchema(BaseModel): diff --git a/auth_backend/auth_plugins/yandex.py b/auth_backend/auth_plugins/yandex.py index 04cf62c7..49e4a88e 100644 --- a/auth_backend/auth_plugins/yandex.py +++ b/auth_backend/auth_plugins/yandex.py @@ -10,10 +10,10 @@ from fastapi_sqlalchemy import db from pydantic import BaseModel, Field -from auth_backend.auth_plugins.auth_method import MethodMeta, OauthMeta, Session +from auth_backend.auth_plugins.auth_method import OauthMeta, Session from auth_backend.exceptions import AlreadyExists, OauthAuthFailed from auth_backend.kafka.kafka import get_kafka_producer -from auth_backend.models.db import AuthMethod, User, UserSession +from auth_backend.models.db import User, UserSession from auth_backend.schemas.types.scopes import Scope from auth_backend.settings import Settings from auth_backend.utils.security import UnionAuth @@ -31,19 +31,9 @@ class YandexSettings(Settings): YANDEX_BLACKLIST_DOMAINS: list[str] | None = ['my.msu.ru'] -class YandexAuthParams(MethodMeta): - __auth_method__ = "YandexAuth" - __fields__ = frozenset(("user_id",)) - __required_fields__ = frozenset(("user_id",)) - - user_id: AuthMethod = None - - class YandexAuth(OauthMeta): prefix = '/yandex' tags = ['Yandex'] - - fields = YandexAuthParams settings = YandexSettings() class OauthResponseSchema(BaseModel): diff --git a/auth_backend/models/db.py b/auth_backend/models/db.py index d3976d92..8c31cee9 100644 --- a/auth_backend/models/db.py +++ b/auth_backend/models/db.py @@ -76,33 +76,6 @@ def indirect_groups(self) -> set[Group]: def active_sessions(self) -> list[UserSession]: return [row for row in self.sessions if not row.expired] - @hybrid_property - def auth_methods(self): - """Все доступные методы авторизации юзера - - Args: - None - Returns: - MethodsDict - - user.auth_method.. === AuthMethod instance - - user.auth_methods. = Соответствущему объекту MethodsMeta - - Пример: - ``` - user.auth_methods.email.email.value - ``` - - """ - from auth_backend.auth_plugins.methods_dict import MethodsDict - - self.__auth_methods_cached = self.__auth_methods_cached or MethodsDict.__new__( - MethodsDict, self._auth_methods, self - ) - - return self.__auth_methods_cached - class Group(BaseDbModel): id: Mapped[int] = mapped_column(Integer, primary_key=True) diff --git a/auth_backend/routes/user.py b/auth_backend/routes/user.py index 09192cf8..7275f91e 100644 --- a/auth_backend/routes/user.py +++ b/auth_backend/routes/user.py @@ -3,6 +3,7 @@ from fastapi import APIRouter, Depends, Query from fastapi_sqlalchemy import db +from sqlalchemy.orm import Session from auth_backend.models.db import AuthMethod, Group, User, UserGroup, UserSession from auth_backend.schemas.models import User as UserModel @@ -16,6 +17,7 @@ UserScopes, UsersGet, ) +from auth_backend.utils.auth_params import get_auth_params from auth_backend.utils.security import UnionAuth @@ -33,12 +35,13 @@ async def get_user( Scopes: `["auth.user.read"]` """ result: dict[str, str | int] = {} - user = User.get(user_id, session=db.session) + user: User = User.get(user_id, session=db.session) # type: ignore + auth_params = get_auth_params(user.id, "email", db.session) result = ( result | UserInfo( id=user_id, - email=user.auth_methods.email.email.value if user.auth_methods.email.email else None, + email=auth_params["email"].value if "email" in auth_params else None, ).model_dump() ) if "groups" in info: @@ -61,6 +64,17 @@ async def get_user( return UserGet(**result).model_dump(exclude_unset=True, exclude={"session_scopes"}) +def get_users_auth_params(auth_method: str, session: Session) -> dict[int, dict[str, AuthMethod]]: + """Don't use it in public API routes""" + retval = {} + methods: list[AuthMethod] = AuthMethod.query(session=session).filter(AuthMethod.auth_method == auth_method).all() + for method in methods: + if method.user_id not in retval: + retval[method.user_id] = {} + retval[method.user_id][method.param] = method + return retval + + @user.get("", response_model=UsersGet, response_model_exclude_unset=True) async def get_users( _: UserSession = Depends(UnionAuth(scopes=["auth.user.read"], allow_none=False, auto_error=True)), @@ -69,13 +83,19 @@ async def get_users( """ Scopes: `["auth.user.read"]` """ + ## TODO: Add pagination users = User.query(session=db.session).all() result = {} result["items"] = [] + all_user_auth_params = get_users_auth_params("email", db.session) for user in users: add = { "id": user.id, - "email": user.auth_methods.email.email.value if user.auth_methods.email.email else None, + "email": ( + all_user_auth_params[user.id]["email"].value + if "email" in (all_user_auth_params.get(user.id) or []) + else None + ), } if "groups" in info: add["groups"] = [group.id for group in user.groups] diff --git a/auth_backend/routes/user_session.py b/auth_backend/routes/user_session.py index e784fa9c..aecbe4a2 100644 --- a/auth_backend/routes/user_session.py +++ b/auth_backend/routes/user_session.py @@ -23,6 +23,7 @@ UserScopes, ) from auth_backend.utils import user_session_control +from auth_backend.utils.auth_params import get_auth_params from auth_backend.utils.security import UnionAuth @@ -51,13 +52,14 @@ async def me( default=[] ), ) -> dict[str, str | int]: + auth_params = get_auth_params(session.user_id, "email", db.session) session.expires = session_expires_date() # Автопродление сессии при активности пользователя result: dict[str, str | int] = {} result = ( result | UserInfo( id=session.user_id, - email=session.user.auth_methods.email.email.value if session.user.auth_methods.email.email else None, + email=auth_params["email"].value if "email" in auth_params else None, ).model_dump() ) if "groups" in info: diff --git a/auth_backend/utils/auth_params.py b/auth_backend/utils/auth_params.py new file mode 100644 index 00000000..0f8553e8 --- /dev/null +++ b/auth_backend/utils/auth_params.py @@ -0,0 +1,15 @@ +from sqlalchemy.orm import Session + +from auth_backend.models.db import AuthMethod + + +def get_auth_params(user_id: int, auth_method: str, session: Session) -> dict[str, AuthMethod]: + retval: dict[str, AuthMethod] = {} + methods: list[AuthMethod] = ( + AuthMethod.query(session=session) + .filter(AuthMethod.user_id == user_id, AuthMethod.auth_method == auth_method) + .all() + ) + for method in methods: + retval[method.param] = method + return retval diff --git a/auth_backend/utils/user_session_control.py b/auth_backend/utils/user_session_control.py index aded8a12..e45f5f18 100644 --- a/auth_backend/utils/user_session_control.py +++ b/auth_backend/utils/user_session_control.py @@ -18,8 +18,8 @@ async def create_session( user: User, scopes_list_names: list[TypeScope] | None, - expires: datetime = None, - session_name: str = None, + expires: datetime | None = None, + session_name: str | None = None, *, db_session: DbSession, ) -> Session: diff --git a/tests/test_routes/test_change_password.py b/tests/test_routes/test_change_password.py index fef29604..0b500b96 100644 --- a/tests/test_routes/test_change_password.py +++ b/tests/test_routes/test_change_password.py @@ -3,6 +3,7 @@ from starlette import status from auth_backend.models.db import AuthMethod +from auth_backend.utils.auth_params import get_auth_params url = "/email/reset/password" @@ -18,11 +19,11 @@ def test_unprocessable_jsons_no_token(client_auth: TestClient, dbsession: Sessio ) response = client_auth.get(f"/email/approve?token={token.value}") assert response.status_code == status.HTTP_200_OK - + auth_params = get_auth_params(user_id, "email", dbsession) response = client_auth.post( f"{url}/restore", json={ - "email": token.user.auth_methods.email.email.value, + "email": auth_params["email"].value, }, ) assert response.status_code == status.HTTP_200_OK @@ -88,7 +89,7 @@ def test_unprocessable_jsons_with_token(client_auth: TestClient, dbsession: Sess assert response.status_code == status.HTTP_200_OK -def test_no_token(client_auth: TestClient, dbsession: Session, user_id: str): +def test_no_token(client_auth: TestClient, dbsession: Session, user_id: int): token = ( dbsession.query(AuthMethod) .filter( @@ -96,13 +97,14 @@ def test_no_token(client_auth: TestClient, dbsession: Session, user_id: str): ) .one() ) - response = client_auth.post(f"{url}/restore", json={"email": token.user.auth_methods.email.email.value}) + auth_params = get_auth_params(user_id, "email", dbsession) + response = client_auth.post(f"{url}/restore", json={"email": auth_params["email"].value}) assert response.status_code == status.HTTP_401_UNAUTHORIZED response = client_auth.get(f"/email/approve?token={token.value}") assert response.status_code == status.HTTP_200_OK - response = client_auth.post(f"{url}/restore", json={"email": token.user.auth_methods.email.email.value}) + response = client_auth.post(f"{url}/restore", json={"email": auth_params["email"].value}) assert response.status_code == status.HTTP_200_OK reset_token: AuthMethod = ( dbsession.query(AuthMethod) @@ -110,6 +112,7 @@ def test_no_token(client_auth: TestClient, dbsession: Session, user_id: str): .one() ) assert reset_token + auth_params = get_auth_params(user_id, "email", dbsession) response = client_auth.post( f"{url}", @@ -127,13 +130,13 @@ def test_no_token(client_auth: TestClient, dbsession: Session, user_id: str): response = client_auth.post( "/email/login", - json={"email": reset_token.user.auth_methods.email.email.value, "password": "string", "scopes": []}, + json={"email": auth_params["email"].value, "password": "string", "scopes": []}, ) assert response.status_code == status.HTTP_401_UNAUTHORIZED response = client_auth.post( "/email/login", - json={"email": reset_token.user.auth_methods.email.email.value, "password": "changedstring2", "scopes": []}, + json={"email": auth_params["email"].value, "password": "changedstring2", "scopes": []}, ) assert response.status_code == status.HTTP_200_OK @@ -177,7 +180,7 @@ def test_with_token(client_auth: TestClient, dbsession: Session, user): assert response.status_code == status.HTTP_200_OK -def test_no_token_two_requests(client_auth: TestClient, dbsession: Session, user_id: str): +def test_no_token_two_requests(client_auth: TestClient, dbsession: Session, user_id: int): token = ( dbsession.query(AuthMethod) .filter( @@ -189,7 +192,9 @@ def test_no_token_two_requests(client_auth: TestClient, dbsession: Session, user response = client_auth.get(f"/email/approve?token={token.value}") assert response.status_code == status.HTTP_200_OK - response = client_auth.post(f"{url}/restore", json={"email": token.user.auth_methods.email.email.value}) + auth_params = get_auth_params(user_id, "email", dbsession) + + response = client_auth.post(f"{url}/restore", json={"email": auth_params["email"].value}) assert response.status_code == status.HTTP_200_OK reset_token_1: AuthMethod = ( dbsession.query(AuthMethod) @@ -203,7 +208,7 @@ def test_no_token_two_requests(client_auth: TestClient, dbsession: Session, user ) assert reset_token_1 - response = client_auth.post(f"{url}/restore", json={"email": token.user.auth_methods.email.email.value}) + response = client_auth.post(f"{url}/restore", json={"email": auth_params["email"].value}) assert response.status_code == status.HTTP_200_OK reset_token_2: AuthMethod = ( dbsession.query(AuthMethod)