Rename rbac_policy_parser to policy_authority
This change is a follow-up to commit
I8ba89ab5e134b15e97ac20a7aacbfd70896e192f
which introduced an abstract class from which (previously)
rbac_policy_parser and requirements authority inherit, providing
rbac_rule_validation with 2 ways of validating RBAC.
For the sake of naming consistency, rbac_policy_parser is renamed
to policy_authority. This naming scheme is better because
"policy parser" is implementation-specific and doesn't convey
what the file (and class name) do from a high-level perspective.
Because this file is only used internally to Patrole, it can be
changed without backward-compatibility concerns.
This commit also includes documentation for the policy authority
module and the rbac_rule_validation module.
Change-Id: Ie09fc2d884f9211244b062fdd5fe018970c2bb2d
diff --git a/patrole_tempest_plugin/policy_authority.py b/patrole_tempest_plugin/policy_authority.py
new file mode 100644
index 0000000..af227c4
--- /dev/null
+++ b/patrole_tempest_plugin/policy_authority.py
@@ -0,0 +1,264 @@
+# Copyright 2017 AT&T Corporation.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import copy
+import json
+import os
+
+from oslo_log import log as logging
+from oslo_policy import policy
+import stevedore
+from tempest import clients
+from tempest.common import credentials_factory as credentials
+from tempest import config
+
+from patrole_tempest_plugin import rbac_exceptions
+from patrole_tempest_plugin.rbac_utils import RbacAuthority
+
+CONF = config.CONF
+LOG = logging.getLogger(__name__)
+
+
+class PolicyAuthority(RbacAuthority):
+ """A class for parsing policy rules into lists of allowed roles.
+
+ RBAC testing requires that each rule in a policy file be broken up into
+ the roles that constitute it. This class automates that process.
+
+ The list of roles per rule can be reverse-engineered by checking, for
+ each role, whether a given rule is allowed using oslo policy.
+ """
+
+ def __init__(self, project_id, user_id, service, extra_target_data=None):
+ """Initialization of Rbac Policy Parser.
+
+ Parses a policy file to create a dictionary, mapping policy actions to
+ roles. If a policy file does not exist, checks whether the policy file
+ is registered as a namespace under oslo.policy.policies. Nova, for
+ example, doesn't use a policy.json file by default; its policy is
+ implemented in code and registered as 'nova' under
+ oslo.policy.policies.
+
+ If the policy file is not found in either place, raises an exception.
+
+ Additionally, if the policy file exists in both code and as a
+ policy.json (for example, by creating a custom nova policy.json file),
+ the custom policy file over the default policy implementation is
+ prioritized.
+
+ :param uuid project_id: project_id of object performing API call
+ :param uuid user_id: user_id of object performing API call
+ :param string service: service of the policy file
+ :param dict extra_target_data: dictionary containing additional object
+ data needed by oslo.policy to validate generic checks
+ """
+
+ if extra_target_data is None:
+ extra_target_data = {}
+
+ self.validate_service(service)
+
+ # Prioritize dynamically searching for policy files over relying on
+ # deprecated service-specific policy file locations.
+ if CONF.patrole.custom_policy_files:
+ self.discover_policy_files()
+ self.path = self.policy_files.get(service)
+ else:
+ self.path = getattr(CONF.patrole, '%s_policy_file' % str(service),
+ None)
+
+ self.rules = policy.Rules.load(self._get_policy_data(service),
+ 'default')
+ self.project_id = project_id
+ self.user_id = user_id
+ self.extra_target_data = extra_target_data
+
+ @classmethod
+ def validate_service(cls, service):
+ """Validate whether the service passed to ``__init__`` exists."""
+ service = service.lower().strip() if service else None
+
+ # Cache the list of available services in memory to avoid needlessly
+ # doing an API call every time.
+ if not hasattr(cls, 'available_services'):
+ admin_mgr = clients.Manager(
+ credentials.get_configured_admin_credentials())
+ services_client = (admin_mgr.identity_services_v3_client
+ if CONF.identity_feature_enabled.api_v3
+ else admin_mgr.identity_services_client)
+ services = services_client.list_services()['services']
+ cls.available_services = [s['name'] for s in services]
+
+ if not service or service not in cls.available_services:
+ LOG.debug("%s is NOT a valid service.", service)
+ raise rbac_exceptions.RbacInvalidService(
+ "%s is NOT a valid service." % 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
+ # out of the potential paths in ``CONF.patrole.custom_policy_files``.
+ if not hasattr(cls, 'policy_files'):
+ cls.policy_files = {}
+ 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)
+
+ def allowed(self, rule_name, role):
+ is_admin_context = self._is_admin_context(role)
+ is_allowed = self._allowed(
+ access=self._get_access_token(role),
+ apply_rule=rule_name,
+ is_admin=is_admin_context)
+ return is_allowed
+
+ def _get_policy_data(self, service):
+ 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):
+ try:
+ with open(self.path, 'r') as policy_file:
+ file_policy_data = policy_file.read()
+ file_policy_data = json.loads(file_policy_data)
+ except (IOError, ValueError) as e:
+ msg = "Failed to read policy file for service. "
+ if isinstance(e, IOError):
+ msg += "Please check that policy path exists."
+ 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],
+ 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]:
+ mgr_policy_data[rule.name] = str(rule.check)
+
+ # If data from both file and code exist, combine both together.
+ if file_policy_data and mgr_policy_data:
+ # Add the policy actions from code first.
+ for action, rule in mgr_policy_data.items():
+ policy_data[action] = rule
+ # Overwrite with any custom policy actions defined in policy.json.
+ for action, rule in file_policy_data.items():
+ policy_data[action] = rule
+ elif file_policy_data:
+ policy_data = file_policy_data
+ elif mgr_policy_data:
+ policy_data = mgr_policy_data
+ else:
+ error_message = (
+ 'Policy file for {0} service neither found in code nor at {1}.'
+ .format(service, [loc % service for loc in
+ CONF.patrole.custom_policy_files])
+ )
+ raise rbac_exceptions.RbacParsingException(error_message)
+
+ try:
+ policy_data = json.dumps(policy_data)
+ except ValueError:
+ error_message = 'Policy file for {0} service is invalid.'.format(
+ service)
+ raise rbac_exceptions.RbacParsingException(error_message)
+
+ return policy_data
+
+ def _is_admin_context(self, role):
+ """Checks whether a role has admin context.
+
+ If context_is_admin is contained in the policy file, then checks
+ whether the given role is contained in context_is_admin. If it is not
+ in the policy file, then default to context_is_admin: admin.
+ """
+ if 'context_is_admin' in self.rules.keys():
+ return self._allowed(
+ access=self._get_access_token(role),
+ apply_rule='context_is_admin')
+ return role == CONF.identity.admin_role
+
+ def _get_access_token(self, role):
+ access_token = {
+ "token": {
+ "roles": [
+ {
+ "name": role
+ }
+ ],
+ "project_id": self.project_id,
+ "tenant_id": self.project_id,
+ "user_id": self.user_id
+ }
+ }
+ return access_token
+
+ def _allowed(self, access, apply_rule, is_admin=False):
+ """Checks if a given rule in a policy is allowed with given access.
+
+ Adapted from oslo_policy.shell.
+
+ :param access: type dict: dictionary from ``_get_access_token``
+ :param apply_rule: type string: rule to be checked
+ :param is_admin: type bool: whether admin context is used
+ """
+ access_data = copy.copy(access['token'])
+ access_data['roles'] = [role['name'] for role in access_data['roles']]
+ access_data['is_admin'] = is_admin
+ # TODO(felipemonteiro): Dynamically calculate is_admin_project rather
+ # than hard-coding it to True. is_admin_project cannot be determined
+ # from the role, but rather from project and domain names. See
+ # _populate_is_admin_project in keystone.token.providers.common
+ # for more information.
+ access_data['is_admin_project'] = True
+
+ class Object(object):
+ pass
+ o = Object()
+ o.rules = self.rules
+
+ target = {"project_id": access_data['project_id'],
+ "tenant_id": access_data['project_id'],
+ "network:tenant_id": access_data['project_id'],
+ "user_id": access_data['user_id']}
+ if self.extra_target_data:
+ target.update(self.extra_target_data)
+
+ result = self._try_rule(apply_rule, target, access_data, o)
+ return result
+
+ 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}."\
+ .format(apply_rule, self.path)
+ LOG.debug(message)
+ raise rbac_exceptions.RbacParsingException(message)
+ else:
+ rule = self.rules[apply_rule]
+ return rule(target, access_data, o)