Add support for multiple policy files
Most of the neutron plugins provide an updated version of policy.json
file with full list of rules, but at the same time there are a lot of
other plugins which provide their own policy files and store them in
the policy.d/ folder: neutron-fwaas, networking-bgpvpn, vmware-nsx,
ect...
To implement the tests for such plugins the Patrole should be able to
load and merge multiple policy files for any of the services.
Modify the discover_policy_files function to discover all policy files
for each of the services. Using glob.glob() function makes it possible
to use patterns like '*.json' to discover the policy files.
Modify the _get_policy_data function to load a data from all discovered
policy files for a service.
Update the unit test according to the changes.
Change-Id: Ib24f3d6d7a5ffdeaecce579af9795fd897dce872
diff --git a/patrole_tempest_plugin/policy_authority.py b/patrole_tempest_plugin/policy_authority.py
index 3339a5d..27786ae 100644
--- a/patrole_tempest_plugin/policy_authority.py
+++ b/patrole_tempest_plugin/policy_authority.py
@@ -13,7 +13,9 @@
# License for the specific language governing permissions and limitations
# under the License.
+import collections
import copy
+import glob
import json
import os
@@ -103,17 +105,14 @@
if extra_target_data is None:
extra_target_data = {}
- self.validate_service(service)
+ self.service = self.validate_service(service)
# Prioritize dynamically searching for policy files over relying on
# deprecated service-specific policy file locations.
- self.path = None
if CONF.patrole.custom_policy_files:
self.discover_policy_files()
- self.path = self.policy_files.get(service)
- self.rules = policy.Rules.load(self._get_policy_data(service),
- 'default')
+ self.rules = policy.Rules.load(self._get_policy_data(), 'default')
self.project_id = project_id
self.user_id = user_id
self.extra_target_data = extra_target_data
@@ -139,19 +138,22 @@
raise rbac_exceptions.RbacInvalidServiceException(
"%s is NOT a valid service." % service)
+ return service
+
@classmethod
def discover_policy_files(cls):
"""Dynamically discover the policy file for each service in
- ``cls.available_services``. Pick the first candidate path found
+ ``cls.available_services``. Pick all candidate paths found
out of the potential paths in ``[patrole] custom_policy_files``.
"""
if not hasattr(cls, 'policy_files'):
- cls.policy_files = {}
+ cls.policy_files = collections.defaultdict(list)
for service in cls.available_services:
for candidate_path in CONF.patrole.custom_policy_files:
- if os.path.isfile(candidate_path % service):
- cls.policy_files.setdefault(service,
- candidate_path % service)
+ path = candidate_path % service
+ for filename in glob.iglob(path):
+ if os.path.isfile(filename):
+ cls.policy_files[service].append(filename)
def allowed(self, rule_name, role):
"""Checks if a given rule in a policy is allowed with given role.
@@ -168,17 +170,28 @@
is_admin=is_admin_context)
return is_allowed
- def _get_policy_data(self, service):
+ def _get_policy_data(self):
file_policy_data = {}
mgr_policy_data = {}
policy_data = {}
# Check whether policy file exists and attempt to read it.
- if self.path and os.path.isfile(self.path):
+ for path in self.policy_files[self.service]:
try:
- with open(self.path, 'r') as policy_file:
- file_policy_data = policy_file.read()
- file_policy_data = json.loads(file_policy_data)
+ with open(path, 'r') as fp:
+ for k, v in json.load(fp).items():
+ if k not in file_policy_data:
+ file_policy_data[k] = v
+ else:
+ # If the policy name and rule are the same, no
+ # ambiguity, so no reason to warn.
+ if v != file_policy_data[k]:
+ LOG.warning(
+ "The same policy name: %s was found in "
+ "multiple policies files for service %s. "
+ "This can lead to policy rule ambiguity. "
+ "Using rule: %s", k, self.service,
+ file_policy_data[k])
except (IOError, ValueError) as e:
msg = "Failed to read policy file for service. "
if isinstance(e, IOError):
@@ -186,21 +199,20 @@
else:
msg += "JSON may be improperly formatted."
LOG.debug(msg)
- file_policy_data = {}
# Check whether policy actions are defined in code. Nova and Keystone,
# for example, define their default policy actions in code.
mgr = stevedore.named.NamedExtensionManager(
'oslo.policy.policies',
- names=[service],
+ names=[self.service],
on_load_failure_callback=None,
invoke_on_load=True,
warn_on_missing_entrypoint=False)
if mgr:
- policy_generator = {policy.name: policy.obj for policy in mgr}
- if policy_generator and service in policy_generator:
- for rule in policy_generator[service]:
+ policy_generator = {plc.name: plc.obj for plc in mgr}
+ if policy_generator and self.service in policy_generator:
+ for rule in policy_generator[self.service]:
mgr_policy_data[rule.name] = str(rule.check)
# If data from both file and code exist, combine both together.
@@ -217,10 +229,10 @@
policy_data = mgr_policy_data
else:
error_message = (
- 'Policy file for {0} service was not found among the '
+ 'Policy files for {0} service were not found among the '
'registered in-code policies or in any of the possible policy '
- 'files: {1}.'.format(service,
- [loc % service for loc in
+ 'files: {1}.'.format(self.service,
+ [loc % self.service for loc in
CONF.patrole.custom_policy_files])
)
raise rbac_exceptions.RbacParsingException(error_message)
@@ -228,8 +240,8 @@
try:
policy_data = json.dumps(policy_data)
except (TypeError, ValueError):
- error_message = 'Policy file for {0} service is invalid.'.format(
- service)
+ error_message = 'Policy files for {0} service are invalid.'.format(
+ self.service)
raise rbac_exceptions.RbacParsingException(error_message)
return policy_data
@@ -296,9 +308,11 @@
def _try_rule(self, apply_rule, target, access_data, o):
if apply_rule not in self.rules:
- message = ("Policy action \"{0}\" not found in policy file: {1} or"
- " among registered policy in code defaults for service."
- ).format(apply_rule, self.path)
+ message = ('Policy action "{0}" not found in policy files: '
+ '{1} or among registered policy in code defaults for '
+ '{2} service.').format(apply_rule,
+ self.policy_files[self.service],
+ self.service)
LOG.debug(message)
raise rbac_exceptions.RbacParsingException(message)
else: