Merge "Reject rules with column refs on non-datasources"
This commit is contained in:
commit
a45c643a8c
|
@ -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'
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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']
|
||||
|
|
Loading…
Reference in New Issue