Skip to content

Commit 7323491

Browse files
committed
Simplified implementation of configuration variables
1 parent aa8b485 commit 7323491

File tree

3 files changed

+29
-94
lines changed

3 files changed

+29
-94
lines changed

netjsonconfig/backends/base.py

-23
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,3 @@
1-
from jinja2.exceptions import SecurityError
2-
from jinja2.sandbox import SandboxedEnvironment
3-
4-
5-
class ConfigSandbox(SandboxedEnvironment):
6-
intercepted_binops = frozenset(SandboxedEnvironment.default_binop_table.keys())
7-
intercepted_unops = frozenset(SandboxedEnvironment.default_unop_table.keys())
8-
9-
def __init__(self, *args, **kwargs):
10-
super(ConfigSandbox, self).__init__(*args, **kwargs)
11-
self.globals = {}
12-
self.tests = {}
13-
self.filters = {}
14-
self.block_start_string = '{=##'
15-
self.block_end_string = '##=}'
16-
17-
def call_binop(self, context, operator, left, right):
18-
raise SecurityError('binary operator {0} not allowed'.format(operator))
19-
20-
def call_unop(self, context, operator, left):
21-
raise SecurityError('unary operator {0} not allowed'.format(operator))
22-
23-
241
class BaseRenderer(object):
252
"""
263
Renderers are used to generate specific configuration blocks.

netjsonconfig/backends/openwrt/openwrt.py

+17-35
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@
77

88
import six
99
from jinja2 import Environment, PackageLoader
10-
from jinja2.exceptions import SecurityError
1110
from jsonschema import validate
1211
from jsonschema.exceptions import ValidationError as JsonSchemaError
1312

1413
from . import renderers
1514
from ...exceptions import ValidationError
16-
from ...utils import merge_config
17-
from ..base import ConfigSandbox
15+
from ...utils import evaluate_vars, merge_config, var_pattern
1816
from .schema import DEFAULT_FILE_MODE, schema
1917

2018

@@ -45,11 +43,10 @@ def __init__(self, config, templates=[], context={}):
4543
if 'type' not in config:
4644
config.update({'type': 'DeviceConfiguration'})
4745
self.config = self._merge_config(config, templates)
48-
self.context = context
46+
self.config = self._evaluate_vars(self.config, context)
4947
self.env = Environment(loader=PackageLoader('netjsonconfig.backends.openwrt',
5048
'templates'),
5149
trim_blocks=True)
52-
self.sandbox = ConfigSandbox()
5350

5451
def _load(self, config):
5552
""" loads config from string or dict """
@@ -77,6 +74,18 @@ def _merge_config(self, config, templates):
7774
return merge_config(base_config, config)
7875
return config
7976

77+
def _evaluate_vars(self, config, context):
78+
""" evaluates configuration variables """
79+
# return immediately if context is empty
80+
if not context:
81+
return config
82+
# return immediately if no variables are found
83+
netjson = self.json(validate=False)
84+
if var_pattern.search(netjson) is None:
85+
return config
86+
# only if variables are found perform evaluation
87+
return evaluate_vars(config, context)
88+
8089
def render(self, files=True):
8190
"""
8291
Converts the configuration dictionary into the native OpenWRT UCI format.
@@ -101,9 +110,6 @@ def render(self, files=True):
101110
files_output = self._render_files()
102111
if files_output:
103112
output += files_output.replace('\n\n\n', '\n\n') # max 3 \n
104-
# configuration variables
105-
if self.context:
106-
output = self._render_context(output)
107113
return output
108114

109115
def _render_files(self):
@@ -125,38 +131,13 @@ def _render_files(self):
125131
output += file_output
126132
return output
127133

