py.lib.aw_web_tools

Yohn Y. 2024-11-09 Parent:b9fd029be707 Child:74f5377d83ab

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          """\