检测路线是否不是最外层/错误"order" Flask 中的装饰器



由于@route装饰器必须使用给定给装饰器的当前回调来注册视图,因此它必须是最外层的装饰器,以便在处理请求时接收要调用的正确函数。

这会造成一种可能的情况,即视图已被装饰,但由于装饰器的顺序错误,因此不会调用装饰的函数。如果用于装饰需要用户登录、具有特定角色或具有特定标志的视图,则该复选框将被忽略。

我们目前的解决方案是让标准操作拒绝访问资源,然后要求装饰器允许访问。在这种情况下,如果在处理请求时没有调用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:请参阅我的另一个答案,以获得更可重用、更少黑客攻击的解决方案。

更新:这里有一个明显不那么棘手的解决方案。但是,它要求您使用自定义函数而不是CCD_ 9。它接受任意数量的decorator,并按照给定的顺序应用它们,然后确保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()函数切换装饰器的顺序,那么它什么也不做。

一个缺点是必须在每个文件中显式调用此方法。我不知道这有多可靠,我承认它很难看,如果它坏了,我不会承担任何责任,但这只是一个开始!我相信一定有更好的办法。

我添加了另一个答案,因为现在我有了一些最不容易破解的东西(读:我使用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"

我相信这正是你在提问中想要的。

最新更新