Определить, не является ли маршрут внешним / неправильным «порядком» декораторов во Flask - PullRequest
0 голосов
/ 14 ноября 2018

Поскольку декоратор @route должен зарегистрировать представление с текущим обратным вызовом, переданным декоратору, он должен быть внешним декоратором , чтобы получить правильную функцию, которая будет вызываться при обработке запроса.

Это создает возможную ситуацию, когда представление было оформлено, но поскольку декораторы расположены в неправильном порядке, функция декорирования не вызывается.Если используется для украшения представлений, которые требуют, чтобы пользователь вошел в систему, имел определенную роль или имел определенный флаг, проверка будет молча исключена.

Наше текущее исправление состоит в том, чтобы стандартным действием было отказатьдоступ к ресурсу, затем требуется декоратор, чтобы разрешить доступ.В этом случае, если декоратор не вызывается во время обработки запроса, запрос завершится неудачей.

Но есть случаи использования, когда это становится громоздким, поскольку требует декорирования всех представлений, кромете немногие, которые должны быть освобождены.Для чистой иерархической структуры это может работать, но для проверки отдельных флагов структура может усложниться.

Есть ли правильный способ обнаружить, что нас вызывают в полезном месте в иерархии декораций?Т.е. можем ли мы обнаружить, что еще не был route декоратор, примененный к функции, которую мы получаем для переноса?

# wrapped in wrong order - @require_administrator should be after @app.route
@require_administrator
@app.route('/users', methods=['GET'])

Реализован как:

def require_administrator(func):
    @functools.wraps(func)
    def has_administrator(*args, **kwargs):
        if not getattr(g, 'user') or not g.user.is_administrator:
            abort(403)

        return func(*args, **kwargs)

    return has_administrator

Здесь я хотел бы определить, переносится ли мой пользовательский декоратор после @app.route, и, следовательно, никогда не будет вызываться при запросе

Использование functools.wraps заменяет упакованную функцию новой во всех отношениях, поэтому просмотр __name__ функции, которая будет упакована, не удастся.Это также происходит на каждом этапе процесса обёртывания декоратора.

Я пробовал смотреть как traceback, так и inspect, но не нашел приличного способа определения правильности последовательности.

Обновление

Моим лучшим на данный момент лучшим решением является проверка имени вызываемой функции по набору зарегистрированных конечных точек.Но так как Route() декоратор может изменить имя конечной точки, мне придется поддерживать это и для моего декоратора в этом случае, и он будет молча проходить, если другая функция использовала то же имя конечной точки, что и текущаяfunction.

Также необходимо выполнить итерацию набора зарегистрированных конечных точек, поскольку я не смог найти простой способ проверить, существует ли только имя конечной точки (возможно, более эффективный, если попытаться создать URL с ним ипоймать исключение).

def require_administrator_checked(func):
    for rule in app.url_map.iter_rules():
        if func.__name__ == rule.endpoint:
            raise DecoratorOrderError(f"Wrapped endpoint '{rule.endpoint}' has already been registered - wrong order of decorators?")

    # as above ..

Ответы [ 2 ]

0 голосов
/ 14 ноября 2018

Я добавляю другой ответ, потому что теперь у меня есть кое-что, что является наименьшим количеством хакерства (читай: я использую inspect для чтения исходного кода данной функции вместо того, чтобы читать весь файл сам), работает через модули, и может быть повторно использован для любых других декораторов, которые всегда должны быть последними. Вам также не нужно использовать другой синтаксис для app.route, как в обновлении моего другого ответа.

Вот как это сделать (Внимание: это начальный этап закрытия):

import flask
import inspect


class DecoratorOrderError(TypeError):
    pass


def assert_last_decorator(final_decorator):
    """
    Converts a decorator so that an exception is raised when it is not the last    decorator to be used on a function.
    This only works for decorator syntax, not if somebody explicitly uses the decorator, e.g.
    final_decorator = some_other_decorator(final_decorator) will still work without an exception.

    :param final_decorator: The decorator that should be made final.
    :return: The same decorator, but it checks that it is the last one before calling the inner function.
    """
    def check_decorator_order(func):
        # Use inspect to read the code of the function
        code, _ = inspect.getsourcelines(func)
        decorators = []
        for line in code:
            if line.startswith("@"):
                decorators.append(line)
            else:
                break

        # Remove the "@", function calls, and any object calls, such as "app.route". We just want the name of the decorator function (e.g. "route")
        decorator_names_only = [dec.replace("@", "").split("(")[0].split(".")[-1] for dec in decorators]
        is_final_decorator = [final_decorator.__name__ == name for name in decorator_names_only]
        num_finals = sum(is_final_decorator)

        if num_finals > 1 or (num_finals == 1 and not is_final_decorator[0]):
            raise DecoratorOrderError(f"'{final_decorator.__name__}' is not the topmost decorator of function '{func.__name__}'")

        return func

    def handle_arguments(*args, **kwargs):
        # Used to pass the arguments to the final decorator

        def handle_function(f):
            # Which function should be decorated by the final decorator?
            return final_decorator(*args, **kwargs)(check_decorator_order(f))

        return handle_function

    return handle_arguments

