diff options
-rw-r--r-- | doc/source/cli/index.rst | 40 | ||||
-rw-r--r-- | oslo_policy/generator.py | 69 | ||||
-rw-r--r-- | oslo_policy/tests/test_generator.py | 48 | ||||
-rw-r--r-- | releasenotes/notes/policy-file-validator-906d5cff864a2d51.yaml | 6 | ||||
-rw-r--r-- | setup.cfg | 1 |
5 files changed, 163 insertions, 1 deletions
diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst index c7023de..bef7c99 100644 --- a/doc/source/cli/index.rst +++ b/doc/source/cli/index.rst @@ -151,3 +151,43 @@ For more information regarding the options supported by this tool: .. code-block:: bash oslopolicy-list-redundant --help + +oslopolicy_validator +==================== + +The ``oslopolicy-validator`` tool can be used to perform basic sanity checks +against a policy file. It will detect the following problems: + +* A missing policy file +* Rules which have invalid syntax +* Rules which reference non-existent other rules +* Rules which form a cyclical reference with another rule +* Rules which do not exist in the specified namespace + +This tool does very little validation of the content of the rules. Other tools, +such as ``oslopolicy-checker``, should be used to check that rules do what is +intended. + +``oslopolicy-validator`` exits with a ``0`` return code on success and ``1`` on +failure. + +.. note:: At this time the policy validator can only handle single policy + files, not policy dirs. + +Examples +-------- + +Validate the policy file used for Keystone: + +.. code-block:: bash + + oslopolicy-validator --config-file /etc/keystone/keystone.conf --namespace keystone + +Sample output from a failed validation:: + + $ oslopolicy-validator --config-file keystone.conf --namespace keystone + WARNING:oslo_policy.policy:Policies ['foo', 'bar'] are part of a cyclical reference. + Invalid rules found + Failed to parse rule: (role:admin and system_scope:all) or (role:foo and oken.domain.id:%(target.user.domain_id)s)) + Unknown rule found in policy file: foo + Unknown rule found in policy file: bar diff --git a/oslo_policy/generator.py b/oslo_policy/generator.py index 48a9972..40f374d 100644 --- a/oslo_policy/generator.py +++ b/oslo_policy/generator.py @@ -328,12 +328,70 @@ def _list_redundant(namespace): enforcer.load_rules() for name, file_rule in enforcer.file_rules.items(): - reg_rule = enforcer.registered_rules.get(name, None) + reg_rule = enforcer.registered_rules.get(name) if reg_rule: if file_rule == reg_rule: print(reg_rule) +def _validate_policy(namespace): + """Perform basic sanity checks on a policy file + + Checks for the following errors in the configured policy file: + + * A missing policy file + * Rules which have invalid syntax + * Rules which reference non-existent other rules + * Rules which form a cyclical reference with another rule + * Rules which do not exist in the specified namespace + + :param namespace: The name under which the oslo.policy enforcer is + registered. + :returns: 0 if all policies validated correctly, 1 if not. + """ + return_code = 0 + enforcer = _get_enforcer(namespace) + # NOTE(bnemec): We don't want to see policy deprecation warnings in the + # output of this tool. They tend to overwhelm the output that the user + # actually cares about. If we check for deprecated rules in this tool, + # we need to do it another way. + enforcer.suppress_deprecation_warnings = True + # Disable logging from the parser code. We'll be printing any errors we + # find below. + logging.disable(logging.ERROR) + # Ensure that files have been parsed + enforcer.load_rules() + + if enforcer._informed_no_policy_file: + print('Configured policy file "%s" not found' % enforcer.policy_file) + # If the policy file is completely missing then the rest of our checks + # don't make sense. + return 1 + + # Re-enable logging so we get messages for things like cyclical references + logging.disable(logging.NOTSET) + result = enforcer.check_rules() + if not result: + print('Invalid rules found') + return_code = 1 + + # TODO(bnemec): Allow this to handle policy_dir + with open(cfg.CONF.oslo_policy.policy_file) as f: + unparsed_policies = yaml.safe_load(f.read()) + for name, file_rule in enforcer.file_rules.items(): + reg_rule = enforcer.registered_rules.get(name) + if reg_rule is None: + print('Unknown rule found in policy file:', name) + return_code = 1 + # If a rule has invalid syntax it will be forced to '!'. If the literal + # rule from the policy file isn't '!' then this means there was an + # error parsing it. + if str(enforcer.rules[name]) == '!' and unparsed_policies[name] != '!': + print('Failed to parse rule:', unparsed_policies[name]) + return_code = 1 + return return_code + + def on_load_failure_callback(*args, **kwargs): raise @@ -423,3 +481,12 @@ def list_redundant(args=None): conf(args) _check_for_namespace_opt(conf) _list_redundant(conf.namespace) + + +def validate_policy(args=None): + logging.basicConfig(level=logging.WARN) + conf = cfg.CONF + conf.register_cli_opts(ENFORCER_OPTS) + conf.register_opts(ENFORCER_OPTS) + conf(args) + sys.exit(_validate_policy(conf.namespace)) diff --git a/oslo_policy/tests/test_generator.py b/oslo_policy/tests/test_generator.py index 86003fe..af6398f 100644 --- a/oslo_policy/tests/test_generator.py +++ b/oslo_policy/tests/test_generator.py @@ -751,3 +751,51 @@ class GetEnforcerTestCase(base.PolicyBaseTestCase): mock_instance.__contains__.return_value = False mock_manager.return_value = mock_instance self.assertRaises(KeyError, generator._get_enforcer, 'nonexistent') + + +class ValidatorTestCase(base.PolicyBaseTestCase): + def _get_test_enforcer(self): + test_rules = [policy.RuleDefault('foo', 'foo:bar=baz'), + policy.RuleDefault('bar', 'bar:foo=baz')] + enforcer = policy.Enforcer(self.conf) + enforcer.register_defaults(test_rules) + return enforcer + + def _test_policy(self, rule, success=False, missing_file=False): + policy_file = self.get_config_file_fullname('test.yaml') + if missing_file: + policy_file = 'bogus.yaml' + self.create_config_file('test.yaml', rule) + self.create_config_file('test.conf', + '[oslo_policy]\npolicy_file=%s' % policy_file) + # Reparse now that we've created our configs + self.conf(args=['--config-dir', self.config_dir]) + + with mock.patch('oslo_policy.generator._get_enforcer') as ge: + ge.return_value = self._get_test_enforcer() + result = generator._validate_policy('test') + if success: + self.assertEqual(0, result) + else: + self.assertEqual(1, result) + + def test_success(self): + self._test_policy('foo: rule:bar', success=True) + + def test_cyclical_reference(self): + self._test_policy('foo: rule:bar\nbar: rule:foo') + + def test_invalid_syntax(self): + self._test_policy('foo: (bar))') + + def test_false_okay(self): + self._test_policy('foo: !', success=True) + + def test_reference_nonexistent(self): + self._test_policy('foo: rule:baz') + + def test_nonexistent(self): + self._test_policy('baz: rule:foo') + + def test_missing_policy_file(self): + self._test_policy('', missing_file=True) diff --git a/releasenotes/notes/policy-file-validator-906d5cff864a2d51.yaml b/releasenotes/notes/policy-file-validator-906d5cff864a2d51.yaml new file mode 100644 index 0000000..5e41c62 --- /dev/null +++ b/releasenotes/notes/policy-file-validator-906d5cff864a2d51.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + A new tool, ``oslopolicy-validator``, has been added. It allows deployers + to easily run basic sanity checks against their policy files. See the + documentation for full details. @@ -35,6 +35,7 @@ console_scripts = oslopolicy-policy-generator = oslo_policy.generator:generate_policy oslopolicy-list-redundant = oslo_policy.generator:list_redundant oslopolicy-policy-upgrade = oslo_policy.generator:upgrade_policy + oslopolicy-validator = oslo_policy.generator:validate_policy oslo.policy.rule_checks = http = oslo_policy._external:HttpCheck |