Permalink
Browse files

Simplified implementation of configuration variables

  • Loading branch information...
nemesisdesign committed Feb 19, 2016
1 parent aa8b485 commit 7323491915980bbb339443a7e1137d8594261c39
Showing with 29 additions and 94 deletions.
  1. +0 −23 netjsonconfig/backends/base.py
  2. +17 −35 netjsonconfig/backends/openwrt/openwrt.py
  3. +12 −36 tests/openwrt/test_context.py
@@ -1,26 +1,3 @@
from jinja2.exceptions import SecurityError
from jinja2.sandbox import SandboxedEnvironment
class ConfigSandbox(SandboxedEnvironment):
intercepted_binops = frozenset(SandboxedEnvironment.default_binop_table.keys())
intercepted_unops = frozenset(SandboxedEnvironment.default_unop_table.keys())
def __init__(self, *args, **kwargs):
super(ConfigSandbox, self).__init__(*args, **kwargs)
self.globals = {}
self.tests = {}
self.filters = {}
self.block_start_string = '{=##'
self.block_end_string = '##=}'
def call_binop(self, context, operator, left, right):
raise SecurityError('binary operator {0} not allowed'.format(operator))
def call_unop(self, context, operator, left):
raise SecurityError('unary operator {0} not allowed'.format(operator))
class BaseRenderer(object):
"""
Renderers are used to generate specific configuration blocks.
@@ -7,14 +7,12 @@
import six
from jinja2 import Environment, PackageLoader
from jinja2.exceptions import SecurityError
from jsonschema import validate
from jsonschema.exceptions import ValidationError as JsonSchemaError
from . import renderers
from ...exceptions import ValidationError
from ...utils import merge_config
from ..base import ConfigSandbox
from ...utils import evaluate_vars, merge_config, var_pattern
from .schema import DEFAULT_FILE_MODE, schema
@@ -45,11 +43,10 @@ def __init__(self, config, templates=[], context={}):
if 'type' not in config:
config.update({'type': 'DeviceConfiguration'})
self.config = self._merge_config(config, templates)
self.context = context
self.config = self._evaluate_vars(self.config, context)
self.env = Environment(loader=PackageLoader('netjsonconfig.backends.openwrt',
'templates'),
trim_blocks=True)
self.sandbox = ConfigSandbox()
def _load(self, config):
""" loads config from string or dict """
@@ -77,6 +74,18 @@ def _merge_config(self, config, templates):
return merge_config(base_config, config)
return config
def _evaluate_vars(self, config, context):
""" evaluates configuration variables """
# return immediately if context is empty
if not context:
return config
# return immediately if no variables are found
netjson = self.json(validate=False)
if var_pattern.search(netjson) is None:
return config
# only if variables are found perform evaluation
return evaluate_vars(config, context)
def render(self, files=True):
"""
Converts the configuration dictionary into the native OpenWRT UCI format.
@@ -101,9 +110,6 @@ def render(self, files=True):
files_output = self._render_files()
if files_output:
output += files_output.replace('\n\n\n', '\n\n') # max 3 \n
# configuration variables
if self.context:
output = self._render_context(output)
return output
def _render_files(self):
@@ -125,38 +131,13 @@ def _render_files(self):
output += file_output
return output
def _render_context(self, input_):
"""
Evaluates configuration variables passed in ``context`` arg
:param input_: string containing rendered configuration
:returns: string containing modified input_
"""
# disable jinja blocks
block_start = self.sandbox.block_start_string
block_end = self.sandbox.block_end_string
if block_start in input_ or block_end in input_:
raise SecurityError('blocks are disabled')
# forbid calling methods or accessing properties
forbidden = ['(', '.', '[', '__']
var_start = re.escape(self.sandbox.variable_start_string)
var_end = re.escape(self.sandbox.variable_end_string)
exp = '{0}.*?{1}'.format(var_start, var_end)
exp = re.compile(exp)
for match in exp.findall(input_, re.DOTALL):
if any(char in match for char in forbidden):
raise SecurityError('"{0}" contains one or more forbidden '
'characters'.format(match))
output = self.sandbox.from_string(input_).render(self.context)
return output
def validate(self):
try:
validate(self.config, self.schema)
except JsonSchemaError as e:
raise ValidationError(e)
def json(self, *args, **kwargs):
def json(self, validate=True, *args, **kwargs):
"""
returns a string formatted in **NetJSON**;
performs validation before returning output;
@@ -165,7 +146,8 @@ def json(self, *args, **kwargs):
:returns: string
"""
self.validate()
if validate:
self.validate()
return json.dumps(self.config, *args, **kwargs)
@classmethod
@@ -1,8 +1,7 @@
import unittest
from jinja2.exceptions import SecurityError
from netjsonconfig import OpenWrt
from netjsonconfig.backends.openwrt.timezones import timezones
from netjsonconfig.utils import _TabsMixin
@@ -38,37 +37,14 @@ def test_template(self):
self.assertIn("option hostname 'test-context-name'", output)
self.assertIn("option description 'test.context.desc'", output)
def test_sandbox(self):
danger = """{{ self.__repr__.__globals__.get('sys').version }}"""
config = {"general": {"description": danger}}
o = OpenWrt(config, context={"description": "sandbox"})
with self.assertRaises(SecurityError):
print(o.render())
def test_security_binop(self):
danger = """desc: {{ 10**10 }}"""
config = {"general": {"description": danger}}
o = OpenWrt(config, context={"description": "sandbox"})
with self.assertRaises(SecurityError):
print(o.render())
def test_security_unop(self):
danger = """desc: {{ -10 }}"""
config = {"general": {"description": danger}}
o = OpenWrt(config, context={"description": "sandbox"})
with self.assertRaises(SecurityError):
print(o.render())
def test_security_block(self):
danger = """{=## if True ##=}true{=## endif ##=}"""
config = {"general": {"description": danger}}
o = OpenWrt(config, context={"description": "sandbox"})
with self.assertRaises(SecurityError):
print(o.render())
def test_security_methods(self):
danger = """{{ "{.__getitem__.__globals__[sys].version}".format(self) }}"""
config = {"general": {"description": danger}}
o = OpenWrt(config, context={"description": "sandbox"})
with self.assertRaises(SecurityError):
print(o.render())
def test_evaluation_order(self):
config = {
"general": {
"timezone": "{{ tz }}",
}
}
context = {"tz": "Europe/Amsterdam"}
o = OpenWrt(config, context=context)
line = "option timezone '{Europe/Amsterdam}'".format(**timezones)
output = o.render()
self.assertIn(line, output)

0 comments on commit 7323491

Please sign in to comment.