py.lib.aw_web_tools
9:9c734271dcf2 Browse Files
.. 1.202411.2 + Поддержка Authelia
.hgignore setup.py src/aw_web_tools/authelia_helper.py src/aw_web_tools/id_helper.py
1.1 --- a/.hgignore Sun Nov 03 19:25:24 2024 +0300 1.2 +++ b/.hgignore Sat Nov 09 16:45:06 2024 +0300 1.3 @@ -1,6 +1,7 @@ 1.4 syntax: glob 1.5 .idea/* 1.6 .e/* 1.7 +_env/* 1.8 build/* 1.9 dist/* 1.10 -*.egg-info/* 1.11 \ No newline at end of file 1.12 +*.egg-info/*
2.1 --- a/setup.py Sun Nov 03 19:25:24 2024 +0300 2.2 +++ b/setup.py Sat Nov 09 16:45:06 2024 +0300 2.3 @@ -2,7 +2,7 @@ 2.4 2.5 setup( 2.6 name='aw_web_tools', 2.7 - version='1.202411.1', 2.8 + version='1.202411.2', 2.9 packages=['aw_web_tools'], 2.10 package_dir={'aw_web_tools': 'src/aw_web_tools'}, 2.11 description='Инструментарий для Web-разработки'
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 3.2 +++ b/src/aw_web_tools/authelia_helper.py Sat Nov 09 16:45:06 2024 +0300 3.3 @@ -0,0 +1,96 @@ 3.4 +# coding: utf-8 3.5 +"""\ 3.6 +Поддержка работы с системой авторизации [authelia](https://www.authelia.com/) 3.7 +""" 3.8 +import bottle 3.9 +from typing import Optional, List 3.10 +from collections import namedtuple 3.11 + 3.12 +from . import Error 3.13 + 3.14 + 3.15 +# Константы 3.16 +HEADER_USER = 'Remote-User' # Заголовок по умолчанию, в котором система аутентификации передаёт имя УЗ пользователя 3.17 +HEADER_GROUPS = 'Remote-Groups' # Заголовок по умолчанию, в котором система аутентификации передаёт группы пользовтеля 3.18 +HEADER_EMAIL = 'Remote-Email' # Заголовок по умолчанию, в котором система аутентификации передаёт e-mail пользователя 3.19 +HEADER_NAME = 'Remote-Name' # Заголовок по умолчанию, в котором система аутентификации передаёт имя пользователя 3.20 + 3.21 + 3.22 +class AHError(Error): 3.23 + """\ 3.24 + Базовый класс исключений модуля адаптера Authelia 3.25 + """ 3.26 + 3.27 + 3.28 +class AHAuthError(AHError): 3.29 + """\ 3.30 + Проблемы с авторизацией пользователя 3.31 + """ 3.32 + def __init__(self): 3.33 + super().__init__('Пользователь не авторизован') 3.34 + 3.35 + 3.36 +AHUser = namedtuple('AHUser', ['uname', 'groups', 'email', 'name']) 3.37 + 3.38 + 3.39 +class AutheliaHelper(object): 3.40 + def __init__(self, 3.41 + group_filter: Optional[List[str]] = None, 3.42 + header_user: str = HEADER_USER, 3.43 + header_groups: str = HEADER_GROUPS, 3.44 + header_email: str = HEADER_EMAIL, 3.45 + header_name: str = HEADER_NAME 3.46 + ): 3.47 + """\ 3.48 + :param group_filter: Фильтр передаваемый системой аутентификации групп, которые интересны нам. Ели ``None`` 3.49 + группы, передаваемые в заголовке не фильтруются 3.50 + :param header_user: Имя заголовка с именем УЗ пользователя. 3.51 + :param header_groups: Имя заголовка с группами пользователя 3.52 + :param header_name: Имя заголовка с именем пользователя 3.53 + """ 3.54 + self.header_user = header_user 3.55 + self.header_groups = header_groups 3.56 + self.header_email = header_email 3.57 + self.header_name = header_name 3.58 + 3.59 + self.group_filter = None 3.60 + self.group_filter_set = None 3.61 + if group_filter is not None: 3.62 + self.group_filter = {} 3.63 + for i in group_filter: 3.64 + self.group_filter[i.lower()] = i 3.65 + 3.66 + self.group_filter_set = set(self.group_filter.keys()) 3.67 + 3.68 + def __call__(self, request: bottle.BaseRequest = bottle.request) -> AHUser: 3.69 + """\ 3.70 + Обработка запроса. Если заголовки системы авторизации присутствуют, информацией из них заполняется 3.71 + объект ``AHUser``. Если данные не найдены, то будет возбуждено исключение. 3.72 + 3.73 + :param request: Обрабатываемый запрос 3.74 + :returns: Объект ``AHUser`` с данным авторизации. 3.75 + """ 3.76 + 3.77 + uname = request.get_header(self.header_user) 3.78 + email = request.get_header(self.header_email) 3.79 + name = request.get_header(self.header_name) 3.80 + groups = request.get_header(self.header_groups) 3.81 + 3.82 + if uname is None: 3.83 + raise AHAuthError() 3.84 + 3.85 + if groups is not None: 3.86 + groups = tuple(map(lambda x: x.stip(), groups.split(','))) 3.87 + if self.group_filter is not None: 3.88 + _groups = [] 3.89 + _buf = set(map(lambda x: x.lower(), groups)) 3.90 + 3.91 + for grp in _buf & self.group_filter_set: 3.92 + try: 3.93 + _groups.append(self.group_filter[grp]) 3.94 + 3.95 + except KeyError: 3.96 + AHError(f'Ошибка в фильтре групп. Для ключа "{grp}" не нашлось значения, хотя оно присутствует ' 3.97 + f'в множестве возможных значений.') 3.98 + 3.99 + return AHUser(uname=uname, groups=groups, name=name, email=email)
4.1 --- a/src/aw_web_tools/id_helper.py Sun Nov 03 19:25:24 2024 +0300 4.2 +++ b/src/aw_web_tools/id_helper.py Sat Nov 09 16:45:06 2024 +0300 4.3 @@ -5,10 +5,12 @@ 4.4 import bottle 4.5 from string import ascii_letters, digits 4.6 from typing import Optional, Dict, Iterable, Any 4.7 + 4.8 from . import btle_tools as tools 4.9 from . import Error 4.10 from . import jwt_helper 4.11 from .cookie import Cookie 4.12 +from .authelia_helper import AHAuthError, AutheliaHelper, AHUser 4.13 4.14 4.15 SESSION_TIMEOUT = 86400 # Продолжительность сессии по умолчанию. 4.16 @@ -229,10 +231,12 @@ 4.17 def __init__(self, 4.18 app_name: str, 4.19 sign_secret: str, 4.20 + authelia_helper: Optional[AutheliaHelper] = None, 4.21 sess_timeout: int = SESSION_TIMEOUT 4.22 ): 4.23 """\ 4.24 :param app_name: Имя приложения. Допускаются цифры символы латиницы и знак ``-`` 4.25 + :param authelia_helper: Объект AutheliaHelper с описанием системы аутентификации, если используем Authelia 4.26 :param sign_secret: Секрет, которым будет подписываться JWT. 4.27 :param sess_timeout: Время жизни сессии пользователя. 4.28 """ 4.29 @@ -244,6 +248,7 @@ 4.30 self.cookie_name = f'X-ID-{self.app_name}' 4.31 self.sess_timeout = sess_timeout 4.32 self.jwt_helper = jwt.JWTHelper(sign_secret) 4.33 + self.ah = authelia_helper 4.34 4.35 # Свойства необходимые для работы в качестве плагина Bottle 4.36 self.name = 'IDHelper' 4.37 @@ -300,24 +305,39 @@ 4.38 """ 4.39 sid = tools.get_cookie(self.cookie_name) 4.40 if sid is None: 4.41 - raise IDNotFound() 4.42 + if self.ah is None: 4.43 + IDNotFound() 4.44 4.45 - try: 4.46 - uid_raw = self.jwt_helper.decode(sid, check_timeout=True) 4.47 - uid = UID.from_dict(uid_raw) 4.48 + else: 4.49 + try: 4.50 + _ahuser = self.ah(request=request) 4.51 + uid = UID(uname=_ahuser.uname, acc_tags=_ahuser.groups, user_meta={ 4.52 + 'email': _ahuser.email, 4.53 + 'name': _ahuser.name, 4.54 + }) 4.55 4.56 - fp = tools.get_session_fingerprint(uid.get_fp_id(), request=request) 4.57 + uid.to_env(request=request) 4.58 + 4.59 + except AHAuthError: 4.60 + raise IDNotFound() 4.61 4.62 - if fp != uid_raw.get('fp'): 4.63 - raise IDCheckError('Проверка подписи сессии не прошла') 4.64 + else: 4.65 + try: 4.66 + uid_raw = self.jwt_helper.decode(sid, check_timeout=True) 4.67 + uid = UID.from_dict(uid_raw) 4.68 4.69 - uid.to_env(request=request) 4.70 + fp = tools.get_session_fingerprint(uid.get_fp_id(), request=request) 4.71 4.72 - except (jwt.JWTAuthError, UIDError) as e: 4.73 - raise IDCheckError(f'Ошибка проверки ID: {e}') 4.74 + if fp != uid_raw.get('fp'): 4.75 + raise IDCheckError('Проверка подписи сессии не прошла') 4.76 + 4.77 + uid.to_env(request=request) 4.78 4.79 - except (jwt.JWTError, IDError) as e: 4.80 - raise IDError(f'Ошибка в обработке ID запроса: {e}') 4.81 + except (jwt.JWTAuthError, UIDError) as e: 4.82 + raise IDCheckError(f'Ошибка проверки ID: {e}') 4.83 + 4.84 + except (jwt.JWTError, IDError) as e: 4.85 + raise IDError(f'Ошибка в обработке ID запроса: {e}') 4.86 4.87 def need_id(self): 4.88 """\