summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--babel.cfg2
-rw-r--r--doc/requirements.txt6
-rw-r--r--doc/source/cli/index.rst40
-rw-r--r--doc/source/conf.py11
-rw-r--r--doc/source/index.rst13
-rw-r--r--lower-constraints.txt9
-rw-r--r--oslo_policy/generator.py69
-rw-r--r--oslo_policy/policy.py41
-rw-r--r--oslo_policy/sphinxext.py4
-rw-r--r--oslo_policy/sphinxpolicygen.py4
-rw-r--r--oslo_policy/tests/test_generator.py48
-rw-r--r--oslo_policy/tests/test_policy.py43
-rw-r--r--releasenotes/notes/bug-1880959-8f1370a59759d40d.yaml8
-rw-r--r--releasenotes/notes/policy-file-validator-906d5cff864a2d51.yaml6
-rw-r--r--releasenotes/source/conf.py8
-rw-r--r--setup.cfg15
16 files changed, 279 insertions, 48 deletions
diff --git a/babel.cfg b/babel.cfg
deleted file mode 100644
index 15cd6cb..0000000
--- a/babel.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-[python: **.py]
-
diff --git a/doc/requirements.txt b/doc/requirements.txt
index 9cf3697..3882a1e 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -2,8 +2,8 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
-openstackdocstheme>=1.20.0 # Apache-2.0
-sphinx>=1.8.0,!=2.1.0 # BSD
+openstackdocstheme>=2.2.0 # Apache-2.0
+sphinx>=2.0.0,!=2.1.0 # BSD
sphinxcontrib-apidoc>=0.2.0 # BSD
-reno>=2.5.0 # Apache-2.0
+reno>=3.1.0 # Apache-2.0
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/doc/source/conf.py b/doc/source/conf.py
index c2b91e4..0ec69f1 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -25,9 +25,9 @@ extensions = [
]
# openstackdocstheme options
-repository_name = 'openstack/oslo.policy'
-bug_project = 'oslo.policy'
-bug_tag = ''
+openstackdocs_repo_name = 'openstack/oslo.policy'
+openstackdocs_bug_project = 'oslo.policy'
+openstackdocs_bug_tag = ''
# autodoc generation is a bit aggressive and a nuisance when doing heavy
# text edit cycles.
@@ -40,7 +40,6 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
-project = u'oslo.policy'
copyright = u'2014, OpenStack Foundation'
source_tree = 'https://opendev.org/openstack/oslo.policy/src/branch/master'
@@ -52,19 +51,17 @@ add_function_parentheses = True
add_module_names = True
# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = 'native'
# A list of ignored prefixes for module index sorting.
modindex_common_prefix = ['oslo_policy.']
-
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'openstackdocs'
-
# -- sphinx.ext.extlinks configuration ---------------------------------------
extlinks = {
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 07a96ca..65b977a 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -5,6 +5,9 @@
An OpenStack library providing support for RBAC policy enforcement across all
OpenStack services.
+Contents
+========
+
.. toctree::
:maxdepth: 2
@@ -16,7 +19,15 @@ OpenStack services.
reference/index
contributor/index
-.. rubric:: Indices and tables
+Release Notes
+=============
+
+Read also the `oslo.policy Release Notes
+<https://docs.openstack.org/releasenotes/oslo.policy/>`_.
+
+
+Indices and tables
+==================
* :ref:`genindex`
* :ref:`modindex`
diff --git a/lower-constraints.txt b/lower-constraints.txt
index 5b0949e..616bc90 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -8,22 +8,18 @@ docutils==0.11
dulwich==0.15.0
extras==1.0.0
fixtures==3.0.0
-flake8==2.5.5
gitdb==0.6.4
GitPython==1.0.1
-hacking==0.12.0
imagesize==0.7.1
iso8601==0.1.11
Jinja2==2.10
keystoneauth1==3.4.0
linecache2==1.0.0
MarkupSafe==1.0
-mccabe==0.2.1
mox3==0.20.0
msgpack-python==0.4.0
netaddr==0.7.18
netifaces==0.10.4
-openstackdocstheme==1.20.0
os-client-config==1.28.0
oslo.config==5.2.0
oslo.context==2.22.0
@@ -32,15 +28,12 @@ oslo.serialization==2.18.0
oslo.utils==3.40.0
oslotest==3.2.0
pbr==2.0.0
-pep8==1.5.7
-pyflakes==0.8.1
Pygments==2.2.0
pyparsing==2.1.0
python-mimeparse==1.6.0
python-subunit==1.0.0
pytz==2013.6
PyYAML==3.12
-reno==2.5.0
requests==2.14.2
requests-mock==1.2.0
requestsexceptions==1.2.0
@@ -49,8 +42,6 @@ six==1.10.0
stestr==2.0.0
smmap==0.9.0
snowballstemmer==1.2.1
-Sphinx==1.8.0
-sphinxcontrib-websupport==1.0.1
stevedore==1.20.0
testrepository==0.0.18
testtools==2.2.0
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/policy.py b/oslo_policy/policy.py
index 406ed35..be0bd00 100644
--- a/oslo_policy/policy.py
+++ b/oslo_policy/policy.py
@@ -560,6 +560,8 @@ class Enforcer(object):
if force_reload:
self.use_conf = force_reload
+ policy_file_rules_changed = False
+
if self.use_conf:
if not self.policy_path:
try:
@@ -571,18 +573,30 @@ class Enforcer(object):
self._informed_no_policy_file = True
if self.policy_path:
- self._load_policy_file(self.policy_path, force_reload,
- overwrite=self.overwrite)
+ # If the policy file rules have changed any policy.d rules
+ # also need to be reapplied on top of that change.
+ policy_file_rules_changed = self._load_policy_file(
+ self.policy_path,
+ force_reload,
+ overwrite=self.overwrite
+ )
+
+ force_reload_policy_dir = force_reload
+ if policy_file_rules_changed:
+ force_reload_policy_dir = True
+
for path in self.conf.oslo_policy.policy_dirs:
try:
path = self._get_policy_path(path)
except cfg.ConfigFilesNotFoundError:
continue
- if (force_reload or self._is_directory_updated(
- self._policy_dir_mtimes, path)):
- self._walk_through_policy_directory(path,
- self._load_policy_file,
- force_reload, False)
+ if (self._is_directory_updated(self._policy_dir_mtimes, path)
+ or force_reload_policy_dir):
+ self._walk_through_policy_directory(
+ path,
+ self._load_policy_file,
+ force_reload_policy_dir, False
+ )
for default in self.registered_rules.values():
if default.deprecated_for_removal:
@@ -771,6 +785,8 @@ class Enforcer(object):
# is in the cache
mtime = 0
if os.path.exists(path):
+ if not os.path.isdir(path):
+ raise ValueError('{} is not a directory'.format(path))
# Make a list of all the files
files = [path] + [os.path.join(path, file) for file in
os.listdir(path)]
@@ -809,14 +825,25 @@ class Enforcer(object):
self.file_rules[name] = RuleDefault(name, check_str)
def _load_policy_file(self, path, force_reload, overwrite=True):
+ """Load policy rules from the specified policy file.
+
+ :param str path: A path of the policy file to load rules from.
+ :param bool force_reload: Forcefully reload the policy file content.
+ :param bool overwrite: Replace policy rules instead of updating them.
+ :return: A bool indicating whether rules have been changed or not.
+ :rtype: bool
+ """
+ rules_changed = False
reloaded, data = _cache_handler.read_cached_file(
self._file_cache, path, force_reload=force_reload)
if reloaded or not self.rules:
rules = Rules.load(data, self.default_rule)
self.set_rules(rules, overwrite=overwrite, use_conf=True)
+ rules_changed = True
self._record_file_rules(data, overwrite)
self._loaded_files.append(path)
LOG.debug('Reloaded policy file: %(path)s', {'path': path})
+ return rules_changed
def _get_policy_path(self, path):
"""Locate the policy YAML/JSON data file/path.
diff --git a/oslo_policy/sphinxext.py b/oslo_policy/sphinxext.py
index d8b8ce0..a6c02b6 100644
--- a/oslo_policy/sphinxext.py
+++ b/oslo_policy/sphinxext.py
@@ -165,3 +165,7 @@ class ShowPolicyDirective(rst.Directive):
def setup(app):
app.add_directive('show-policy', ShowPolicyDirective)
+ return {
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }
diff --git a/oslo_policy/sphinxpolicygen.py b/oslo_policy/sphinxpolicygen.py
index 2693e34..1aef057 100644
--- a/oslo_policy/sphinxpolicygen.py
+++ b/oslo_policy/sphinxpolicygen.py
@@ -92,3 +92,7 @@ def setup(app):
app.add_config_value('policy_generator_config_file', None, 'env')
app.add_config_value('sample_policy_basename', None, 'env')
app.connect('builder-inited', generate_sample)
+ return {
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }
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/oslo_policy/tests/test_policy.py b/oslo_policy/tests/test_policy.py
index b67504b..6b5facf 100644
--- a/oslo_policy/tests/test_policy.py
+++ b/oslo_policy/tests/test_policy.py
@@ -296,6 +296,48 @@ class EnforcerTest(base.PolicyBaseTestCase):
os.path.join('policy.d', 'b.conf'),
])
+ def test_load_directory_after_file_update(self):
+ self.create_config_file(
+ os.path.join('policy.d', 'a.conf'), POLICY_A_CONTENTS)
+ self.enforcer.load_rules(True)
+ self.assertIsNotNone(self.enforcer.rules)
+ loaded_rules = jsonutils.loads(str(self.enforcer.rules))
+ self.assertEqual('role:fakeA', loaded_rules['default'])
+ self.assertEqual('is_admin:True', loaded_rules['admin'])
+ self.check_loaded_files([
+ 'policy.json',
+ os.path.join('policy.d', 'a.conf'),
+ ])
+ new_policy_json_contents = jsonutils.dumps({
+ "default": "rule:admin",
+ "admin": "is_admin:True",
+ "foo": "rule:bar",
+ })
+ # Modify the policy.json file and then validate that the rules
+ # from the policy directory are re-applied on top of the
+ # new rules from the file.
+ self.create_config_file('policy.json', new_policy_json_contents)
+ policy_file_path = self.get_config_file_fullname('policy.json')
+ # Force the mtime change since the unit test may write to this file
+ # too fast for mtime to actually change.
+ stinfo = os.stat(policy_file_path)
+ os.utime(policy_file_path, (stinfo.st_atime + 42,
+ stinfo.st_mtime + 42))
+
+ self.enforcer.load_rules()
+
+ self.assertIsNotNone(self.enforcer.rules)
+ loaded_rules = jsonutils.loads(str(self.enforcer.rules))
+ self.assertEqual('role:fakeA', loaded_rules['default'])
+ self.assertEqual('is_admin:True', loaded_rules['admin'])
+ self.assertEqual('rule:bar', loaded_rules['foo'])
+ self.check_loaded_files([
+ 'policy.json',
+ os.path.join('policy.d', 'a.conf'),
+ 'policy.json',
+ os.path.join('policy.d', 'a.conf'),
+ ])
+
def test_load_directory_opts_registered(self):
self._test_scenario_with_opts_registered(self.test_load_directory)
@@ -421,6 +463,7 @@ class EnforcerTest(base.PolicyBaseTestCase):
[os.path.join('policy.d', 'a.conf')],
group='oslo_policy')
self.assertRaises(ValueError, self.enforcer.load_rules, True)
+ self.assertRaises(ValueError, self.enforcer.load_rules, False)
@mock.patch('oslo_policy.policy.Enforcer.check_rules')
def test_load_rules_twice(self, mock_check_rules):
diff --git a/releasenotes/notes/bug-1880959-8f1370a59759d40d.yaml b/releasenotes/notes/bug-1880959-8f1370a59759d40d.yaml
new file mode 100644
index 0000000..929f509
--- /dev/null
+++ b/releasenotes/notes/bug-1880959-8f1370a59759d40d.yaml
@@ -0,0 +1,8 @@
+---
+fixes:
+ - |
+ [`bug 1880959 <https://bugs.launchpad.net/keystone/+bug/1880959>`_]
+ The behavior of policy file reloading from policy directories was fixed.
+ Previously the rules from policy files located in the directories
+ specified in the ``policy_dirs`` option were not reapplied after the rules
+ from the primary policy file have been reapplied due to a change.
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.
diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py
index 5084a9d..23e98c1 100644
--- a/releasenotes/source/conf.py
+++ b/releasenotes/source/conf.py
@@ -40,9 +40,9 @@ extensions = [
]
# openstackdocstheme options
-repository_name = 'openstack/oslo.policy'
-bug_project = 'oslo.policy'
-bug_tag = ''
+openstackdocs_repo_name = 'openstack/oslo.policy'
+openstackdocs_bug_project = 'oslo.policy'
+openstackdocs_bug_tag = ''
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -96,7 +96,7 @@ exclude_patterns = []
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = 'native'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
diff --git a/setup.cfg b/setup.cfg
index b94ba2d..056fb14 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -35,21 +35,8 @@ 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
https = oslo_policy._external:HttpsCheck
-
-[compile_catalog]
-directory = oslo_policy/locale
-domain = oslo_policy
-
-[update_catalog]
-domain = oslo_policy
-output_dir = oslo_policy/locale
-input_file = oslo_policy/locale/oslo_policy.pot
-
-[extract_messages]
-keywords = _ gettext ngettext l_ lazy_gettext
-mapping_file = babel.cfg
-output_file = oslo_policy/locale/oslo_policy.pot