Merge "Reject rules with column refs on non-datasources"

This commit is contained in:
Jenkins 2016-03-11 03:34:54 +00:00 committed by Gerrit Code Review
commit a45c643a8c
8 changed files with 128 additions and 46 deletions

View File

@ -23,6 +23,15 @@ try:
except ImportError:
import httplib
# TODO(thinrichs): move this out of api directory. Could go into
# the exceptions.py file. The HTTP error codes may make these errors
# look like they are only useful for the API, but actually they are
# just encoding the classification of the error using http codes.
# To make this more explicit, we could have 2 dictionaries where
# one maps an error name (readable for programmers) to an error number
# and another dictionary that maps an error name/number to the HTTP
# classification. But then it would be easy for a programmer when
# adding a new error to forget one or the other.
# name of unknown error
UNKNOWN = 'unknown'

View File

@ -29,6 +29,7 @@ from six.moves import range
from oslo_log import log as logging
from congress.datalog import analysis
from congress.datalog import base
from congress.datalog.builtin import congressbuiltin
# set up appropriate antlr paths per python version and import runtime
@ -800,7 +801,8 @@ class Literal (object):
self.table.drop_service()
return self
def eliminate_column_references(self, schema, index, prefix=''):
def eliminate_column_references(self, theories, default_theory=None,
index=0, prefix=''):
"""Expand column references to traditional datalog positional args.
Returns a new literal, unless no column references.
@ -808,10 +810,16 @@ class Literal (object):
# corner cases
if len(self.named_arguments) == 0:
return self
if schema is None:
theory = literal_theory(self, theories, default_theory)
if theory is None or theory.schema is None:
raise exception.IncompleteSchemaException(
"Literal %s uses named arguments, but the "
"schema is unknown." % self)
if theory.kind != base.DATASOURCE_POLICY_TYPE: # eventually remove
raise exception.PolicyException(
"Literal {} uses column references, but '{}' does not "
"reference a datasource policy".format(self, theory.name))
schema = theory.schema
if self.table.table not in schema:
raise exception.IncompleteSchemaException(
"Literal {} uses unknown table {} "
@ -1069,19 +1077,18 @@ class Rule(object):
Throws exception if RULE is inconsistent with schemas.
"""
pre = self._unused_variable_prefix()
heads = []
for i in range(0, len(self.heads)):
heads.append(self.heads[i].eliminate_column_references(
literal_schema(self.heads[i], theories, default_theory),
i, prefix='%s%s' % (pre, i)))
theories, default_theory=default_theory,
index=i, prefix='%s%s' % (pre, i)))
body = []
for i in range(0, len(self.body)):
body.append(self.body[i].eliminate_column_references(
literal_schema(self.body[i], theories, default_theory),
i, prefix='%s%s' % (pre, i)))
theories, default_theory=default_theory,
index=i, prefix='%s%s' % (pre, i)))
return Rule(heads, body, self.location, name=self.name,
comment=self.comment, original_str=self.original_str)
@ -1635,8 +1642,33 @@ def rule_body_safety(rule):
return [e]
def literal_schema(literal, theories, default_theory=None):
"""Return the schema that applies to LITERAL or None."""
def literal_schema(literal, theories, default_theory=None,
theory_assertion=None):
"""Return the schema that applies to LITERAL or None.
:param LITERAL is a Literal for which we want the schema
:param THEORIES is a dictionary mapping the name of the theory
to the theory object
:param DEFAULT_THEORY is the theory to use if no theory is
recorded as part of LITERAL
:returns: the schema that applies to LITERAL or None
"""
theory = literal_theory(literal, theories, default_theory)
if theory is None:
return
return theory.schema
def literal_theory(literal, theories, default_theory=None):
"""Return the theory that applies to LITERAL or None.
:param LITERAL is a Literal for which we want the schema
:param THEORIES is a dictionary mapping the name of the theory
to the theory object
:param DEFAULT_THEORY is the theory to use if no theory is
recorded as part of LITERAL
:returns: the theory that applies to LITERAL or None
"""
if theories is None:
return
# figure out theory that pertains to this literal
@ -1648,8 +1680,7 @@ def literal_schema(literal, theories, default_theory=None):
if active_theory not in theories:
# May not have been created yet
return
# return schema
return theories[active_theory].schema
return theories[active_theory]
def schema_consistency(thing, theories, theory=None):
@ -1717,8 +1748,9 @@ def literal_schema_consistency(literal, theories, theory=None):
def check_schema_consistency(item, theories, theory=None):
errors = []
if item.is_rule():
errors.extend(literal_schema_consistency(
item.head, theories, theory))
for head in item.heads:
errors.extend(literal_schema_consistency(
head, theories, theory))
for lit in item.body:
errors.extend(literal_schema_consistency(
lit, theories, theory))

View File