Теперь вы можете заменить функцию app.route на эту функцию, примененную к функции app.route. Это важно и должно быть сделано перед любым использованием декоратора app.route, поэтому я предлагаю сделать это при создании приложения.

app = flask.Flask(__name__)
app.route = assert_last_decorator(app.route)


def require_administrator(func):
    @functools.wraps(func)
    def has_administrator(*args, **kwargs):
        print("Would check admin now")

        return func(*args, **kwargs)

    return has_administrator


@app.route("/good", methods=["GET"])  # Works
@require_administrator
def test_good():
    return "ok"

@require_administrator
@app.route("/bad", methods=["GET"])  # Raises an Exception
def test_bad():
    return "not ok"

Я полагаю, что это почти то, что вы хотели в вашем вопросе.

0 голосов
/ 14 ноября 2018

Обновление 2 : см. Другой мой ответ для более многоразового и менее хакерского решения.

Обновление : Вот решительно менее взломать решение. Тем не менее, он требует от вас использовать пользовательская функция вместо app.route. Он принимает произвольное количество декораторов и применяет их в указанном порядке, а затем проверяет, что app.route вызывается как конечная функция. Для этого необходимо использовать только этот декоратор для каждой функции.

def safe_route(rule, app, *decorators, **options):
    def _route(func):
        for decorator in decorators:
            func = decorator(func)
        return app.route(rule, **options)(func)
    return _route

Затем вы можете использовать его так:

def require_administrator(func):
    @functools.wraps(func)
    def has_administrator(*args, **kwargs):
        print("Would check admin now")

        return func(*args, **kwargs)

    return has_administrator

@safe_route("/", app, require_administrator, methods=["GET"])
def test2():
    return "foo"

test2()
print(test2.__name__)

Это печатает:

Would check admin now
foo
test2

Так что, если все поставляемые декораторы используют functools.wraps, это также сохраняет имя test2.

Старый ответ : Если у вас все в порядке с заведомо хакерским решением, вы можете провести собственную проверку, прочитав файл построчно. Вот очень грубая функция, которая делает это. Вы можете уточнить это немного, например на данный момент оно опирается на приложение под названием «приложение», в определениях функций, перед которыми есть хотя бы одна пустая строка (нормальное поведение PEP-8, но все еще может возникнуть проблема), ...

Вот полный код, который я использовал для его проверки.

import flask
import functools
from itertools import groupby


class DecoratorOrderError(TypeError):
    pass


app = flask.Flask(__name__)


def require_administrator(func):
    @functools.wraps(func)
    def has_administrator(*args, **kwargs):
        print("Would check admin now")

        return func(*args, **kwargs)

    return has_administrator


@require_administrator  # Will raise a custom exception
@app.route("/", methods=["GET"])
def test():
    return "ok"


def check_route_is_topmost_decorator():
    # Read own source
    with open(__file__) as f:
        content = [line.strip() for line in f.readlines()]

    # Split source code on line breaks
    split_by_lines = [list(group) for k, group in groupby(content, lambda x: x == "") if not k]

    # Find consecutive decorators
    decorator_groups = dict()
    for line_group in split_by_lines:
        decorators = []
        for line in line_group:
            if line.startswith("@"):
                decorators.append(line)
            elif decorators:
                decorator_groups[line] = decorators
                break
            else:
                break

    # Check if app.route is the last one (if it exists)
    for func_def, decorators in decorator_groups.items():
        is_route = [dec.startswith("@app.route") for dec in decorators]
        if sum(is_route) > 1 or (sum(is_route) == 1 and not decorators[0].startswith("@app.route")):
            raise DecoratorOrderError(f"@app.route is not the topmost decorator for '{func_def}'")


check_route_is_topmost_decorator()

Этот фрагмент выдаст вам следующую ошибку:

Traceback (most recent call last):
  File "/home/vXYZ/test_sso.py", line 51, in <module>
    check_route_is_topmost_decorator()
  File "/home/vXYZ/test_sso.py", line 48, in check_route_is_topmost_decorator
    raise DecoratorOrderError(f"@app.route is not the topmost decorator for '{func_def}'")
__main__.DecoratorOrderError: @app.route is not the topmost decorator for 'def test():'

Если вы переключаете порядок декоратора для функции test(), он просто ничего не делает.

Недостатком является то, что вы должны вызывать этот метод явно в каждом файле. Я не знаю точно, насколько это надежно, я признаю, что это довольно уродливо, и я не буду брать на себя никакой ответственности, если это сломается, но это начало! Я уверен, что должен быть лучший путь.

...