Skip to content

Commit

Permalink
Simplified implementation of configuration variables
Browse files Browse the repository at this point in the history
  • Loading branch information
nemesifier committed Feb 19, 2016
1 parent aa8b485 commit 7323491
Show file tree
Hide file tree
Showing 3 changed files with 29 additions and 94 deletions.
23 changes: 0 additions & 23 deletions netjsonconfig/backends/base.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
52 changes: 17 additions & 35 deletions netjsonconfig/backends/openwrt/openwrt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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 """
Expand Down Expand Up @@ -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.
Expand All @@ -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):
Expand All @@ -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;
Expand All @@ -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
Expand Down
48 changes: 12 additions & 36 deletions tests/openwrt/test_context.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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.