@ -25,6 +25,7 @@ from oslo_config import cfg
from oslo_log import log as logging
import six
from congress.api import error_codes
from congress import utils
@ -77,22 +78,27 @@ class CongressException(Exception):
pass
if not message:
try:
message = self.msg_fmt % kwargs
if self.name is not None:
error_code = error_codes.get_num(self.name)
description = error_codes.get_desc(self.name)
message = "(%s) %s" % (error_code, description)
else:
try:
message = self.msg_fmt % kwargs
except Exception:
exc_info = sys.exc_info()
# kwargs doesn't match a variable in the message
# log the issue and the kwargs
LOG.exception(_('Exception in string format operation'))
for name, value in kwargs.items():
LOG.error("%s: %s", name, value) # noqa
except Exception:
exc_info = sys.exc_info()
# kwargs doesn't match a variable in the message
# log the issue and the kwargs
LOG.exception(_('Exception in string format operation'))
for name, value in kwargs.items():
LOG.error("%s: %s", name, value) # noqa
if CONF.fatal_exception_format_errors:
six.reraise(exc_info[0], exc_info[1], exc_info[2])
else:
# at least get the core message out if something happened
message = self.msg_fmt
if CONF.fatal_exception_format_errors:
six.reraise(exc_info[0], exc_info[1], exc_info[2])
else:
# at least get the core message out
message = self.msg_fmt
super(CongressException, self).__init__(message)

View File

@ -992,7 +992,7 @@ class Runtime (object):
try:
oldformula = event.formula
event.formula = oldformula.eliminate_column_references(
self.theory, event.target)
self.theory, default_theory=event.target)
# doesn't copy over ID since it creates a new one
event.formula.set_id(oldformula.id)
enabled.append(event)

View File

