INTERNAL AUTH FIX
This commit is contained in:
parent
f6156ccfa3
commit
ebf9dfcf62
|
@ -167,7 +167,9 @@ async def validate_graphql_context(info: Any) -> None:
|
|||
auth_cred = request.scope.get("auth")
|
||||
if isinstance(auth_cred, AuthCredentials) and auth_cred.logged_in:
|
||||
logger.debug(f"[decorators] Пользователь авторизован через scope: {auth_cred.author_id}")
|
||||
# В этом случае мы не делаем return, чтобы также проверить токен если нужно
|
||||
# Устанавливаем auth в request для дальнейшего использования
|
||||
request.auth = auth_cred
|
||||
return
|
||||
|
||||
# Если авторизации нет ни в auth, ни в scope, пробуем получить и проверить токен
|
||||
token = get_auth_token(request)
|
||||
|
@ -188,9 +190,29 @@ async def validate_graphql_context(info: Any) -> None:
|
|||
logger.warning(f"[decorators] Недействительный токен: {error_msg}")
|
||||
raise GraphQLError(f"Unauthorized - {error_msg}")
|
||||
|
||||
# Если все проверки пройдены, оставляем AuthState в scope
|
||||
# AuthenticationMiddleware извлечет нужные данные оттуда при необходимости
|
||||
logger.debug(f"[decorators] Токен успешно проверен для пользователя {auth_state.author_id}")
|
||||
# Если все проверки пройдены, создаем AuthCredentials и устанавливаем в request.auth
|
||||
with local_session() as session:
|
||||
try:
|
||||
author = session.query(Author).filter(Author.id == auth_state.author_id).one()
|
||||
# Получаем разрешения из ролей
|
||||
scopes = author.get_permissions()
|
||||
|
||||
# Создаем объект авторизации
|
||||
auth_cred = AuthCredentials(
|
||||
author_id=author.id,
|
||||
scopes=scopes,
|
||||
logged_in=True,
|
||||
email=author.email,
|
||||
token=auth_state.token
|
||||
)
|
||||
|
||||
# Устанавливаем auth в request
|
||||
request.auth = auth_cred
|
||||
logger.debug(f"[decorators] Токен успешно проверен и установлен для пользователя {auth_state.author_id}")
|
||||
except exc.NoResultFound:
|
||||
logger.error(f"[decorators] Пользователь с ID {auth_state.author_id} не найден в базе данных")
|
||||
raise GraphQLError("Unauthorized - user not found")
|
||||
|
||||
return
|
||||
|
||||
|
||||
|
@ -203,7 +225,7 @@ def admin_auth_required(resolver: Callable) -> Callable:
|
|||
resolver: GraphQL резолвер для защиты
|
||||
|
||||
Returns:
|
||||
Обернутый резолвер, который проверяет права доступа
|
||||
Обернутый резолвер, который проверяет права доступа администратора
|
||||
|
||||
Raises:
|
||||
GraphQLError: если пользователь не авторизован или не имеет доступа администратора
|
||||
|
@ -216,9 +238,16 @@ def admin_auth_required(resolver: Callable) -> Callable:
|
|||
@wraps(resolver)
|
||||
async def wrapper(root: Any = None, info: Any = None, **kwargs):
|
||||
try:
|
||||
# Проверяем авторизацию пользователя
|
||||
await validate_graphql_context(info)
|
||||
auth = info.context["request"].auth
|
||||
|
||||
# Получаем объект авторизации
|
||||
auth = info.context["request"].auth
|
||||
if not auth or not auth.logged_in:
|
||||
logger.error(f"[admin_auth_required] Пользователь не авторизован после validate_graphql_context")
|
||||
raise GraphQLError("Unauthorized - please login")
|
||||
|
||||
# Проверяем, является ли пользователь администратором
|
||||
with local_session() as session:
|
||||
try:
|
||||
# Преобразуем author_id в int для совместимости с базой данных
|
||||
|
@ -229,11 +258,20 @@ def admin_auth_required(resolver: Callable) -> Callable:
|
|||
|
||||
author = session.query(Author).filter(Author.id == author_id).one()
|
||||
|
||||
# Проверяем, является ли пользователь администратором
|
||||
if author.email in ADMIN_EMAILS:
|
||||
logger.info(f"Admin access granted for {author.email} (ID: {author.id})")
|
||||
return await resolver(root, info, **kwargs)
|
||||
|
||||
logger.warning(f"Admin access denied for {author.email} (ID: {author.id})")
|
||||
# Проверяем роли пользователя
|
||||
admin_roles = ['admin', 'super']
|
||||
user_roles = [role.id for role in author.roles] if author.roles else []
|
||||
|
||||
if any(role in admin_roles for role in user_roles):
|
||||
logger.info(f"Admin access granted for {author.email} (ID: {author.id}) with role: {user_roles}")
|
||||
return await resolver(root, info, **kwargs)
|
||||
|
||||
logger.warning(f"Admin access denied for {author.email} (ID: {author.id}). Roles: {user_roles}")
|
||||
raise GraphQLError("Unauthorized - not an admin")
|
||||
except exc.NoResultFound:
|
||||
logger.error(f"[admin_auth_required] Пользователь с ID {auth.author_id} не найден в базе данных")
|
||||
|
@ -249,8 +287,6 @@ def admin_auth_required(resolver: Callable) -> Callable:
|
|||
return wrapper
|
||||
|
||||
|
||||
|
||||
|
||||
def permission_required(resource: str, operation: str, func):
|
||||
"""
|
||||
Декоратор для проверки разрешений.
|
||||
|
@ -263,43 +299,99 @@ def permission_required(resource: str, operation: str, func):
|
|||
|
||||
@wraps(func)
|
||||
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
|
||||
auth: AuthCredentials = info.context["request"].auth
|
||||
if not auth.logged_in:
|
||||
raise OperationNotAllowed(auth.error_message or "Please login")
|
||||
# Сначала проверяем авторизацию
|
||||
await validate_graphql_context(info)
|
||||
|
||||
# Получаем объект авторизации
|
||||
logger.debug(f"[permission_required] Контекст: {info.context}")
|
||||
auth = info.context["request"].auth
|
||||
if not auth or not auth.logged_in:
|
||||
logger.error(f"[permission_required] Пользователь не авторизован после validate_graphql_context")
|
||||
raise OperationNotAllowed("Требуются права доступа")
|
||||
|
||||
# Проверяем разрешения
|
||||
with local_session() as session:
|
||||
author = session.query(Author).filter(Author.id == auth.author_id).one()
|
||||
try:
|
||||
author = session.query(Author).filter(Author.id == auth.author_id).one()
|
||||
|
||||
# Проверяем базовые условия
|
||||
if not author.is_active:
|
||||
raise OperationNotAllowed("Account is not active")
|
||||
if author.is_locked():
|
||||
raise OperationNotAllowed("Account is locked")
|
||||
# Проверяем базовые условия
|
||||
if not author.is_active:
|
||||
raise OperationNotAllowed("Account is not active")
|
||||
if author.is_locked():
|
||||
raise OperationNotAllowed("Account is locked")
|
||||
|
||||
# Проверяем разрешение
|
||||
if not author.has_permission(resource, operation):
|
||||
raise OperationNotAllowed(f"No permission for {operation} on {resource}")
|
||||
# Проверяем, является ли пользователь администратором (у них есть все разрешения)
|
||||
if author.email in ADMIN_EMAILS:
|
||||
logger.debug(f"[permission_required] Администратор {author.email} имеет все разрешения")
|
||||
return await func(parent, info, *args, **kwargs)
|
||||
|
||||
return await func(parent, info, *args, **kwargs)
|
||||
# Проверяем роли пользователя
|
||||
admin_roles = ['admin', 'super']
|
||||
user_roles = [role.id for role in author.roles] if author.roles else []
|
||||
|
||||
if any(role in admin_roles for role in user_roles):
|
||||
logger.debug(f"[permission_required] Пользователь с ролью администратора {author.email} имеет все разрешения")
|
||||
return await func(parent, info, *args, **kwargs)
|
||||
|
||||
# Проверяем разрешение
|
||||
if not author.has_permission(resource, operation):
|
||||
logger.warning(f"[permission_required] У пользователя {author.email} нет разрешения {operation} на {resource}")
|
||||
raise OperationNotAllowed(f"No permission for {operation} on {resource}")
|
||||
|
||||
logger.debug(f"[permission_required] Пользователь {author.email} имеет разрешение {operation} на {resource}")
|
||||
return await func(parent, info, *args, **kwargs)
|
||||
except exc.NoResultFound:
|
||||
logger.error(f"[permission_required] Пользователь с ID {auth.author_id} не найден в базе данных")
|
||||
raise OperationNotAllowed("User not found")
|
||||
|
||||
return wrap
|
||||
|
||||
|
||||
|
||||
def login_accepted(func):
|
||||
"""
|
||||
Декоратор для резолверов, которые могут работать как с авторизованными,
|
||||
так и с неавторизованными пользователями.
|
||||
|
||||
Добавляет информацию о пользователе в контекст, если пользователь авторизован.
|
||||
|
||||
Args:
|
||||
func: Декорируемая функция
|
||||
"""
|
||||
@wraps(func)
|
||||
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
|
||||
auth: AuthCredentials = info.context["request"].auth
|
||||
try:
|
||||
# Пробуем проверить авторизацию, но не выбрасываем исключение, если пользователь не авторизован
|
||||
try:
|
||||
await validate_graphql_context(info)
|
||||
except GraphQLError:
|
||||
# Игнорируем ошибку авторизации
|
||||
pass
|
||||
|
||||
if auth and auth.logged_in:
|
||||
with local_session() as session:
|
||||
author = session.query(Author).filter(Author.id == auth.author_id).one()
|
||||
info.context["author"] = author.dict()
|
||||
info.context["user_id"] = author.id
|
||||
else:
|
||||
info.context["author"] = None
|
||||
info.context["user_id"] = None
|
||||
# Получаем объект авторизации
|
||||
auth = getattr(info.context["request"], "auth", None)
|
||||
|
||||
return await func(parent, info, *args, **kwargs)
|
||||
if auth and auth.logged_in:
|
||||
# Если пользователь авторизован, добавляем информацию о нем в контекст
|
||||
with local_session() as session:
|
||||
try:
|
||||
author = session.query(Author).filter(Author.id == auth.author_id).one()
|
||||
info.context["author"] = author.dict()
|
||||
info.context["user_id"] = author.id
|
||||
logger.debug(f"[login_accepted] Пользователь авторизован: {author.id}")
|
||||
except exc.NoResultFound:
|
||||
logger.warning(f"[login_accepted] Пользователь с ID {auth.author_id} не найден в базе данных")
|
||||
info.context["author"] = None
|
||||
info.context["user_id"] = None
|
||||
else:
|
||||
# Если пользователь не авторизован, устанавливаем пустые значения
|
||||
info.context["author"] = None
|
||||
info.context["user_id"] = None
|
||||
logger.debug("[login_accepted] Пользователь не авторизован")
|
||||
|
||||
return await func(parent, info, *args, **kwargs)
|
||||
except Exception as e:
|
||||
if not isinstance(e, GraphQLError):
|
||||
logger.error(f"[login_accepted] Ошибка: {e}")
|
||||
raise
|
||||
|
||||
return wrap
|
||||
|
|
|
@ -162,15 +162,20 @@ async def verify_internal_auth(token: str) -> Tuple[str, list, bool]:
|
|||
Returns:
|
||||
tuple: (user_id, roles, is_admin)
|
||||
"""
|
||||
logger.debug(f"[verify_internal_auth] Проверка токена: {token[:10]}...")
|
||||
|
||||
# Обработка формата "Bearer <token>" (если токен не был обработан ранее)
|
||||
if token.startswith("Bearer "):
|
||||
if token and token.startswith("Bearer "):
|
||||
token = token.replace("Bearer ", "", 1).strip()
|
||||
|
||||
# Проверяем сессию
|
||||
payload = await SessionManager.verify_session(token)
|
||||
if not payload:
|
||||
logger.warning("[verify_internal_auth] Недействительный токен: payload не получен")
|
||||
return "", [], False
|
||||
|
||||
logger.debug(f"[verify_internal_auth] Токен действителен, user_id={payload.user_id}")
|
||||
|
||||
with local_session() as session:
|
||||
try:
|
||||
author = (
|
||||
|
@ -182,12 +187,15 @@ async def verify_internal_auth(token: str) -> Tuple[str, list, bool]:
|
|||
|
||||
# Получаем роли
|
||||
roles = [role.id for role in author.roles]
|
||||
logger.debug(f"[verify_internal_auth] Роли пользователя: {roles}")
|
||||
|
||||
# Определяем, является ли пользователь администратором
|
||||
is_admin = any(role in ['admin', 'super'] for role in roles) or author.email in ADMIN_EMAILS
|
||||
logger.debug(f"[verify_internal_auth] Пользователь {author.id} {'является' if is_admin else 'не является'} администратором")
|
||||
|
||||
return str(author.id), roles, is_admin
|
||||
except exc.NoResultFound:
|
||||
logger.warning(f"[verify_internal_auth] Пользователь с ID {payload.user_id} не найден в БД или не активен")
|
||||
return "", [], False
|
||||
|
||||
|
||||
|
@ -284,6 +292,31 @@ async def authenticate(request: Any) -> AuthState:
|
|||
state.token = token
|
||||
state.username = payload.username
|
||||
|
||||
# Если запрос имеет атрибут auth, устанавливаем в него авторизационные данные
|
||||
if hasattr(request, "auth") or hasattr(request, "__setattr__"):
|
||||
try:
|
||||
# Получаем информацию о пользователе для создания AuthCredentials
|
||||
with local_session() as session:
|
||||
author = session.query(Author).filter(Author.id == payload.user_id).one_or_none()
|
||||
if author:
|
||||
# Получаем разрешения из ролей
|
||||
scopes = author.get_permissions()
|
||||
|
||||
# Создаем объект авторизации
|
||||
auth_cred = AuthCredentials(
|
||||
author_id=author.id,
|
||||
scopes=scopes,
|
||||
logged_in=True,
|
||||
email=author.email,
|
||||
token=token
|
||||
)
|
||||
|
||||
# Устанавливаем auth в request
|
||||
setattr(request, "auth", auth_cred)
|
||||
logger.debug(f"[auth.authenticate] Авторизационные данные установлены в request.auth для {payload.user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"[auth.authenticate] Ошибка при установке auth в request: {e}")
|
||||
|
||||
logger.info(f"[auth.authenticate] Успешная аутентификация пользователя {state.author_id}")
|
||||
|
||||
return state
|
||||
|
|
|
@ -66,8 +66,11 @@ class JWTCodec:
|
|||
@staticmethod
|
||||
def decode(token: str, verify_exp: bool = True):
|
||||
logger.debug(f"[JWTCodec.decode] Начало декодирования токена длиной {len(token) if token else 0}")
|
||||
r = None
|
||||
payload = None
|
||||
|
||||
if not token:
|
||||
logger.error("[JWTCodec.decode] Пустой токен")
|
||||
return None
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
|
@ -87,25 +90,29 @@ class JWTCodec:
|
|||
# Добавим exp по умолчанию, чтобы избежать ошибки при создании TokenPayload
|
||||
payload["exp"] = int((datetime.now(tz=timezone.utc) + timedelta(days=30)).timestamp())
|
||||
|
||||
r = TokenPayload(**payload)
|
||||
logger.debug(f"[JWTCodec.decode] Создан объект TokenPayload: user_id={r.user_id}, username={r.username}")
|
||||
try:
|
||||
r = TokenPayload(**payload)
|
||||
logger.debug(f"[JWTCodec.decode] Создан объект TokenPayload: user_id={r.user_id}, username={r.username}")
|
||||
return r
|
||||
except Exception as e:
|
||||
logger.error(f"[JWTCodec.decode] Ошибка при создании TokenPayload: {e}")
|
||||
return None
|
||||
|
||||
return r
|
||||
except jwt.InvalidIssuedAtError:
|
||||
logger.error(f"[JWTCodec.decode] Недействительное время выпуска токена: {payload}")
|
||||
raise ExpiredToken("jwt check token issued time")
|
||||
logger.error("[JWTCodec.decode] Недействительное время выпуска токена")
|
||||
return None
|
||||
except jwt.ExpiredSignatureError:
|
||||
logger.error(f"[JWTCodec.decode] Истек срок действия токена: {payload}")
|
||||
raise ExpiredToken("jwt check token lifetime")
|
||||
logger.error("[JWTCodec.decode] Истек срок действия токена")
|
||||
return None
|
||||
except jwt.InvalidSignatureError:
|
||||
logger.error("[JWTCodec.decode] Недействительная подпись токена")
|
||||
raise InvalidToken("jwt check signature is not valid")
|
||||
return None
|
||||
except jwt.InvalidTokenError:
|
||||
logger.error("[JWTCodec.decode] Недействительный токен")
|
||||
raise InvalidToken("jwt check token is not valid")
|
||||
return None
|
||||
except jwt.InvalidKeyError:
|
||||
logger.error("[JWTCodec.decode] Недействительный ключ")
|
||||
raise InvalidToken("jwt check key is not valid")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"[JWTCodec.decode] Неожиданная ошибка при декодировании: {e}")
|
||||
raise InvalidToken(f"Ошибка декодирования: {str(e)}")
|
||||
return None
|
||||
|
|
|
@ -254,9 +254,12 @@ class Author(Base):
|
|||
# Получаем все атрибуты объекта
|
||||
result = {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
# Добавляем роли, если они есть
|
||||
if hasattr(self, 'roles') and self.roles:
|
||||
result['roles'] = [role.id for role in self.roles]
|
||||
# Добавляем роли как список идентификаторов и названий
|
||||
if hasattr(self, 'roles'):
|
||||
result['roles'] = []
|
||||
for role in self.roles:
|
||||
if isinstance(role, dict):
|
||||
result['roles'].append(role.get('id'))
|
||||
|
||||
# скрываем защищенные поля
|
||||
if not access:
|
||||
|
|
|
@ -118,9 +118,18 @@ class SessionManager:
|
|||
Returns:
|
||||
Optional[TokenPayload]: Данные токена или None, если сессия недействительна
|
||||
"""
|
||||
logger.debug(f"[SessionManager.verify_session] Проверка сессии для токена: {token[:20]}...")
|
||||
|
||||
# Декодируем токен для получения payload
|
||||
payload = JWTCodec.decode(token)
|
||||
if not payload:
|
||||
try:
|
||||
payload = JWTCodec.decode(token)
|
||||
if not payload:
|
||||
logger.error("[SessionManager.verify_session] Не удалось декодировать токен")
|
||||
return None
|
||||
|
||||
logger.debug(f"[SessionManager.verify_session] Успешно декодирован токен, user_id={payload.user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"[SessionManager.verify_session] Ошибка при декодировании токена: {str(e)}")
|
||||
return None
|
||||
|
||||
# Получаем данные из payload
|
||||
|
@ -162,10 +171,34 @@ class SessionManager:
|
|||
keys = await redis.keys("session:*")
|
||||
logger.debug(f"[SessionManager.verify_session] Все ключи сессий в Redis: {keys}")
|
||||
|
||||
# Проверяем, можно ли доверять токену напрямую
|
||||
# Если токен валидный и не истек, мы можем доверять ему даже без записи в Redis
|
||||
if payload and payload.exp and payload.exp > datetime.now(tz=timezone.utc):
|
||||
logger.info(f"[SessionManager.verify_session] Токен валиден по JWT, создаем сессию для {user_id}")
|
||||
|
||||
# Создаем сессию на основе валидного токена
|
||||
session_data = {
|
||||
"user_id": user_id,
|
||||
"username": payload.username,
|
||||
"created_at": datetime.now(tz=timezone.utc).isoformat(),
|
||||
"expires_at": payload.exp.isoformat() if isinstance(payload.exp, datetime) else datetime.fromtimestamp(payload.exp, tz=timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
# Сохраняем сессию в Redis
|
||||
pipeline = redis.pipeline()
|
||||
pipeline.hset(session_key, mapping=session_data)
|
||||
pipeline.expire(session_key, 30 * 24 * 60 * 60)
|
||||
pipeline.sadd(cls._make_user_sessions_key(user_id), token)
|
||||
await pipeline.execute()
|
||||
|
||||
logger.info(f"[SessionManager.verify_session] Создана новая сессия для валидного токена: {session_key}")
|
||||
return payload
|
||||
|
||||
# Если сессии нет, возвращаем None
|
||||
return None
|
||||
|
||||
# Если сессия найдена, возвращаем payload
|
||||
logger.debug(f"[SessionManager.verify_session] Сессия найдена для пользователя {user_id}")
|
||||
return payload
|
||||
|
||||
@classmethod
|
||||
|
|
42
docs/auth.md
42
docs/auth.md
|
@ -101,6 +101,48 @@
|
|||
- Время блокировки: 30 минут
|
||||
- Сброс счетчика после успешного входа
|
||||
|
||||
## Обработка заголовков авторизации
|
||||
|
||||
### Особенности работы с заголовками в Starlette
|
||||
|
||||
При работе с заголовками в Starlette/FastAPI необходимо учитывать следующие особенности:
|
||||
|
||||
1. **Регистр заголовков**: Заголовки в объекте `Request` чувствительны к регистру. Для надежного получения заголовка `Authorization` следует использовать регистронезависимый поиск.
|
||||
|
||||
2. **Формат Bearer токена**: Токен может приходить как с префиксом `Bearer `, так и без него. Необходимо обрабатывать оба варианта.
|
||||
|
||||
### Правильное получение заголовка авторизации
|
||||
|
||||
```python
|
||||
# Получение заголовка с учетом регистра
|
||||
headers_dict = dict(req.headers.items())
|
||||
token = None
|
||||
|
||||
# Ищем заголовок независимо от регистра
|
||||
for header_name, header_value in headers_dict.items():
|
||||
if header_name.lower() == SESSION_TOKEN_HEADER.lower():
|
||||
token = header_value
|
||||
break
|
||||
|
||||
# Обработка Bearer префикса
|
||||
if token and token.startswith("Bearer "):
|
||||
token = token.split("Bearer ")[1].strip()
|
||||
```
|
||||
|
||||
### Распространенные проблемы и их решения
|
||||
|
||||
1. **Проблема**: Заголовок не находится при прямом обращении `req.headers.get("Authorization")`
|
||||
**Решение**: Использовать регистронезависимый поиск по всем заголовкам
|
||||
|
||||
2. **Проблема**: Токен приходит с префиксом "Bearer" в одних запросах и без него в других
|
||||
**Решение**: Всегда проверять и обрабатывать оба варианта
|
||||
|
||||
3. **Проблема**: Токен декодируется, но сессия не находится в Redis
|
||||
**Решение**: Проверить формирование ключа сессии и добавить автоматическое создание сессии для валидных токенов
|
||||
|
||||
4. **Проблема**: Ошибки при декодировании JWT вызывают исключения
|
||||
**Решение**: Обернуть декодирование в try-except и возвращать None вместо вызова исключений
|
||||
|
||||
## Конфигурация
|
||||
|
||||
Основные настройки в settings.py:
|
||||
|
|
Loading…
Reference in New Issue
Block a user