检测路径是否不是Flask中的装饰器的最外层/错误的“顺序”

时间:2018-11-14 10:02:34

标签: python flask python-decorators

由于@route装饰器必须使用赋予装饰器的当前回调来注册视图,因此has to be the outermost decorator会收到在处理请求时要调用的正确函数。

这会导致装饰视图的可能情况,但是由于装饰器的顺序错误,因此装饰函数不会被调用。如果用于装饰需要用户登录,具有特定角色或具有特定标志的视图,则该检查将被忽略。

我们当前的解决方法是使标准操作是拒绝对资源的访问,然后要求装饰者允许访问。在这种情况下,如果处理请求时未调用装饰器,则请求将失败。

但是在某些用例中,这变得很麻烦,因为它要求您装饰所有视图,除了少数应豁免的视图。对于纯分层布局,这可能会起作用,但是对于检查单个标志,结构可能会变得复杂。

是否存在正确的方法来检测是否在装饰性层次结构中的某个有用位置调用了我们?即我们是否可以检测到尚未将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__将会失败。在装饰器包装过程的每个步骤中也会发生这种情况。

我尝试同时查看tracebackinspect,但是还没有找到确定该顺序是否正确的任何体面的方法。

更新

我目前最好的解决方案是对照已注册的端点集检查调用的函数名。但是,由于Route()装饰器可以更改端点的名称,因此在这种情况下,我也必须为装饰器支持该名称,并且如果其他函数使用的端点名称与当前功能。

它还必须迭代一组已注册的端点,因为我无法找到一种简单的方法来检查是否仅存在端点名称(通过尝试使用它构建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 :(得分:2)

更新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的名称。

旧答案: 如果您可以接受公认的hack-y解决方案,则可以通过逐行读取文件来进行自己的检查。这是执行此操作的一个非常粗糙的函数。您可以对此进行一些改进,例如目前,它依赖于名为“ app”的应用, 函数定义之前至少有一个空行(正常的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()函数,则它只会执行任何操作。

一个缺点是您必须在每个文件中显式调用此方法。 我不完全知道这有多可靠,我承认这很丑陋,如果出现问题,我将不承担任何责任,但这只是一个开始!我相信肯定有更好的方法。

答案 1 :(得分:1)

我要添加另一个答案,因为现在我拥有一些最少的技巧(阅读:我正在使用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"

我相信这几乎是您在问题中想要的。