Identity trust rbac tests

Add identity trust rbac tests corresponding to the policy actions
in [0].

Most of the policy actions in [0] have a rule of '' (empty string),
meaning any role can perform the action. However, the
"identity:create_trust" policy action has a rule of
base.RULE_TRUST_OWNER which translates to:

  user_id:%(trust.trustor_user_id)s [1].

This is a rather unique rule, one that is not dependent on the
current user's user_id, project_id or even role. Rather, this
rule translates to: "Does the current user's user_id match
the user_id of the trustor creating a trust with a trustee?"

As should be expected, "trust.trustor_user_id" can only be
dynamically calculated at runtime, rather than immediately
retrieved from a tempest credential variable, as is the case
with user_id and project_id.

Hence, this patch not only 1) creates trust rbac tests but 2)
enhances the framework, particularly the rbac_rule_validation
decorator, as well as the rbac_policy_parser framework, to
handle additional target data that must be passed to
oslo-policy-checker in order for proper authorization
determination.

The "target" parameter in oslo.policy is a dictionary that
contains "As much information about the object being
operated on as possible" [2]. Accordingly, the
rbac_rule_validation decorator has been enhanced with a new
param called `extra_target_data` that is a dictionary
containing key-value pairs of dynamically calculated
data needed by oslo.policy to correctly determine whether
the "target" has authorization to perform a policy action.

For example,

    extra_target_data={
        "trust.trustor_user_id": "os.auth_provider.credentials.user_id"
    })

means that trust.trustor_user_id equals the primary credential's
user_id. So, if a trustor's user_id equals the primary credential's
user_id, then the policy parser will return True for `is_allowed`.
However, if a trustor's user_id doesn'tequal to the primary credential's
user_id, but rather the alt credential's user_id, say, then `is_allowed`
returns False.

Thus, the only way to do negative testing with test_trusts_rbac is
to explicitly create a negative test, as described in the above
paragraph, which is what this patch does. Normally, negative testing
is baked in, dependent on the `rbac_test_role`, but with more
complicated policy enforcement as described above, this is not
possible.

While it is possible to create a new CONF value called trustor_user_id,
it would require using pre-provisioned credentials, which is something
Patrole doesn't explicitly use.

[0] https://github.com/openstack/keystone/blob/master/keystone/common/policies/trust.py
[1] https://github.com/openstack/keystone/blob/master/keystone/common/policies/base.py
[2] https://docs.openstack.org/developer/oslo.policy/api/oslo_policy.html

Depends-On: Ib82e8b8a0d6c8587fb0b1ce415e751c3ebc3c2f9
Change-Id: I5c00fdb345556066343bdaeb5f008d639a94bc4b
diff --git a/patrole_tempest_plugin/rbac_rule_validation.py b/patrole_tempest_plugin/rbac_rule_validation.py
index ec63119..6a5ed5e 100644
--- a/patrole_tempest_plugin/rbac_rule_validation.py
+++ b/patrole_tempest_plugin/rbac_rule_validation.py
@@ -29,7 +29,8 @@
 LOG = logging.getLogger(__name__)
 
 
-def action(service, rule, admin_only=False, expected_error_code=403):
+def action(service, rule, admin_only=False, expected_error_code=403,
+           extra_target_data={}):
     """A decorator which does a policy check and matches it against test run.
 
     A decorator which allows for positive and negative RBAC testing. Given
@@ -66,10 +67,10 @@
                 caller_ref = None
                 if args and isinstance(args[0], test.BaseTestCase):
                     caller_ref = args[0]
-                tenant_id = caller_ref.auth_provider.credentials.tenant_id
+                project_id = caller_ref.auth_provider.credentials.project_id
                 user_id = caller_ref.auth_provider.credentials.user_id
             except AttributeError as e:
-                msg = ("{0}: tenant_id/user_id not found in "
+                msg = ("{0}: project_id/user_id not found in "
                        "cls.auth_provider.credentials".format(e))
                 LOG.error(msg)
                 raise rbac_exceptions.RbacResourceSetupFailed(msg)
@@ -80,10 +81,11 @@
                          "check for policy action {0}.".format(rule))
                 allowed = CONF.rbac.rbac_test_role == 'admin'
             else:
-                authority = rbac_auth.RbacAuthority(tenant_id, user_id,
-                                                    service)
-                allowed = authority.get_permission(rule,
-                                                   CONF.rbac.rbac_test_role)
+                authority = rbac_auth.RbacAuthority(
+                    project_id, user_id, service,
+                    _format_extra_target_data(caller_ref, extra_target_data))
+                allowed = authority.get_permission(
+                    rule, CONF.rbac.rbac_test_role)
 
             expected_exception, irregular_msg = _get_exception_type(
                 expected_error_code)
@@ -146,3 +148,34 @@
         raise rbac_exceptions.RbacInvalidErrorCode()
 
     return expected_exception, irregular_msg
+
+
+def _format_extra_target_data(test_obj, extra_target_data):
+    """Formats the "extra_target_data" dictionary with correct test data.
+
+    Before being formatted, "extra_target_data" is a dictionary that maps a
+    policy string like "trust.trustor_user_id" to a nested list of BaseTestCase
+    attributes. For example, the attribute list in:
+
+        "trust.trustor_user_id": "os.auth_provider.credentials.user_id"
+
+    is parsed by iteratively calling `getattr` until the value of "user_id"
+    is resolved. The resulting dictionary returns:
+
+        "trust.trustor_user_id": "the user_id of the `primary` credential"
+
+    :param test_obj: type BaseTestCase (tempest base test class)
+    :param extra_target_data: dictionary with unresolved string literals that
+                              reference nested BaseTestCase attributes
+    :returns: dictionary with resolved BaseTestCase attributes
+    """
+    attr_value = test_obj
+    formatted_target_data = {}
+
+    for user_attribute, attr_string in extra_target_data.items():
+        attrs = attr_string.split('.')
+        for attr in attrs:
+            attr_value = getattr(attr_value, attr)
+        formatted_target_data[user_attribute] = attr_value
+
+    return formatted_target_data