def _validate_switch_role(self, test_obj, toggle_rbac_role): """Validates that the rbac role passed to `switch_role` is legal. Throws an error for the following improper usages of `switch_role`: * `switch_role` is not called with a boolean value * `switch_role` is never called in a test file, except in tearDown * `switch_role` is called with the same boolean value twice """ if not isinstance(toggle_rbac_role, bool): raise rbac_exceptions.RbacResourceSetupFailed( 'toggle_rbac_role must be a boolean value.') # The unique key is the combination of module path plus class name. class_name = test_obj.__name__ if isinstance(test_obj, type) else \ test_obj.__class__.__name__ module_name = test_obj.__module__ key = '%s.%s' % (module_name, class_name) self.switch_role_history.setdefault(key, None) if self.switch_role_history[key] == toggle_rbac_role: # If an exception was thrown, like a skipException or otherwise, # then this is a legitimate reason why `switch_role` was not # called, so only raise an exception if no current exception is # being handled. if sys.exc_info()[0] is None: self.switch_role_history[key] = False error_message = '`toggle_rbac_role` must not be called with '\ 'the same bool value twice. Make sure that you included '\ 'a rbac_utils.switch_role method call inside the test.' LOG.error(error_message) raise rbac_exceptions.RbacResourceSetupFailed(error_message) else: self.switch_role_history[key] = toggle_rbac_role
def _is_authorized(test_obj, service, rule_name, extra_target_data): try: project_id = test_obj.auth_provider.credentials.project_id user_id = test_obj.auth_provider.credentials.user_id except AttributeError as e: msg = ("{0}: project_id/user_id not found in " "cls.auth_provider.credentials".format(e)) LOG.error(msg) raise rbac_exceptions.RbacResourceSetupFailed(msg) try: role = CONF.rbac.rbac_test_role formatted_target_data = _format_extra_target_data( test_obj, extra_target_data) policy_parser = rbac_policy_parser.RbacPolicyParser( project_id, user_id, service, extra_target_data=formatted_target_data) is_allowed = policy_parser.allowed(rule_name, role) if is_allowed: LOG.debug("[Action]: %s, [Role]: %s is allowed!", rule_name, role) else: LOG.debug("[Action]: %s, [Role]: %s is NOT allowed!", rule_name, role) return is_allowed except rbac_exceptions.RbacParsingException as e: if CONF.rbac.strict_policy_check: raise e else: raise testtools.TestCase.skipException(str(e)) return False
def wrapper(*args, **kwargs): if args and isinstance(args[0], test.BaseTestCase): test_obj = args[0] else: raise rbac_exceptions.RbacResourceSetupFailed( '`rbac_rule_validation` decorator can only be applied to ' 'an instance of `tempest.test.BaseTestCase`.') allowed = _is_authorized(test_obj, service, rule, extra_target_data, admin_only) expected_exception, irregular_msg = _get_exception_type( expected_error_code) test_status = 'Allowed' try: test_func(*args, **kwargs) except rbac_exceptions.RbacInvalidService as e: msg = ("%s is not a valid service." % service) test_status = ('Error, %s' % (msg)) LOG.error(msg) raise exceptions.NotFound("%s RbacInvalidService was: %s" % (msg, e)) except (expected_exception, rbac_exceptions.RbacConflictingPolicies, rbac_exceptions.RbacMalformedResponse) as e: test_status = 'Denied' if irregular_msg: LOG.warning(irregular_msg.format(rule, service)) if allowed: msg = ("Role %s was not allowed to perform %s." % (role, rule)) LOG.error(msg) raise exceptions.Forbidden("%s Exception was: %s" % (msg, e)) except Exception as e: with excutils.save_and_reraise_exception(): exc_info = sys.exc_info() error_details = six.text_type(exc_info[1]) msg = ("An unexpected exception has occurred during test: " "%s. Exception was: %s" % (test_func.__name__, error_details)) test_status = 'Error, %s' % (error_details) LOG.error(msg) else: if not allowed: LOG.error("Role %s was allowed to perform %s", role, rule) raise rbac_exceptions.RbacOverPermission( "OverPermission: Role %s was allowed to perform %s" % (role, rule)) finally: test_obj.rbac_utils.switch_role(test_obj, toggle_rbac_role=False) if CONF.patrole_log.enable_reporting: RBACLOG.info( "[Service]: %s, [Test]: %s, [Rule]: %s, " "[Expected]: %s, [Actual]: %s", service, test_func.__name__, rule, "Allowed" if allowed else "Denied", test_status)
def _is_authorized(test_obj, service, rule, extra_target_data): """Validates whether current RBAC role has permission to do policy action. :param test_obj: An instance or subclass of ``tempest.test.BaseTestCase``. :param service: The OpenStack service that enforces ``rule``. :param rule: The name of the policy action. Examples include "identity:create_user" or "os_compute_api:os-agents". :param extra_target_data: Dictionary, keyed with ``oslo.policy`` generic check names, whose values are string literals that reference nested ``tempest.test.BaseTestCase`` attributes. Used by ``oslo.policy`` for performing matching against attributes that are sent along with the API calls. :returns: True if the current RBAC role can perform the policy action, else False. :raises RbacResourceSetupFailed: If `project_id` or `user_id` are missing from the `auth_provider` attribute in `test_obj`. """ try: project_id = test_obj.os_primary.credentials.project_id user_id = test_obj.os_primary.credentials.user_id except AttributeError as e: msg = ("{0}: project_id or user_id not found in os_primary.credentials" .format(e)) LOG.error(msg) raise rbac_exceptions.RbacResourceSetupFailed(msg) roles = CONF.patrole.rbac_test_roles # TODO(vegasq) drop once CONF.patrole.rbac_test_role is removed if CONF.patrole.rbac_test_role: if not roles: roles.append(CONF.patrole.rbac_test_role) # Adding implied roles roles = test_obj.get_all_needed_roles(roles) # Test RBAC against custom requirements. Otherwise use oslo.policy. if CONF.patrole.test_custom_requirements: authority = requirements_authority.RequirementsAuthority( CONF.patrole.custom_requirements_file, service) else: formatted_target_data = _format_extra_target_data( test_obj, extra_target_data) authority = policy_authority.PolicyAuthority( project_id, user_id, service, extra_target_data=formatted_target_data) is_allowed = authority.allowed(rule, roles) if is_allowed: LOG.debug("[Policy action]: %s, [Role]: %s is allowed!", rule, roles) else: LOG.debug("[Policy action]: %s, [Role]: %s is NOT allowed!", rule, roles) return is_allowed
def wrapper(*args, **kwargs): if args and isinstance(args[0], test.BaseTestCase): test_obj = args[0] else: raise rbac_exceptions.RbacResourceSetupFailed( '`rbac_rule_validation` decorator can only be applied to ' 'an instance of `tempest.test.BaseTestCase`.') if admin_only: LOG.info("As admin_only is True, only admin role should be " "allowed to perform the API. Skipping oslo.policy " "check for policy action {0}.".format(rule)) allowed = test_obj.rbac_utils.is_admin else: allowed = _is_authorized(test_obj, service, rule, extra_target_data) expected_exception, irregular_msg = _get_exception_type( expected_error_code) try: func(*args, **kwargs) except rbac_exceptions.RbacInvalidService as e: msg = ("%s is not a valid service." % service) LOG.error(msg) raise exceptions.NotFound("%s RbacInvalidService was: %s" % (msg, e)) except (expected_exception, rbac_exceptions.RbacActionFailed) as e: if irregular_msg: LOG.warning(irregular_msg.format(rule, service)) if allowed: msg = ("Role %s was not allowed to perform %s." % (role, rule)) LOG.error(msg) raise exceptions.Forbidden("%s Exception was: %s" % (msg, e)) except Exception as e: exc_info = sys.exc_info() error_details = exc_info[1].__str__() msg = ("%s An unexpected exception has occurred: Expected " "exception was %s, which was not thrown." % (error_details, expected_exception.__name__)) LOG.error(msg) six.reraise(exc_info[0], exc_info[0](msg), exc_info[2]) else: if not allowed: LOG.error("Role %s was allowed to perform %s", role, rule) raise rbac_exceptions.RbacOverPermission( "OverPermission: Role %s was allowed to perform %s" % (role, rule)) finally: test_obj.rbac_utils.switch_role(test_obj, toggle_rbac_role=False)
def _get_roles_by_name(self): available_roles = self.admin_roles_client.list_roles() admin_role_id = rbac_role_id = None for role in available_roles['roles']: if role['name'] == CONF.patrole.rbac_test_role: rbac_role_id = role['id'] if role['name'] == CONF.identity.admin_role: admin_role_id = role['id'] if not all([admin_role_id, rbac_role_id]): msg = ("Roles defined by `[patrole] rbac_test_role` and " "`[identity] admin_role` must be defined in the system.") raise rbac_exceptions.RbacResourceSetupFailed(msg) self.admin_role_id = admin_role_id self.rbac_role_id = rbac_role_id
def _get_roles(self): available_roles = self.roles_client.list_roles() admin_role_id = rbac_role_id = None for role in available_roles['roles']: if role['name'] == CONF.rbac.rbac_test_role: rbac_role_id = role['id'] if role['name'] == CONF.identity.admin_role: admin_role_id = role['id'] if not admin_role_id or not rbac_role_id: msg = "Role with name 'admin' does not exist in the system."\ if not admin_role_id else "Role defined by rbac_test_role "\ "does not exist in the system." raise rbac_exceptions.RbacResourceSetupFailed(msg) self.admin_role_id = admin_role_id self.rbac_role_id = rbac_role_id
def switch_role(self, test_obj, toggle_rbac_role=False): self.user_id = test_obj.auth_provider.credentials.user_id self.project_id = test_obj.auth_provider.credentials.tenant_id self.token = test_obj.auth_provider.get_token() self.identity_version = test_obj.get_identity_version() if not credentials.is_admin_available( identity_version=self.identity_version): msg = "Missing Identity Admin API credentials in configuration." raise rbac_exceptions.RbacResourceSetupFailed(msg) self.roles_client = test_obj.os_admin.roles_v3_client LOG.debug('Switching role to: %s', toggle_rbac_role) try: if not self.admin_role_id or not self.rbac_role_id: self._get_roles() self._validate_switch_role(test_obj, toggle_rbac_role) if toggle_rbac_role: self._add_role_to_user(self.rbac_role_id) else: self._add_role_to_user(self.admin_role_id) except Exception as exp: LOG.error(exp) raise finally: # NOTE(felipemonteiro): These two comments below are copied from # tempest.api.identity.v2/v3.test_users. # # Reset auth again to verify the password restore does work. # Clear auth restores the original credentials and deletes # cached auth data. test_obj.auth_provider.clear_auth() # Fernet tokens are not subsecond aware and Keystone should only be # precise to the second. Sleep to ensure we are passing the second # boundary before attempting to authenticate. If token is of type # uuid, then do not sleep. if not uuid_utils.is_uuid_like(self.token): time.sleep(1) test_obj.auth_provider.set_auth()
def _init_roles(cls): available_roles = cls.admin_roles_client.list_roles()['roles'] cls._role_map = {r['name']: r['id'] for r in available_roles} LOG.debug('Available roles: %s', cls._role_map.keys()) rbac_role_ids = [] roles = CONF.patrole.rbac_test_roles # TODO(vegasq) drop once CONF.patrole.rbac_test_role is removed if CONF.patrole.rbac_test_role: if not roles: roles.append(CONF.patrole.rbac_test_role) for role_name in roles: rbac_role_ids.append(cls._role_map.get(role_name)) admin_role_id = cls._role_map.get(CONF.identity.admin_role) if not all([admin_role_id, all(rbac_role_ids)]): missing_roles = [] msg = ("Could not find `[patrole] rbac_test_roles` or " "`[identity] admin_role`, both of which are required for " "RBAC testing.") if not admin_role_id: missing_roles.append(CONF.identity.admin_role) if not all(rbac_role_ids): missing_roles += [ role_name for role_name in roles if role_name not in cls._role_map ] msg += " Following roles were not found: %s." % ( ", ".join(missing_roles)) msg += " Available roles: %s." % ", ".join(cls._role_map) raise rbac_exceptions.RbacResourceSetupFailed(msg) cls._admin_role_id = admin_role_id cls._rbac_role_ids = rbac_role_ids # Adding backward mapping cls._role_map.update({v: k for k, v in cls._role_map.items()})
def _get_roles_by_name(self): available_roles = self.admin_roles_client.list_roles()['roles'] role_map = {r['name']: r['id'] for r in available_roles} LOG.debug('Available roles: %s', list(role_map.keys())) admin_role_id = role_map.get(CONF.identity.admin_role) rbac_role_id = role_map.get(CONF.patrole.rbac_test_role) if not all([admin_role_id, rbac_role_id]): missing_roles = [] msg = ("Could not find `[patrole] rbac_test_role` or " "`[identity] admin_role`, both of which are required for " "RBAC testing.") if not admin_role_id: missing_roles.append(CONF.identity.admin_role) if not rbac_role_id: missing_roles.append(CONF.patrole.rbac_test_role) msg += " Following roles were not found: %s." % ( ", ".join(missing_roles)) msg += " Available roles: %s." % ", ".join(list(role_map.keys())) raise rbac_exceptions.RbacResourceSetupFailed(msg) self.admin_role_id = admin_role_id self.rbac_role_id = rbac_role_id
def wrapper(*args, **kwargs): if args and isinstance(args[0], test.BaseTestCase): test_obj = args[0] else: raise rbac_exceptions.RbacResourceSetupFailed( '`rbac_rule_validation` decorator can only be applied to ' 'an instance of `tempest.test.BaseTestCase`.') allowed = True disallowed_rules = [] for rule in rules: _allowed = _is_authorized(test_obj, service, rule, extra_target_data) if not _allowed: disallowed_rules.append(rule) allowed = allowed and _allowed exp_error_code = expected_error_code if disallowed_rules: # Choose the first disallowed rule and expect the error # code corresponding to it. first_error_index = rules.index(disallowed_rules[0]) exp_error_code = expected_error_codes[first_error_index] LOG.debug("%s: Expecting %d to be raised for policy name: %s", test_func.__name__, exp_error_code, disallowed_rules[0]) expected_exception, irregular_msg = _get_exception_type( exp_error_code) caught_exception = None test_status = 'Allowed' try: test_func(*args, **kwargs) except rbac_exceptions.RbacInvalidServiceException: with excutils.save_and_reraise_exception(): msg = ("%s is not a valid service." % service) # FIXME(felipemonteiro): This test_status is logged too # late. Need a function to log it before re-raising. test_status = ('Error, %s' % (msg)) LOG.error(msg) except (expected_exception, rbac_exceptions.RbacConflictingPolicies, rbac_exceptions.RbacMalformedResponse) as actual_exception: caught_exception = actual_exception test_status = 'Denied' if irregular_msg: LOG.warning(irregular_msg, test_func.__name__, ', '.join(rules), service) if allowed: msg = ("Role %s was not allowed to perform the following " "actions: %s. Expected allowed actions: %s. " "Expected disallowed actions: %s." % (role, sorted(rules), sorted(set(rules) - set(disallowed_rules)), sorted(disallowed_rules))) LOG.error(msg) raise rbac_exceptions.RbacUnderPermissionException( "%s Exception was: %s" % (msg, actual_exception)) except Exception as actual_exception: caught_exception = actual_exception if _check_for_expected_mismatch_exception( expected_exception, actual_exception): LOG.error( 'Expected and actual exceptions do not match. ' 'Expected: %s. Actual: %s.', expected_exception, actual_exception.__class__) raise rbac_exceptions.RbacExpectedWrongException( expected=expected_exception, actual=actual_exception.__class__, exception=actual_exception) else: with excutils.save_and_reraise_exception(): exc_info = sys.exc_info() error_details = six.text_type(exc_info[1]) msg = ("An unexpected exception has occurred during " "test: %s. Exception was: %s" % (test_func.__name__, error_details)) test_status = 'Error, %s' % (error_details) LOG.error(msg) else: if not allowed: msg = ( "OverPermission: Role %s was allowed to perform the " "following disallowed actions: %s" % (role, sorted(disallowed_rules))) LOG.error(msg) raise rbac_exceptions.RbacOverPermissionException(msg) finally: if CONF.patrole_log.enable_reporting: RBACLOG.info( "[Service]: %s, [Test]: %s, [Rules]: %s, " "[Expected]: %s, [Actual]: %s", service, test_func.__name__, ', '.join(rules), "Allowed" if allowed else "Denied", test_status) # Sanity-check that ``override_role`` was called to eliminate # false-positives and bad test flows resulting from exceptions # getting raised too early, too late or not at all, within # the scope of an RBAC test. _validate_override_role_called( test_obj, actual_exception=caught_exception)
def _is_authorized(test_obj, service, rule, extra_target_data, admin_only): """Validates whether current RBAC role has permission to do policy action. :param test_obj: An instance or subclass of ``tempest.test.BaseTestCase``. :param service: The OpenStack service that enforces ``rule``. :param rule: The name of the policy action. Examples include "identity:create_user" or "os_compute_api:os-agents". :param extra_target_data: Dictionary, keyed with ``oslo.policy`` generic check names, whose values are string literals that reference nested ``tempest.test.BaseTestCase`` attributes. Used by ``oslo.policy`` for performing matching against attributes that are sent along with the API calls. :param admin_only: Skips over ``oslo.policy`` check because the policy action defined by ``rule`` is not enforced by the service's policy enforcement engine. For example, Keystone v2 performs an admin check for most of its endpoints. If True, ``rule`` is effectively ignored. :returns: True if the current RBAC role can perform the policy action, else False. :raises RbacResourceSetupFailed: If `project_id` or `user_id` are missing from the `auth_provider` attribute in `test_obj`. :raises RbacParsingException: if ``[patrole] strict_policy_check`` is True and the ``rule`` does not exist in the system. :raises skipException: If ``[patrole] strict_policy_check`` is False and the ``rule`` does not exist in the system. """ if admin_only: LOG.info("As admin_only is True, only admin role should be " "allowed to perform the API. Skipping oslo.policy " "check for policy action {0}.".format(rule)) return rbac_utils.is_admin() try: project_id = test_obj.os_primary.credentials.project_id user_id = test_obj.os_primary.credentials.user_id except AttributeError as e: msg = ("{0}: project_id or user_id not found in os_primary.credentials" .format(e)) LOG.error(msg) raise rbac_exceptions.RbacResourceSetupFailed(msg) try: role = CONF.patrole.rbac_test_role # Test RBAC against custom requirements. Otherwise use oslo.policy. if CONF.patrole.test_custom_requirements: authority = requirements_authority.RequirementsAuthority( CONF.patrole.custom_requirements_file, service) else: formatted_target_data = _format_extra_target_data( test_obj, extra_target_data) authority = policy_authority.PolicyAuthority( project_id, user_id, service, extra_target_data=formatted_target_data) is_allowed = authority.allowed(rule, role) if is_allowed: LOG.debug("[Action]: %s, [Role]: %s is allowed!", rule, role) else: LOG.debug("[Action]: %s, [Role]: %s is NOT allowed!", rule, role) return is_allowed except rbac_exceptions.RbacParsingException as e: if CONF.patrole.strict_policy_check: raise e else: raise testtools.TestCase.skipException(str(e)) return False