@ -21,6 +21,7 @@ from oslo_config import cfg
from congress.api import rule_model
from congress.api import webservice
from congress.datalog import base as datalogbase
from congress.datalog import compile
from congress import harness
from congress.tests import base
@ -74,9 +75,9 @@ class TestRuleModel(base.SqlTestCase):
self.rule_model.add_item,
test_rule, {})
def test_add_rule_using_schema(self):
def test_add_rule_with_colrefs(self):
engine = self.engine
engine.create_policy('beta')
engine.create_policy('beta', kind=datalogbase.DATASOURCE_POLICY_TYPE)
engine.set_schema(
'beta', compile.Schema({'q': ("name", "status", "year")}))
# insert/retrieve rule with column references
@ -87,6 +88,16 @@ class TestRuleModel(base.SqlTestCase):
{}, context=self.context)
self.rule_model.get_item(id1, {}, context=self.context)
def test_add_rule_with_bad_colrefs(self):
engine = self.engine
engine.create_policy('beta') # not datasource policy
# exception because col refs over non-datasource policy
self.assertRaises(
webservice.DataModelException,
self.rule_model.add_item,
{'rule': 'p(x) :- beta:q(name=x)'},
{}, context=self.context)
def test_get_items(self):
ret = self.rule_model.get_items({}, context=self.context)
self.assertTrue(all(p in ret['results']

View File

@ -20,6 +20,7 @@ from __future__ import absolute_import
import copy
from congress.datalog import analysis
from congress.datalog import base as datalogbase
from congress.datalog import compile
from congress.datalog import utility
from congress import exception
@ -172,6 +173,7 @@ class TestColumnReferences(base.TestCase):
"""Placeholder so we don't use the actual policy-engine for tests."""
def __init__(self, schema):
self.schema = schema
self.kind = datalogbase.DATASOURCE_POLICY_TYPE
def test_column_references_lowlevel(self):
"""Test column-references with low-level checks."""

View File

@ -1591,7 +1591,7 @@ class TestDisabledRules(base.TestCase):
# insertions
def test_insert_enabled(self):
run = agnostic.Runtime()
run.create_policy('test')
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
schema = compile.Schema({'q': ('id', 'name', 'status')})
run.set_schema('test', schema)
obj = run.policy_object('test')
@ -1602,7 +1602,7 @@ class TestDisabledRules(base.TestCase):
def test_insert_disabled(self):
run = agnostic.Runtime()
run.create_policy('test')
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
obj = run.policy_object('test')
run.insert('p(x) :- q(id=x)')
self.assertEqual(len(run.disabled_events), 1)
@ -1610,7 +1610,7 @@ class TestDisabledRules(base.TestCase):
def test_insert_errors(self):
run = agnostic.Runtime()
run.create_policy('test')
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
schema = compile.Schema({'q': ('name', 'status')})
run.set_schema('test', schema)
obj = run.policy_object('test')
@ -1624,7 +1624,7 @@ class TestDisabledRules(base.TestCase):
def test_insert_set_schema_disabled(self):
run = agnostic.Runtime()
run.create_policy('test')
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
obj = run.policy_object('test')
run.insert('p(x) :- q(id=x)') # rule is disabled
self.assertEqual(len(run.disabled_events), 1)
@ -1637,8 +1637,8 @@ class TestDisabledRules(base.TestCase):
def test_insert_set_schema_disabled_multiple(self):
# insert rule that gets disabled
run = agnostic.Runtime()
run.create_policy('test')
run.create_policy('nova')
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
run.create_policy('nova', kind=datalog_base.DATASOURCE_POLICY_TYPE)
obj = run.policy_object('test')
run.insert('p(x) :- q(id=x), nova:r(id=x)', 'test')
self.assertEqual(len(run.disabled_events), 1)
@ -1657,7 +1657,7 @@ class TestDisabledRules(base.TestCase):
def test_insert_set_schema_errors(self):
run = agnostic.Runtime()
run.create_policy('test')
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
obj = run.policy_object('test')
run.insert('p(x) :- q(id=x)') # rule is disabled
self.assertEqual(len(run.disabled_events), 1)
@ -1669,7 +1669,7 @@ class TestDisabledRules(base.TestCase):
def test_insert_inferred_schema_errors(self):
run = agnostic.Runtime()
run.create_policy('test')
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
run.insert('p(x) :- q(x)')
permitted, errs = run.insert('q(1,2)')
self.assertFalse(permitted)
@ -1677,7 +1677,7 @@ class TestDisabledRules(base.TestCase):
# deletions
def test_delete_enabled(self):
run = agnostic.Runtime()
run.create_policy('test')
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
schema = compile.Schema({'q': ('id', 'name', 'status')})
run.set_schema('test', schema)
obj = run.policy_object('test')
@ -1690,7 +1690,7 @@ class TestDisabledRules(base.TestCase):
def test_delete_set_schema_disabled(self):
run = agnostic.Runtime()
run.create_policy('test')
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
obj = run.policy_object('test')
run.insert('p(x) :- q(id=x)')
run.delete('p(x) :- q(id=x)')
@ -1703,7 +1703,7 @@ class TestDisabledRules(base.TestCase):
def test_delete_errors(self):
run = agnostic.Runtime()
run.create_policy('test')
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
schema = compile.Schema({'q': ('name', 'status')})
run.set_schema('test', schema)
obj = run.policy_object('test')
@ -1717,7 +1717,7 @@ class TestDisabledRules(base.TestCase):
def test_delete_set_schema_errors(self):
run = agnostic.Runtime()
run.create_policy('test')
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
obj = run.policy_object('test')
run.delete('p(x) :- q(id=x)') # rule is disabled
self.assertEqual(len(run.disabled_events), 1)
@ -1741,7 +1741,7 @@ class TestDisabledRules(base.TestCase):
# Ensures that cannot change schema once it is set.
# Can be removed once we support schema changes (e.g. for upgrade).
run = agnostic.Runtime()
run.create_policy('test')
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
schema = compile.Schema({'q': ('name', 'status')})
run.set_schema('test', schema)
schema = compile.Schema({'q': ('id', 'name', 'status')})
@ -1751,6 +1751,16 @@ class TestDisabledRules(base.TestCase):
except exception.CongressException as e:
self.assertTrue("Schema for test already set" in str(e))
def test_insert_without_datasource_policy(self):
run = agnostic.Runtime()
run.create_policy('beta') # not datasource policy
# exception because col refs over non-datasource policy
permitted, errors = run.insert('p(x) :- beta:q(name=x)')
self.assertFalse(permitted)
self.assertTrue(
any("does not reference a datasource policy" in str(e)
for e in errors))
class TestDelegation(base.TestCase):
"""Tests for Runtime's delegation functionality."""

View File

@ -89,19 +89,31 @@ class TestRuleModel(base.SqlTestCase):
# so that when it subscribes, the snapshot can be returned.
# Or fix the subscribe() implementation so that we can subscribe before
# the service has been created.
# def test_add_rule_using_schema(self):
# def test_add_rule_with_colrefs(self):
# engine = self.engine
# engine.create_policy('beta')
# engine.create_policy('beta', kind=datalogbase.DATASOURCE_POLICY_TYPE)
# engine.set_schema(
# 'beta', compile.Schema({'q': ("name", "status", "year")}))
# # insert/retrieve rule with column references
# # testing that no errors are thrown--correctness tested elsewhere
# # just testing that no errors are thrown--correctness elsewhere
# # Assuming that api-models are pass-throughs to functionality
# (id1, _) = self.rule_model.add_item(
# {'rule': 'p(x) :- beta:q(name=x)'},
# {}, context=self.context)
# self.rule_model.get_item(id1, {}, context=self.context)
# def test_add_rule_with_bad_colrefs(self):
# engine = self.engine
# engine.create_policy('beta') # not datasource policy
# # insert/retrieve rule with column references
# # just testing that no errors are thrown--correctness elsewhere
# # Assuming that api-models are pass-throughs to functionality
# self.assertRaises(
# webservice.DataModelException,
# self.rule_model.add_item,
# {'rule': 'p(x) :- beta:q(name=x)'},
# {}, context=self.context)
def test_get_items(self):
ret = self.rule_model.get_items({}, context=self.context)
self.assertTrue(all(p in ret['results']