From 8a7233cc65467d5f35db19cbd0d32c7b0d1b3f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D0=BC=D0=B0=D0=BD=20=D0=98=D0=B3=D0=BE=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87=20=D0=94=D1=8C=D1=8F=D0=BA=D0=BE=D0=B2?= Date: Thu, 21 Nov 2024 21:02:18 +0000 Subject: [PATCH 1/8] Authentic basic --- auth_backend/auth_plugins/__init__.py | 2 + auth_backend/auth_plugins/authentic.py | 289 +++++++++++++++++++++++++ 2 files changed, 291 insertions(+) create mode 100644 auth_backend/auth_plugins/authentic.py diff --git a/auth_backend/auth_plugins/__init__.py b/auth_backend/auth_plugins/__init__.py index 8dfbc700..480835a1 100644 --- a/auth_backend/auth_plugins/__init__.py +++ b/auth_backend/auth_plugins/__init__.py @@ -1,6 +1,7 @@ from auth_backend.auth_method import AUTH_METHODS, AuthPluginMeta from .airflow import AirflowOuterAuth +from .authentic import AuthenticAuth from .coder import CoderOuterAuth from .email import Email from .github import GithubAuth @@ -31,6 +32,7 @@ "VkAuth", "GithubAuth", "KeycloakAuth", + "AuthenticAuth", # Провайдеры синхронизации паролей "PostgresOuterAuth", "CoderOuterAuth", diff --git a/auth_backend/auth_plugins/authentic.py b/auth_backend/auth_plugins/authentic.py new file mode 100644 index 00000000..fc2942b3 --- /dev/null +++ b/auth_backend/auth_plugins/authentic.py @@ -0,0 +1,289 @@ +import logging +from typing import Any +from urllib.parse import quote + +import aiohttp +import jwt +from event_schema.auth import UserLogin +from fastapi import Depends +from fastapi.background import BackgroundTasks +from fastapi_sqlalchemy import db +from pydantic import BaseModel, Field + +from auth_backend.auth_method import AuthPluginMeta, OauthMeta, Session +from auth_backend.auth_method.outer import ConnectionIssue, OuterAuthMeta +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.schemas.types.scopes import Scope +from auth_backend.settings import Settings +from auth_backend.utils.security import UnionAuth + + +AUTH_METHOD_ID_PARAM_NAME = 'user_id' +logger = logging.getLogger(__name__) + + +class AuthenticSettings(Settings): + AUTHENTIC_ROOT_URL: str | None = None + AUTHENTIC_REDIRECT_URL: str | None = 'https://app.test.profcomff.com/auth/oauth-authorized/authentic' + AUTHENTIC_CLIENT_ID: str | None = None + AUTHENTIC_CLIENT_SECRET: str | None = None + AUTHENTIC_CERT: str | None = None + AUTHENTIC_TOKEN: str | None = None + + +class AuthenticAuth(OauthMeta, OuterAuthMeta): + """Вход в приложение по аккаунту Authentic""" + + prefix = '/authentic' + tags = ['authentic'] + settings = AuthenticSettings() + + class OauthResponseSchema(BaseModel): + code: str | None = None + id_token: str | None = Field(default=None, help="Authentic JWT token identifier") + scopes: list[Scope] | None = None + session_name: str | None = None + + @classmethod + async def __get_token(cls, code: str) -> dict[str]: + async with aiohttp.ClientSession() as session: + async with session.post( + f'{cls.settings.AUTHENTIC_ROOT_URL}/application/o/token/', + data={ + "grant_type": "authorization_code", + "code": code, + "client_id": cls.settings.AUTHENTIC_CLIENT_ID, + "client_secret": cls.settings.AUTHENTIC_CLIENT_SECRET, + "redirect_uri": cls.settings.AUTHENTIC_REDIRECT_URL, + }, + headers={"Accept": "application/x-www-form-urlencoded"}, + ) as response: + token_result = await response.json() + logger.debug(token_result) + return token_result + + @classmethod + async def __decode_token(cls, token: str): + id_token_info = jwt.decode( + token, + str(cls.settings.AUTHENTIC_CERT), + ['RS256'], + {'verify_signature': True}, + ) + logger.debug(id_token_info) + return id_token_info + + @classmethod + def __check_response(cls, token_result: dict[str]): + + if 'access_token' not in token_result: + raise OauthAuthFailed( + 'Invalid credentials for authentic account', + 'Неверные данные для входа в аккаунт authentic', + ) + if 'id_token' not in token_result: + raise OauthAuthFailed( + 'No oauth scope granted from authentic', + 'Не получены данные о пользователе authentic', + ) + + @classmethod + def __get_old_user(cls, user_session: UserSession | None): + if user_session is None: + return None + return {'user_id': user_session.user_id} + + @classmethod + async def _register( + cls, + user_inp: OauthResponseSchema, + background_tasks: BackgroundTasks, + user_session: UserSession | None = Depends(UnionAuth(auto_error=True, scopes=[], allow_none=True)), + ) -> Session: + """Создает аккаунт или привязывает существующий""" + id_token = user_inp.id_token + + # Получаем параметры токена пользователя + if id_token is None: + # Если id_token не передали в register запросе – надо запросить его по коду + if user_inp.code is None: + raise OauthAuthFailed( + 'Nor code or id_token provided', + 'Не передано ни кода авторизации, ни токена идентификации' + ) + token_result = await cls.__get_token(user_inp.code) + cls.__check_response(token_result) + # acess_token_info = await cls.__decode_token(token_result['access_token']) + id_token_info = await cls.__decode_token(token_result['id_token']) + else: + # id_token может быть передан непосредственно из ручки входа + # Это происходит, если пользователь пытался залогиниться, но аккаунта не существовало + id_token_info = await cls.__decode_token(id_token) + + # Субъект передается как id пользователя + # Это настройка делается в Authentic, по умолчанию хэш + authentic_id = id_token_info['sub'] + + # Получаем пользователей, у которых уже есть такой authentic_id + user = await cls._get_user(AUTH_METHOD_ID_PARAM_NAME, authentic_id, db_session=db.session) + + if user is not None: + # Существует пользователь, уже имеющий привязку к этому методу аутентификации + raise AlreadyExists(User, user.id) + + # Создаем нового пользователя или берем существующего, в зависимости от авторизации + if user_session is None: + user = await cls._create_user(db_session=db.session) if user_session is None else user_session.user + else: + user = user_session.user + # Добавляем пользователю метод входа + authentic_id = cls.create_auth_method_param( + AUTH_METHOD_ID_PARAM_NAME, authentic_id, user.id, db_session=db.session + ) + + # Отправляем обновления пользовательских данных в userdata api + background_tasks.add_task( + get_kafka_producer().produce, + cls.settings.KAFKA_USER_LOGIN_TOPIC_NAME, + AuthenticAuth.generate_kafka_key(user.id), + await AuthenticAuth._convert_data_to_userdata_format(id_token_info), + ) + + # Формируем diff пользователя для обработки другими методами входа + new_user = { + 'user_id': user.id, + cls.get_name(): {AUTH_METHOD_ID_PARAM_NAME: authentic_id.value} + } + old_user = cls.__get_old_user(user_session) + await AuthPluginMeta.user_updated(new_user, old_user) + + # Возвразаем сессию пользрвателя + return await cls._create_session( + user, user_inp.scopes, db_session=db.session, session_name=user_inp.session_name + ) + + @classmethod + async def _login(cls, user_inp: OauthResponseSchema, background_tasks: BackgroundTasks) -> Session: + """Вход в пользователя с помощью аккаунта Authentic""" + id_token = user_inp.id_token + + # Получаем параметры токена пользователя + if id_token is None: + # Если id_token не передали в register запросе – надо запросить его по коду + if user_inp.code is None: + raise OauthAuthFailed( + 'Nor code or id_token provided', + 'Не передано ни кода авторизации, ни токена идентификации' + ) + token_result = await cls.__get_token(user_inp.code) + cls.__check_response(token_result) + # acess_token_info = await cls.__decode_token(token_result['access_token']) + id_token = token_result['id_token'] + id_token_info = await cls.__decode_token(id_token) + else: + # id_token может быть передан непосредственно из ручки входа + # Это происходит, если пользователь пытался залогиниться, но аккаунта не существовало + id_token_info = await cls.__decode_token(id_token) + + # Субъект передается как id пользователя + # Это настройка делается в Authentic, по умолчанию хэш + authentic_id = id_token_info['sub'] + + # Получаем пользователей, у которых уже есть такой authentic_id + # Получаем для этого пользователя сессию или, если не существует, направляем на регистрацию + user = await cls._get_user(AUTH_METHOD_ID_PARAM_NAME, authentic_id, db_session=db.session) + if not user: + raise OauthAuthFailed( + 'No users found for authentic account', + 'Пользователь с данным аккаунтом Authentic не найден', + id_token, + ) + user_session = await cls._create_session( + user, user_inp.scopes, db_session=db.session, session_name=user_inp.session_name + ) + + # Отправляем обновления пользовательских данных в userdata api + background_tasks.add_task( + get_kafka_producer().produce, + cls.settings.KAFKA_USER_LOGIN_TOPIC_NAME, + AuthenticAuth.generate_kafka_key(user.id), + await AuthenticAuth._convert_data_to_userdata_format(id_token_info), + ) + + # Формируем diff пользователя для обработки другими методами входа + new_user = {'user_id': user.id} + old_user = cls.__get_old_user(user_session) + await AuthPluginMeta.user_updated(new_user, old_user) + + # Возвразаем сессию пользрвателя + return user_session + + @classmethod + async def _redirect_url(cls): + """URL на который происходит редирект после завершения входа на стороне провайдера""" + return OauthMeta.UrlSchema(url=cls.settings.AUTHENTIC_REDIRECT_URL) + + @classmethod + async def _auth_url(cls): + """URL на который происходит редирект из приложения для авторизации на стороне провайдера""" + return OauthMeta.UrlSchema( + url=f'{cls.settings.AUTHENTIC_ROOT_URL}/application/o/authorize/' + f'?client_id={cls.settings.AUTHENTIC_CLIENT_ID}' + f'&redirect_uri={quote(cls.settings.AUTHENTIC_REDIRECT_URL)}' + f'&scope=openid,tvoyff-manage-password' + f'&response_type=code' + ) + + @classmethod + async def _convert_data_to_userdata_format(cls, data: dict[str, Any]) -> UserLogin: + result = { + "items": [ + {"category": "Личная информация", "param": "Полное имя", "value": data.get("name", "").strip()}, + {"category": "Контакты", "param": "Электронная почта", "value": data.get("email")}, + ], + "source": cls.get_name(), + } + return cls.userdata_process_empty_strings(UserLogin.model_validate(result)) + + @classmethod + async def _get_username(cls, user_id: int) -> AuthMethod: + auth_params = cls.get_auth_method_params(user_id, session=db.session) + authentic_user_id = auth_params.get(AUTH_METHOD_ID_PARAM_NAME) + if not authentic_user_id: + logger.debug("User user_id=%d have no authentic_user_id in outer service %s", user_id, cls.get_name()) + return + return authentic_user_id + + @classmethod + async def _is_outer_user_exists(cls, id: str) -> bool: + """Проверяет наличие пользователя в Authentic""" + logger.debug("_is_outer_user_exists class=%s started", cls.get_name()) + async with aiohttp.ClientSession() as session: + async with session.get( + str(cls.settings.AUTHENTIC_ROOT_URL).removesuffix('/') + f'/core/users/{id}/', + headers={'authorization': "Bearer " + cls.settings.AUTHENTIC_TOKEN, 'Accept': 'application/json'}, + ) as response: + if not response.ok: + raise ConnectionIssue(response.text) + res: dict[str] = await response.json() + return res.get('id') == id + + @classmethod + async def _update_outer_user_password(cls, id: str, password: str): + """Устанавливает пользователю новый пароль в Authentic""" + logger.debug("_update_outer_user_password class=%s started", cls.get_name()) + res = False + async with aiohttp.ClientSession() as session: + async with session.put( + str(cls.settings.AUTHENTIC_ROOT_URL).removesuffix('/') + f'/core/users/{id}/set_password/', + headers={'authorization': "Bearer " + cls.settings.AUTHENTIC_TOKEN, 'Accept': 'application/json'}, + json={'password': password}, + ) as response: + res: dict[str] = response.ok + logger.debug("_update_outer_user_password class=%s response %s", cls.get_name(), str(response.status)) + if res: + logger.info("User %s updated in %s", id, cls.get_name()) + else: + logger.error("User %s can't be updated in %s. Error: %s", id, cls.get_name(), res) From 5c6a66dcc069315278be6b72a33761032d427962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D0=BC=D0=B0=D0=BD=20=D0=98=D0=B3=D0=BE=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87=20=D0=94=D1=8C=D1=8F=D0=BA=D0=BE=D0=B2?= Date: Sat, 23 Nov 2024 15:25:24 +0000 Subject: [PATCH 2/8] outer --- auth_backend/auth_method/outer.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/auth_backend/auth_method/outer.py b/auth_backend/auth_method/outer.py index 26b1ab02..35af90ec 100644 --- a/auth_backend/auth_method/outer.py +++ b/auth_backend/auth_method/outer.py @@ -105,7 +105,7 @@ async def _update_outer_user_password(cls, username: str, password: str): raise NotImplementedError() @classmethod - async def __get_username(cls, user_id: int) -> AuthMethod: + async def _get_username(cls, user_id: int) -> AuthMethod: auth_params = cls.get_auth_method_params(user_id, session=db.session) username = auth_params.get("username") if not username: @@ -133,7 +133,7 @@ async def on_user_update(cls, new_user: dict[str, Any], old_user: dict[str, Any] logger.debug("%s not password, closing", cls.get_name()) return - username = await cls.__get_username(user_id) + username = await cls._get_username(user_id) if not username: # У пользователя нет имени во внешнем сервисе logger.debug("%s not username, closing", cls.get_name()) @@ -167,7 +167,7 @@ async def _get_link( """ if cls.get_scope() not in (s.name for s in request_user.scopes) and request_user.id != user_id: raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authorized") - username = await cls.__get_username(user_id) + username = await cls._get_username(user_id) if not username: raise UserNotLinked(user_id) return GetOuterAccount(username=username.value) @@ -185,7 +185,7 @@ async def _link( """ if cls.post_scope() not in (s.name for s in request_user.scopes): raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authorized") - username = await cls.__get_username(user_id) + username = await cls._get_username(user_id) if username: raise UserLinkingConflict(user_id) param = cls.create_auth_method_param("username", outer.username, user_id, db_session=db.session) @@ -205,7 +205,7 @@ async def _unlink( """ if cls.delete_scope() not in (s.name for s in request_user.scopes): raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authorized") - username = await cls.__get_username(user_id) + username = await cls._get_username(user_id) if not username: raise UserNotLinked(user_id) username.is_deleted = True From f4db4434847c5d73b0cc52895b1f46afdbb7c196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D0=BC=D0=B0=D0=BD=20=D0=98=D0=B3=D0=BE=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87=20=D0=94=D1=8C=D1=8F=D0=BA=D0=BE=D0=B2?= Date: Sat, 23 Nov 2024 16:17:02 +0000 Subject: [PATCH 3/8] get from config --- auth_backend/auth_plugins/authentic.py | 58 +++++++++++++++++++++----- requirements.txt | 1 + 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/auth_backend/auth_plugins/authentic.py b/auth_backend/auth_plugins/authentic.py index fc2942b3..023590ab 100644 --- a/auth_backend/auth_plugins/authentic.py +++ b/auth_backend/auth_plugins/authentic.py @@ -4,11 +4,12 @@ import aiohttp import jwt +from aiocache import cached from event_schema.auth import UserLogin from fastapi import Depends from fastapi.background import BackgroundTasks from fastapi_sqlalchemy import db -from pydantic import BaseModel, Field +from pydantic import AnyHttpUrl, BaseModel, Field from auth_backend.auth_method import AuthPluginMeta, OauthMeta, Session from auth_backend.auth_method.outer import ConnectionIssue, OuterAuthMeta @@ -25,11 +26,11 @@ class AuthenticSettings(Settings): - AUTHENTIC_ROOT_URL: str | None = None - AUTHENTIC_REDIRECT_URL: str | None = 'https://app.test.profcomff.com/auth/oauth-authorized/authentic' + AUTHENTIC_ROOT_URL: AnyHttpUrl | None = None + AUTHENTIC_OIDC_CONFIGURATION_URL: AnyHttpUrl | None = None + AUTHENTIC_REDIRECT_URL: AnyHttpUrl | None = 'https://app.test.profcomff.com/auth/oauth-authorized/authentic' AUTHENTIC_CLIENT_ID: str | None = None AUTHENTIC_CLIENT_SECRET: str | None = None - AUTHENTIC_CERT: str | None = None AUTHENTIC_TOKEN: str | None = None @@ -46,11 +47,47 @@ class OauthResponseSchema(BaseModel): scopes: list[Scope] | None = None session_name: str | None = None + @classmethod + @cached() + async def __get_configuration(cls): + if not cls.settings.AUTHENTIC_OIDC_CONFIGURATION_URL: + raise OauthAuthFailed( + 'Error in OIDC configuration', + 'Ошибка конфигурации OIDC', + 500, + ) + async with aiohttp.ClientSession() as session: + async with session.get( + str(cls.settings.AUTHENTIC_OIDC_CONFIGURATION_URL), + ) as response: + res = await response.json() + logger.debug(res) + return res + + @classmethod + @cached() + async def __get_jwks_options(cls) -> list[dict[str]]: + config = await cls.__get_configuration() + if 'jwks_uri' not in config: + logger.error('No OIDC JWKS config: %s', str(config)) + raise OauthAuthFailed( + 'Error in OIDC configuration', + 'Ошибка конфигурации OIDC', + 500, + ) + jwks_uri = config['jwks_uri'] + async with aiohttp.ClientSession() as session: + async with session.get(jwks_uri) as response: + res = await response.json() + logger.debug(res) + return res.get('keys', []) + @classmethod async def __get_token(cls, code: str) -> dict[str]: + token_url = (await cls.__get_configuration())['token_endpoint'] async with aiohttp.ClientSession() as session: async with session.post( - f'{cls.settings.AUTHENTIC_ROOT_URL}/application/o/token/', + token_url, data={ "grant_type": "authorization_code", "code": code, @@ -66,18 +103,16 @@ async def __get_token(cls, code: str) -> dict[str]: @classmethod async def __decode_token(cls, token: str): + jwks = jwt.PyJWKSet(await cls.__get_jwks_options()) + algorithms = (await cls.__get_configuration()).get('id_token_signing_alg_values_supported', []) id_token_info = jwt.decode( - token, - str(cls.settings.AUTHENTIC_CERT), - ['RS256'], - {'verify_signature': True}, + token, jwks, algorithms, {'verify_signature': True} ) logger.debug(id_token_info) return id_token_info @classmethod def __check_response(cls, token_result: dict[str]): - if 'access_token' not in token_result: raise OauthAuthFailed( 'Invalid credentials for authentic account', @@ -228,8 +263,9 @@ async def _redirect_url(cls): @classmethod async def _auth_url(cls): """URL на который происходит редирект из приложения для авторизации на стороне провайдера""" + authorize_url = (await cls.__get_configuration())['authorization_endpoint'] return OauthMeta.UrlSchema( - url=f'{cls.settings.AUTHENTIC_ROOT_URL}/application/o/authorize/' + url=f'{authorize_url}' f'?client_id={cls.settings.AUTHENTIC_CLIENT_ID}' f'&redirect_uri={quote(cls.settings.AUTHENTIC_REDIRECT_URL)}' f'&scope=openid,tvoyff-manage-password' diff --git a/requirements.txt b/requirements.txt index db19b3a6..ad745ddd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,7 @@ pydantic-settings pytest-asyncio confluent-kafka event-schema-profcomff +aiocache # Google Auth Method google-api-python-client From 78cc3cc1ae485d35d926c83f238e461e648f0b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D0=BC=D0=B0=D0=BD=20=D0=98=D0=B3=D0=BE=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87=20=D0=94=D1=8C=D1=8F=D0=BA=D0=BE=D0=B2?= Date: Sat, 23 Nov 2024 23:54:38 +0000 Subject: [PATCH 4/8] Fix update password --- auth_backend/auth_plugins/authentic.py | 54 +++++++++++++++++++++----- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/auth_backend/auth_plugins/authentic.py b/auth_backend/auth_plugins/authentic.py index 023590ab..1fa57cc8 100644 --- a/auth_backend/auth_plugins/authentic.py +++ b/auth_backend/auth_plugins/authentic.py @@ -34,7 +34,7 @@ class AuthenticSettings(Settings): AUTHENTIC_TOKEN: str | None = None -class AuthenticAuth(OauthMeta, OuterAuthMeta): +class AuthenticAuth(OauthMeta): """Вход в приложение по аккаунту Authentic""" prefix = '/authentic' @@ -66,7 +66,7 @@ async def __get_configuration(cls): @classmethod @cached() - async def __get_jwks_options(cls) -> list[dict[str]]: + async def __get_jwks_options(cls) -> dict[str, list[dict[str]]]: config = await cls.__get_configuration() if 'jwks_uri' not in config: logger.error('No OIDC JWKS config: %s', str(config)) @@ -80,7 +80,7 @@ async def __get_jwks_options(cls) -> list[dict[str]]: async with session.get(jwks_uri) as response: res = await response.json() logger.debug(res) - return res.get('keys', []) + return res @classmethod async def __get_token(cls, code: str) -> dict[str]: @@ -103,10 +103,10 @@ async def __get_token(cls, code: str) -> dict[str]: @classmethod async def __decode_token(cls, token: str): - jwks = jwt.PyJWKSet(await cls.__get_jwks_options()) + jwks = jwt.PyJWKSet.from_dict(await cls.__get_jwks_options()) algorithms = (await cls.__get_configuration()).get('id_token_signing_alg_values_supported', []) id_token_info = jwt.decode( - token, jwks, algorithms, {'verify_signature': True} + token, jwks.keys[0], algorithms, {'verify_signature': True}, audience=cls.settings.AUTHENTIC_CLIENT_ID ) logger.debug(id_token_info) return id_token_info @@ -283,6 +283,41 @@ async def _convert_data_to_userdata_format(cls, data: dict[str, Any]) -> UserLog } return cls.userdata_process_empty_strings(UserLogin.model_validate(result)) + # Обновление пароля пользователя Authentic при обновлении пароля Auth API + @classmethod + async def on_user_update(cls, new_user: dict[str, Any], old_user: dict[str, Any] | None = None): + """Произвести действия на обновление пользователя, в т.ч. обновление в других провайдерах + + Описания входных параметров соответствует параметрам `AuthMethodMeta.user_updated`. + """ + # logger.debug("on_user_update class=%s started, new_user=%s, old_user=%s", cls.get_name(), new_user, old_user) + if not new_user or not old_user: + # Пользователь был только что создан или удален + # Тут не будет дополнительных методов + logger.debug("%s not new_user or not old_user, closing", cls.get_name()) + return + + user_id = new_user.get("user_id") + password = new_user.get("email", {}).get("password") + if not password: + # В этом событии пароль не обновлялся, ничего не делаем + logger.debug("%s not password, closing", cls.get_name()) + return + + username = await cls._get_username(user_id) + if not username: + # У пользователя нет имени во внешнем сервисе + logger.debug("%s not username, closing", cls.get_name()) + return + + if await cls._is_outer_user_exists(username.value): + logger.debug("%s user exists, changing password", cls.get_name()) + await cls._update_outer_user_password(username.value, password) + else: + # Мы не нашли этого пользователя во внешнем сервисе + logger.error("Attention! Authentic user not exists") + logger.debug("on_user_update class=%s finished", cls.get_name()) + @classmethod async def _get_username(cls, user_id: int) -> AuthMethod: auth_params = cls.get_auth_method_params(user_id, session=db.session) @@ -298,13 +333,14 @@ async def _is_outer_user_exists(cls, id: str) -> bool: logger.debug("_is_outer_user_exists class=%s started", cls.get_name()) async with aiohttp.ClientSession() as session: async with session.get( - str(cls.settings.AUTHENTIC_ROOT_URL).removesuffix('/') + f'/core/users/{id}/', + str(cls.settings.AUTHENTIC_ROOT_URL).removesuffix('/') + f'/api/v3/core/users/{id}/', headers={'authorization': "Bearer " + cls.settings.AUTHENTIC_TOKEN, 'Accept': 'application/json'}, ) as response: if not response.ok: raise ConnectionIssue(response.text) res: dict[str] = await response.json() - return res.get('id') == id + logger.debug(res) + return str(res.get('pk')) == id @classmethod async def _update_outer_user_password(cls, id: str, password: str): @@ -312,8 +348,8 @@ async def _update_outer_user_password(cls, id: str, password: str): logger.debug("_update_outer_user_password class=%s started", cls.get_name()) res = False async with aiohttp.ClientSession() as session: - async with session.put( - str(cls.settings.AUTHENTIC_ROOT_URL).removesuffix('/') + f'/core/users/{id}/set_password/', + async with session.post( + str(cls.settings.AUTHENTIC_ROOT_URL).removesuffix('/') + f'/api/v3/core/users/{id}/set_password/', headers={'authorization': "Bearer " + cls.settings.AUTHENTIC_TOKEN, 'Accept': 'application/json'}, json={'password': password}, ) as response: From f7ab4feae8a37db9849105cf9696d0b0e7c957e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D0=BC=D0=B0=D0=BD=20=D0=98=D0=B3=D0=BE=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87=20=D0=94=D1=8C=D1=8F=D0=BA=D0=BE=D0=B2?= Date: Sat, 23 Nov 2024 23:58:08 +0000 Subject: [PATCH 5/8] New deploy vars --- .github/workflows/build_and_publish.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/build_and_publish.yml b/.github/workflows/build_and_publish.yml index 7f7e248a..22539e7a 100644 --- a/.github/workflows/build_and_publish.yml +++ b/.github/workflows/build_and_publish.yml @@ -123,6 +123,12 @@ jobs: --env MAILU_AUTH_BASE_URL='${{ vars.MAILU_AUTH_BASE_URL }}' \ --env MAILU_AUTH_API_KEY='${{ secrets.MAILU_AUTH_API_KEY }}' \ --env POSTGRES_AUTH_DB_DSN='${{ secrets.POSTGRES_AUTH_DB_DSN }}' \ + --env AUTHENTIC_ROOT_URL='${{ vars.AUTHENTIC_ROOT_URL }}' \ + --env AUTHENTIC_OIDC_CONFIGURATION_URL='${{ vars.AUTHENTIC_OIDC_CONFIGURATION_URL }}' \ + --env AUTHENTIC_REDIRECT_URL='${{ vars.AUTHENTIC_REDIRECT_URL }}' \ + --env AUTHENTIC_CLIENT_ID='${{ secrets.AUTHENTIC_CLIENT_ID }}' \ + --env AUTHENTIC_CLIENT_SECRET='${{ secrets.AUTHENTIC_CLIENT_SECRET }}' \ + --env AUTHENTIC_TOKEN='${{ secrets.AUTHENTIC_TOKEN }}' \ --env ENCRYPTION_KEY='${{ secrets.ENCRYPTION_KEY }}' \ --env KAFKA_DSN='${{ secrets.KAFKA_DSN }}' \ --env KAFKA_LOGIN='${{ secrets.KAFKA_LOGIN }}' \ @@ -207,6 +213,12 @@ jobs: --env MAILU_AUTH_BASE_URL='${{ vars.MAILU_AUTH_BASE_URL }}' \ --env MAILU_AUTH_API_KEY='${{ secrets.MAILU_AUTH_API_KEY }}' \ --env POSTGRES_AUTH_DB_DSN='${{ secrets.POSTGRES_AUTH_DB_DSN }}' \ + --env AUTHENTIC_ROOT_URL='${{ vars.AUTHENTIC_ROOT_URL }}' \ + --env AUTHENTIC_OIDC_CONFIGURATION_URL='${{ vars.AUTHENTIC_OIDC_CONFIGURATION_URL }}' \ + --env AUTHENTIC_REDIRECT_URL='${{ vars.AUTHENTIC_REDIRECT_URL }}' \ + --env AUTHENTIC_CLIENT_ID='${{ secrets.AUTHENTIC_CLIENT_ID }}' \ + --env AUTHENTIC_CLIENT_SECRET='${{ secrets.AUTHENTIC_CLIENT_SECRET }}' \ + --env AUTHENTIC_TOKEN='${{ secrets.AUTHENTIC_TOKEN }}' \ --env ENCRYPTION_KEY='${{ secrets.ENCRYPTION_KEY }}' \ --env KAFKA_DSN='${{ secrets.KAFKA_DSN }}' \ --env KAFKA_LOGIN='${{ secrets.KAFKA_LOGIN }}' \ From 6a1d2535d3010b55e6526598f3025293f8ad9cb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D0=BC=D0=B0=D0=BD=20=D0=98=D0=B3=D0=BE=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87=20=D0=94=D1=8C=D1=8F=D0=BA=D0=BE=D0=B2?= Date: Sun, 24 Nov 2024 00:42:21 +0000 Subject: [PATCH 6/8] Style --- auth_backend/auth_plugins/authentic.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/auth_backend/auth_plugins/authentic.py b/auth_backend/auth_plugins/authentic.py index 1fa57cc8..722df787 100644 --- a/auth_backend/auth_plugins/authentic.py +++ b/auth_backend/auth_plugins/authentic.py @@ -12,7 +12,7 @@ from pydantic import AnyHttpUrl, BaseModel, Field from auth_backend.auth_method import AuthPluginMeta, OauthMeta, Session -from auth_backend.auth_method.outer import ConnectionIssue, OuterAuthMeta +from auth_backend.auth_method.outer import ConnectionIssue 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 @@ -146,7 +146,7 @@ async def _register( if user_inp.code is None: raise OauthAuthFailed( 'Nor code or id_token provided', - 'Не передано ни кода авторизации, ни токена идентификации' + 'Не передано ни кода авторизации, ни токена идентификации', ) token_result = await cls.__get_token(user_inp.code) cls.__check_response(token_result) @@ -189,7 +189,7 @@ async def _register( # Формируем diff пользователя для обработки другими методами входа new_user = { 'user_id': user.id, - cls.get_name(): {AUTH_METHOD_ID_PARAM_NAME: authentic_id.value} + cls.get_name(): {AUTH_METHOD_ID_PARAM_NAME: authentic_id.value}, } old_user = cls.__get_old_user(user_session) await AuthPluginMeta.user_updated(new_user, old_user) @@ -210,7 +210,7 @@ async def _login(cls, user_inp: OauthResponseSchema, background_tasks: Backgroun if user_inp.code is None: raise OauthAuthFailed( 'Nor code or id_token provided', - 'Не передано ни кода авторизации, ни токена идентификации' + 'Не передано ни кода авторизации, ни токена идентификации', ) token_result = await cls.__get_token(user_inp.code) cls.__check_response(token_result) From 58526399e89b00a34520052cce5eaed5c3b43509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D0=BC=D0=B0=D0=BD=20=D0=98=D0=B3=D0=BE=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87=20=D0=94=D1=8C=D1=8F=D0=BA=D0=BE=D0=B2?= Date: Sun, 24 Nov 2024 00:44:51 +0000 Subject: [PATCH 7/8] revert outer changes --- auth_backend/auth_method/outer.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/auth_backend/auth_method/outer.py b/auth_backend/auth_method/outer.py index 35af90ec..26b1ab02 100644 --- a/auth_backend/auth_method/outer.py +++ b/auth_backend/auth_method/outer.py @@ -105,7 +105,7 @@ async def _update_outer_user_password(cls, username: str, password: str): raise NotImplementedError() @classmethod - async def _get_username(cls, user_id: int) -> AuthMethod: + async def __get_username(cls, user_id: int) -> AuthMethod: auth_params = cls.get_auth_method_params(user_id, session=db.session) username = auth_params.get("username") if not username: @@ -133,7 +133,7 @@ async def on_user_update(cls, new_user: dict[str, Any], old_user: dict[str, Any] logger.debug("%s not password, closing", cls.get_name()) return - username = await cls._get_username(user_id) + username = await cls.__get_username(user_id) if not username: # У пользователя нет имени во внешнем сервисе logger.debug("%s not username, closing", cls.get_name()) @@ -167,7 +167,7 @@ async def _get_link( """ if cls.get_scope() not in (s.name for s in request_user.scopes) and request_user.id != user_id: raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authorized") - username = await cls._get_username(user_id) + username = await cls.__get_username(user_id) if not username: raise UserNotLinked(user_id) return GetOuterAccount(username=username.value) @@ -185,7 +185,7 @@ async def _link( """ if cls.post_scope() not in (s.name for s in request_user.scopes): raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authorized") - username = await cls._get_username(user_id) + username = await cls.__get_username(user_id) if username: raise UserLinkingConflict(user_id) param = cls.create_auth_method_param("username", outer.username, user_id, db_session=db.session) @@ -205,7 +205,7 @@ async def _unlink( """ if cls.delete_scope() not in (s.name for s in request_user.scopes): raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authorized") - username = await cls._get_username(user_id) + username = await cls.__get_username(user_id) if not username: raise UserNotLinked(user_id) username.is_deleted = True From 28332abdc7b152a3a0ab999f3a74ed5c312c4912 Mon Sep 17 00:00:00 2001 From: Dyakov Roman Date: Sun, 24 Nov 2024 11:49:10 +0300 Subject: [PATCH 8/8] Minor fixes --- auth_backend/auth_plugins/authentic.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/auth_backend/auth_plugins/authentic.py b/auth_backend/auth_plugins/authentic.py index 722df787..3f1ac3ad 100644 --- a/auth_backend/auth_plugins/authentic.py +++ b/auth_backend/auth_plugins/authentic.py @@ -150,7 +150,6 @@ async def _register( ) token_result = await cls.__get_token(user_inp.code) cls.__check_response(token_result) - # acess_token_info = await cls.__decode_token(token_result['access_token']) id_token_info = await cls.__decode_token(token_result['id_token']) else: # id_token может быть передан непосредственно из ручки входа @@ -170,7 +169,7 @@ async def _register( # Создаем нового пользователя или берем существующего, в зависимости от авторизации if user_session is None: - user = await cls._create_user(db_session=db.session) if user_session is None else user_session.user + user = await cls._create_user(db_session=db.session) else: user = user_session.user # Добавляем пользователю метод входа @@ -214,7 +213,6 @@ async def _login(cls, user_inp: OauthResponseSchema, background_tasks: Backgroun ) token_result = await cls.__get_token(user_inp.code) cls.__check_response(token_result) - # acess_token_info = await cls.__decode_token(token_result['access_token']) id_token = token_result['id_token'] id_token_info = await cls.__decode_token(id_token) else: @@ -290,7 +288,7 @@ async def on_user_update(cls, new_user: dict[str, Any], old_user: dict[str, Any] Описания входных параметров соответствует параметрам `AuthMethodMeta.user_updated`. """ - # logger.debug("on_user_update class=%s started, new_user=%s, old_user=%s", cls.get_name(), new_user, old_user) + logger.debug("on_user_update class=%s started, new_user=%s, old_user=%s", cls.get_name(), new_user, old_user) if not new_user or not old_user: # Пользователь был только что создан или удален # Тут не будет дополнительных методов @@ -353,7 +351,7 @@ async def _update_outer_user_password(cls, id: str, password: str): headers={'authorization': "Bearer " + cls.settings.AUTHENTIC_TOKEN, 'Accept': 'application/json'}, json={'password': password}, ) as response: - res: dict[str] = response.ok + res = response.ok logger.debug("_update_outer_user_password class=%s response %s", cls.get_name(), str(response.status)) if res: logger.info("User %s updated in %s", id, cls.get_name())