128-
def _render_context(self, input_):
129-
"""
130-
Evaluates configuration variables passed in ``context`` arg
131-
132-
:param input_: string containing rendered configuration
133-
:returns: string containing modified input_
134-
"""
135-
# disable jinja blocks
136-
block_start = self.sandbox.block_start_string
137-
block_end = self.sandbox.block_end_string
138-
if block_start in input_ or block_end in input_:
139-
raise SecurityError('blocks are disabled')
140-
# forbid calling methods or accessing properties
141-
forbidden = ['(', '.', '[', '__']
142-
var_start = re.escape(self.sandbox.variable_start_string)
143-
var_end = re.escape(self.sandbox.variable_end_string)
144-
exp = '{0}.*?{1}'.format(var_start, var_end)
145-
exp = re.compile(exp)
146-
for match in exp.findall(input_, re.DOTALL):
147-
if any(char in match for char in forbidden):
148-
raise SecurityError('"{0}" contains one or more forbidden '
149-
'characters'.format(match))
150-
output = self.sandbox.from_string(input_).render(self.context)
151-
return output
152-
153134
def validate(self):
154135
try:
155136
validate(self.config, self.schema)
156137
except JsonSchemaError as e:
157138
raise ValidationError(e)
158139

159-
def json(self, *args, **kwargs):
140+
def json(self, validate=True, *args, **kwargs):
160141
"""
161142
returns a string formatted in **NetJSON**;
162143
performs validation before returning output;
@@ -165,7 +146,8 @@ def json(self, *args, **kwargs):
165146
166147
:returns: string
167148
"""
168-
self.validate()
149+
if validate:
150+
self.validate()
169151
return json.dumps(self.config, *args, **kwargs)
170152

171153
@classmethod

tests/openwrt/test_context.py

+12-36
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import unittest
22

3-
from jinja2.exceptions import SecurityError
4-
53
from netjsonconfig import OpenWrt
4+
from netjsonconfig.backends.openwrt.timezones import timezones
65
from netjsonconfig.utils import _TabsMixin
76

87

@@ -38,37 +37,14 @@ def test_template(self):
3837
self.assertIn("option hostname 'test-context-name'", output)
3938
self.assertIn("option description 'test.context.desc'", output)
4039

41-
def test_sandbox(self):
42-
danger = """{{ self.__repr__.__globals__.get('sys').version }}"""
43-
config = {"general": {"description": danger}}
44-
o = OpenWrt(config, context={"description": "sandbox"})
45-
with self.assertRaises(SecurityError):
46-
print(o.render())
47-
48-
def test_security_binop(self):
49-
danger = """desc: {{ 10**10 }}"""
50-
config = {"general": {"description": danger}}
51-
o = OpenWrt(config, context={"description": "sandbox"})
52-
with self.assertRaises(SecurityError):
53-
print(o.render())
54-
55-
def test_security_unop(self):
56-
danger = """desc: {{ -10 }}"""
57-
config = {"general": {"description": danger}}
58-
o = OpenWrt(config, context={"description": "sandbox"})
59-
with self.assertRaises(SecurityError):
60-
print(o.render())
61-
62-
def test_security_block(self):
63-
danger = """{=## if True ##=}true{=## endif ##=}"""
64-
config = {"general": {"description": danger}}
65-
o = OpenWrt(config, context={"description": "sandbox"})
66-
with self.assertRaises(SecurityError):
67-
print(o.render())
68-
69-
def test_security_methods(self):
70-
danger = """{{ "{.__getitem__.__globals__[sys].version}".format(self) }}"""
71-
config = {"general": {"description": danger}}
72-
o = OpenWrt(config, context={"description": "sandbox"})
73-
with self.assertRaises(SecurityError):
74-
print(o.render())
40+
def test_evaluation_order(self):
41+
config = {
42+
"general": {
43+
"timezone": "{{ tz }}",
44+
}
45+
}
46+
context = {"tz": "Europe/Amsterdam"}
47+
o = OpenWrt(config, context=context)
48+
line = "option timezone '{Europe/Amsterdam}'".format(**timezones)
49+
output = o.render()
50+
self.assertIn(line, output)

0 commit comments

Comments
 (0)