Класс декоратора для проверки метода разрешения на уровне


После написания слишком много if has_permission(user): заявления на головы моих методов, я решил попробовать свои силы в написании родовых достаточно декоратора, чтобы сделать это для меня. Вот что я создал:

from inspect import signature

def permissions(callback, **perm_kwargs):
    '''This class decorator is used to define permission levels for individual
    methods of the decorated class.

    Attempting to call a method named by this decorator will first invoke the
    given callback. If the callback returns True, the user is authorized and
    the method is called normally. If it returns False, a permission error is
    raised instead and the method is not called.'''

    def wrap(wrapped_class):
        class PermissionsWrapper:
            def __init__(self, *args, **kwargs):
                self.wrapped = wrapped_class(*args, **kwargs)
                self.permission_levels = {}

                for name, level in perm_kwargs.items():
                    self.wrap_method(name, level)

            def __getattr__(self, attr_name):
                attr = getattr(self.wrapped, attr_name)
                if attr_name not in self.permission_levels:
                    return attr

                def wrapper_func(*args, **kwargs):
                    user = PermissionsWrapper.unpack_user(attr, *args, **kwargs)

                    if callback(user, self.permission_levels[attr_name]):
                        attr(*args, **kwargs)
                    else:
                        raise RuntimeError('Permission Denied.')
                return wrapper_func

            @staticmethod
            def unpack_user(method, *args, **kwargs):
                try:
                    # Verify that the arguments are lexically valid.
                    bindings = signature(method).bind(*args, **kwargs)
                    return bindings.arguments['user']
                except TypeError:
                    # The arguments are invalid. Call the method with the bad
                    # arguments so that a more descriptive exception is raised.
                    method(*args, **kwargs)

            def wrap_method(self, method_name, perm_level):
                if not hasattr(wrapped_class, method_name):
                    error = 'No such method: {}'.format(method_name)
                    raise RuntimeError(error)

                method = getattr(wrapped_class, method_name)
                if not hasattr(method, '__call__'):
                    error = 'Attribute {} is not a method!'.format(method_name)
                    raise RuntimeError(error)

                method_sig = signature(method)
                if 'user' not in method_sig.parameters:
                    error = ('Method signature does not have a "user" argument!'
                         ' The user argument will have its permission level'
                         ' verified and either be allowed to use the method'
                         ', or a permission error will be thrown.')
                    raise RuntimeError(error)

                self.permission_levels[method_name] = perm_level

        return PermissionsWrapper
    return wrap

Затем вы могли бы использовать декоратор такой:

def has_permission(user, required_level):
    if the_user_meets_the_required_level_of_permissions():
        return True
    else:
        return False

@permissions(callback=has_permission,
             destroy_the_company='ADMIN_ONLY', view_profile='GUEST',
             edit_profile='LOGGED_IN', post_comment='GUEST')
class ProfileManager:
    def edit_profile(self, user, form_data):
        pass

    def view_profile(self, user):
        pass

    def post_comment(self, user, comment_text):
        pass

    def destroy_the_company(self, user):
        drop_all_database_tables()
        overwrite_backups()

На момент написания, для меня стало очевидным, что с помощью декоратора на весь класс будет более самодокументируемыми и требуют меньше набирать, чем с помощью отдельного декоратора на каждый метод. Теперь я не так уверен.

  • Может это какие-то преимущества/недостатки по сравнению с более простой за способ оформитель?
  • Есть какие-то крайние случаи, где этот декоратор не удастся?

Редактировать: исправлена декоратор путем добавления unpack_user метод и называя его в wrapper_func.



206
6
задан 1 февраля 2018 в 10:02 Источник Поделиться
Комментарии
2 ответа

Для класса более сложные, чем это, я думаю, что за способ декораторов, лучше читаются и обеспечивают лучшую документации:

class ProfileManager:

@permissions("LOGGED_IN")
def edit_profile(self, user, form_data):
pass

@permissions("GUEST")
def view_profile(self, user):
pass

@permissions("GUEST")
def post_comment(self, user, comment_text):
pass

@permissions("ADMIN")
def destroy_the_company(self, user):
drop_all_database_tables()
overwrite_backups()

Реализация этого нового permissions декоратор может быть как простой, как:

from functools import wraps
import logging

class AuthenticationErrror(RuntimeError):
pass

def permission(permission_level):
def decorator(f):
@wraps(f)
def wrapper(self, user, *args, **kwargs):
if user.has_permission(permission_level):
return f(self, user, *args, **kwargs)
else:
logging.critical("User %s tried accessing function %s without permission (needed: %s)", self.user.name, f.__name__, permission_level)
raise AuthenticationError("403: Not authorized")
return wrapper
return decorator

(непроверено)


Классе декоратор будет смысла ИМО только для настройки уровня разрешения по умолчанию для всех методов.

2
ответ дан 2 февраля 2018 в 10:02 Источник Поделиться

Механизм для получения на аргумента пользователя не работает:

@permissions(callback=print, test='ADMIN')
class Test:
def test(self, user):
pass

>>> Test().test('user')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "cr186560.py", line 31, in wrapper_func
if callback(user, self.permission_levels[attr_name]):
NameError: name 'user' is not defined

Вы проверяли этот код?

4
ответ дан 2 февраля 2018 в 10:02 Источник Поделиться