diff options
| author | Kevin L. Mitchell <kevin.mitchell@rackspace.com> | 2012-10-09 20:14:08 +0100 |
|---|---|---|
| committer | Mark McLoughlin <markmc@redhat.com> | 2012-10-09 21:16:19 +0100 |
| commit | 21b69d86fc2aaf2aa0316c0e0b099a91bcf6937a (patch) | |
| tree | b51775d34aaf707f2ba8687f9404b45007c62928 | |
| parent | fa7dc58b7f0a5de137b30299bfc4d3f3aaa8d0cf (diff) | |
| download | oslo-21b69d86fc2aaf2aa0316c0e0b099a91bcf6937a.tar.gz oslo-21b69d86fc2aaf2aa0316c0e0b099a91bcf6937a.tar.xz oslo-21b69d86fc2aaf2aa0316c0e0b099a91bcf6937a.zip | |
Add a 'not' operator to the policy langage
Implements blueprint fine-grained-policy
Inverting the sense of a check was not possible with the list-of-lists
syntax, but it clearly makes sense to support it.
Change-Id: Ibd92cd75a279efdafec16a26f9aec33f39614b5c
| -rw-r--r-- | openstack/common/policy.py | 42 | ||||
| -rw-r--r-- | tests/unit/test_policy.py | 38 |
2 files changed, 77 insertions, 3 deletions
diff --git a/openstack/common/policy.py b/openstack/common/policy.py index f3efe55..f3b4be1 100644 --- a/openstack/common/policy.py +++ b/openstack/common/policy.py @@ -43,6 +43,11 @@ In the policy language, this becomes:: role:admin or (project_id:%(project_id)s and role:projectadmin) +The policy language also has the "not" operator, allowing a richer +policy rule:: + + project_id:%(project_id)s and not role:dunce + Finally, two special policy checks should be mentioned; the policy check "@" will always accept an access, and the policy check "!" will always reject an access. (Note that if a rule is either the empty @@ -259,6 +264,35 @@ class Check(BaseCheck): return "%s:%s" % (self.kind, self.match) +class NotCheck(BaseCheck): + """ + A policy check that inverts the result of another policy check. + Implements the "not" operator. + """ + + def __init__(self, rule): + """ + Initialize the 'not' check. + + :param rule: The rule to negate. Must be a Check. + """ + + self.rule = rule + + def __str__(self): + """Return a string representation of this check.""" + + return "not %s" % self.rule + + def __call__(self, target, cred): + """ + Check the policy. Returns the logical inverse of the wrapped + check. + """ + + return not self.rule(target, cred) + + class AndCheck(BaseCheck): """ A policy check that requires that a list of other checks all @@ -447,7 +481,7 @@ def _parse_tokenize(rule): # Yield the cleaned token lowered = clean.lower() - if lowered in ('and', 'or'): + if lowered in ('and', 'or', 'not'): # Special tokens yield lowered, clean elif clean: @@ -616,6 +650,12 @@ class ParseState(object): return [('or_expr', or_expr.add_check(check))] + @reducer('not', 'check') + def _make_not_expr(self, _not, check): + """Invert the result of another check.""" + + return [('check', NotCheck(check))] + def _parse_text_rule(rule): """ diff --git a/tests/unit/test_policy.py b/tests/unit/test_policy.py index fd9f9fb..a0cae6f 100644 --- a/tests/unit/test_policy.py +++ b/tests/unit/test_policy.py @@ -223,6 +223,32 @@ class CheckTestCase(unittest.TestCase): self.assertEqual(str(check), 'kind:match') +class NotCheckTestCase(unittest.TestCase): + def test_init(self): + check = policy.NotCheck('rule') + + self.assertEqual(check.rule, 'rule') + + def test_str(self): + check = policy.NotCheck('rule') + + self.assertEqual(str(check), 'not rule') + + def test_call_true(self): + rule = mock.Mock(return_value=True) + check = policy.NotCheck(rule) + + self.assertEqual(check('target', 'cred'), False) + rule.assert_called_once_with('target', 'cred') + + def test_call_false(self): + rule = mock.Mock(return_value=False) + check = policy.NotCheck(rule) + + self.assertEqual(check('target', 'cred'), True) + rule.assert_called_once_with('target', 'cred') + + class OrCheckTestCase(unittest.TestCase): def test_init(self): check = policy.OrCheck(['rule1', 'rule2']) @@ -381,13 +407,13 @@ class ParseListRuleTestCase(unittest.TestCase): class ParseTokenizeTestCase(unittest.TestCase): @mock.patch.object(policy, '_parse_check', lambda x: x) def test_tokenize(self): - exemplar = ("(( ( ((() And)) or ) (check:%(miss)s))) " + exemplar = ("(( ( ((() And)) or ) (check:%(miss)s) not)) " "'a-string' \"another-string\"") expected = [ ('(', '('), ('(', '('), ('(', '('), ('(', '('), ('(', '('), ('(', '('), (')', ')'), ('and', 'And'), (')', ')'), (')', ')'), ('or', 'or'), (')', ')'), ('(', '('), - ('check', 'check:%(miss)s'), (')', ')'), + ('check', 'check:%(miss)s'), (')', ')'), ('not', 'not'), (')', ')'), (')', ')'), ('string', 'a-string'), ('string', 'another-string'), @@ -588,6 +614,14 @@ class ParseStateTestCase(unittest.TestCase): self.assertEqual(result, [('or_expr', 'newcheck')]) mock_expr.add_check.assert_called_once_with('check') + @mock.patch.object(policy, 'NotCheck', lambda x: 'not %s' % x) + def test_make_not_expr(self): + state = policy.ParseState() + + result = state._make_not_expr('not', 'check') + + self.assertEqual(result, [('check', 'not check')]) + class ParseTextRuleTestCase(unittest.TestCase): def test_empty(self): |
