Coverage for /usr/local/lib/python3.12/site-packages/prefect/server/utilities/user_templates.py: 27%
75 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-12-05 13:38 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-12-05 13:38 +0000
1"""Utilities to support safely rendering user-supplied templates"""
3from typing import TYPE_CHECKING, Any, Optional 1a
5from jinja2 import ChainableUndefined, nodes 1a
6from jinja2.sandbox import ImmutableSandboxedEnvironment 1a
8from prefect.logging import get_logger 1a
10if TYPE_CHECKING: 10 ↛ 11line 10 didn't jump to line 11 because the condition on line 10 was never true1a
11 import logging
13logger: "logging.Logger" = get_logger(__name__) 1a
16MAX_TEMPLATE_RANGE = 100 1a
17MAX_LOOP_COUNT = 10 1a
18MAX_NESTED_LOOP_DEPTH = 2 1a
21def _check_template_range(*args: int) -> range: 1a
22 rng = range(*args)
24 if len(rng) > MAX_TEMPLATE_RANGE:
25 raise OverflowError(
26 "Range too big. The sandbox blocks ranges larger than"
27 f" {MAX_TEMPLATE_RANGE=}."
28 )
29 return rng
32class UserTemplateEnvironment(ImmutableSandboxedEnvironment): 1a
33 def __init__(self, *args: Any, **kwargs: Any) -> None: 1a
34 super().__init__(*args, **kwargs) 1a
35 # Override the range function to limit its size
36 self.globals["range"] = _check_template_range # type: ignore 1a
39_template_environment = UserTemplateEnvironment( 1a
40 undefined=ChainableUndefined,
41 enable_async=True,
42 extensions=[
43 # Supports human-friendly rendering of dates and times
44 # https://pypi.org/project/jinja2-humanize-extension/
45 "jinja2_humanize_extension.HumanizeExtension",
46 ],
47)
49_sync_template_environment = UserTemplateEnvironment( 1a
50 undefined=ChainableUndefined,
51 enable_async=False,
52 extensions=[
53 # Supports human-friendly rendering of dates and times
54 # https://pypi.org/project/jinja2-humanize-extension/
55 "jinja2_humanize_extension.HumanizeExtension",
56 ],
57)
60class TemplateSecurityError(Exception): 1a
61 """Raised when extended validation of a template fails."""
63 def __init__(self, message: Optional[str] = None, line_number: int = 0) -> None: 1a
64 self.lineno = line_number
65 self.message = message
66 super().__init__(message)
69def register_user_template_filters(filters: dict[str, Any]) -> None: 1a
70 """Register additional filters that will be available to user templates"""
71 _template_environment.filters.update(filters)
74def validate_user_template(template: str) -> None: 1a
75 root_node = _template_environment.parse(template)
76 _validate_loop_constraints(root_node)
79def _validate_loop_constraints(root_node: nodes.Template): 1a
80 for_nodes = [node for node in root_node.find_all(nodes.For)]
82 if not for_nodes:
83 return
85 if len(for_nodes) > MAX_LOOP_COUNT:
86 raise TemplateSecurityError(
87 f"Contains {len(for_nodes)} for loops. Templates can contain no "
88 f"more than {MAX_LOOP_COUNT} for loops."
89 )
91 max_nested_depth = max(_nested_loop_depth(for_node) for for_node in for_nodes)
92 if max_nested_depth > MAX_NESTED_LOOP_DEPTH:
93 raise TemplateSecurityError(
94 f"Contains nested for loops at a depth of {max_nested_depth}. "
95 "Templates can nest for loops no more than "
96 f"{MAX_NESTED_LOOP_DEPTH} loops deep."
97 )
100def _nested_loop_depth(node: nodes.Node, depth: int = 0) -> int: 1a
101 children = [child for child in node.iter_child_nodes()]
103 if isinstance(node, nodes.For):
104 depth += 1
106 if not children:
107 return depth
109 return max(_nested_loop_depth(child, depth) for child in children)
112def matching_types_in_templates(templates: list[str], types: set[str]) -> list[str]: 1a
113 found: set[str] = set()
115 for template in templates:
116 root_node = _template_environment.parse(template)
117 for node in root_node.find_all(nodes.Name):
118 if node.ctx == "load" and node.name in types:
119 found.add(node.name)
121 return list(found)
124def maybe_template(possible: str) -> bool: 1a
125 return "{{" in possible or "{%" in possible
128async def render_user_template(template: str, context: dict[str, Any]) -> str: 1a
129 if not maybe_template(template):
130 return template
132 try:
133 loaded = _template_environment.from_string(template)
134 return await loaded.render_async(context)
135 except Exception as e:
136 logger.warning("Unhandled exception rendering template", exc_info=True)
137 return (
138 f"Failed to render template due to the following error: {e!r}\n"
139 "Template source:\n"
140 ) + template
143def render_user_template_sync(template: str, context: dict[str, Any]) -> str: 1a
144 if not maybe_template(template):
145 return template
147 try:
148 loaded = _sync_template_environment.from_string(template)
149 return loaded.render(context)
150 except Exception as e:
151 logger.warning("Unhandled exception rendering template", exc_info=True)
152 return (
153 f"Failed to render template due to the following error: {e!r}\n"
154 "Template source:\n"
155 ) + template