Initial functionality framework.
Includes:
rbac_util - Utility for switching between roles for tests.
rbac_auth - Determines if a given role is valid for a given api call.
rbac_rule_validation - Determines if a allowed proper access and denied improper access (403 error)
rbac_role_converter - Converts policy.json files into a list of api's and the roles that can access them.

One example rbac_base in tests/api/rbac_base
One example test in tests/api/images/test_images_rbac.py

New config settings for rbac_flag, rbac_test_role, and rbac_roles

Implements bp: initial-framework
Co-Authored-By: Sangeet Gupta <sg774j@att.com>
Co-Authored-By: Rick Bartra <rb560u@att.com>
Co-Authored-By: Felipe Monteiro <felipe.monteiro@att.com>
Co-Authored-By: Anthony Bellino <ab2434@att.com>
Co-Authored-By: Avishek Dutta <ad620p@att.com>

Change-Id: Ic97b2558ba33ab47ac8174ae37629d36ceb1c9de
diff --git a/tests/custom_rbac_policy.json b/tests/custom_rbac_policy.json
new file mode 100644
index 0000000..0e7466a
--- /dev/null
+++ b/tests/custom_rbac_policy.json
@@ -0,0 +1,14 @@
+{
+    "even_rule": "role:two or role:four or role:six or role:eight",
+    "odd_rule": "role:one or role:three or role:five or role:seven or role:nine",
+    "zero_rule": "role:zero",
+    "prime_rule": "role:one or role:two or role:three or role:five or role:seven",
+    "all_rule": "",
+
+    "policy_action_1": "rule:even_rule",
+    "policy_action_2": "rule:odd_rule",
+    "policy_action_3": "rule:zero_rule",
+    "policy_action_4": "rule:prime_rule",
+    "policy_action_5": "rule:all_rule",
+    "policy_action_6": "role:eight",
+}
diff --git a/tests/test_rbac_role_converter.py b/tests/test_rbac_role_converter.py
new file mode 100644
index 0000000..942d7d0
--- /dev/null
+++ b/tests/test_rbac_role_converter.py
@@ -0,0 +1,67 @@
+#    Copyright 2017 AT&T Inc.
+#
+#    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 os
+
+from tempest import config
+from tempest.tests import base
+
+from patrole_tempest_plugin import rbac_role_converter
+
+CONF = config.CONF
+
+
+class RbacPolicyTest(base.TestCase):
+
+    def setUp(self):
+        super(RbacPolicyTest, self).setUp()
+
+        current_directory = os.path.dirname(os.path.realpath(__file__))
+        self.custom_policy_file = os.path.join(current_directory,
+                                               'custom_rbac_policy.json')
+
+    def test_custom_policy(self):
+        default_roles = ['zero', 'one', 'two', 'three', 'four',
+                         'five', 'six', 'seven', 'eight', 'nine']
+        CONF.set_override('rbac_roles', default_roles, group='rbac',
+                          enforce_type=True)
+
+        self.converter = rbac_role_converter.RbacPolicyConverter(
+            "custom",
+            self.custom_policy_file
+        )
+        self.roles_dict = self.converter.rules
+
+        expected = {
+            'policy_action_1': ['two', 'four', 'six', 'eight'],
+            'policy_action_2': ['one', 'three', 'five', 'seven', 'nine'],
+            'policy_action_3': ['zero'],
+            'policy_action_4': ['one', 'two', 'three', 'five', 'seven'],
+            'policy_action_5': ['zero', 'one', 'two', 'three', 'four', 'five',
+                                'six', 'seven', 'eight', 'nine'],
+            'policy_action_6': ['eight'],
+        }
+
+        fake_rule = 'fake_rule'
+
+        self.assertFalse(fake_rule in self.roles_dict.keys())
+
+        for rule in expected.keys():
+            self.assertTrue(rule in self.roles_dict.keys())
+            expected_roles = expected[rule]
+            unexpected_roles = set(default_roles) - set(expected[rule])
+            for role in expected_roles:
+                self.assertTrue(role in self.roles_dict[rule])
+            for role in unexpected_roles:
+                self.assertFalse(role in self.roles_dict[rule])
diff --git a/tests/test_rbac_rule_validation.py b/tests/test_rbac_rule_validation.py
new file mode 100644
index 0000000..a8e2be3
--- /dev/null
+++ b/tests/test_rbac_rule_validation.py
@@ -0,0 +1,89 @@
+#    Copyright 2017 AT&T Inc.
+#
+#    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 mock
+
+from patrole_tempest_plugin import rbac_exceptions
+from patrole_tempest_plugin import rbac_rule_validation as rbac_rv
+
+from tempest.lib import exceptions
+
+from tempest.tests import base
+
+
+class RBACRuleValidationTest(base.TestCase):
+    @mock.patch('patrole_tempest_plugin.rbac_auth.RbacAuthority')
+    def test_RBAC_rv_happy_path(self, mock_auth):
+        decorator = rbac_rv.action("", "", "")
+        mock_function = mock.Mock()
+        wrapper = decorator(mock_function)
+        wrapper()
+        self.assertTrue(mock_function.called)
+
+    @mock.patch('patrole_tempest_plugin.rbac_auth.RbacAuthority')
+    def test_RBAC_rv_forbidden(self, mock_auth):
+        decorator = rbac_rv.action("", "", "")
+        mock_function = mock.Mock()
+        mock_function.side_effect = exceptions.Forbidden
+        wrapper = decorator(mock_function)
+        self.assertRaises(exceptions.Forbidden, wrapper)
+
+    @mock.patch('patrole_tempest_plugin.rbac_auth.RbacAuthority')
+    def test_RBAC_rv_rbac_action_failed(self, mock_auth):
+        decorator = rbac_rv.action("", "", "")
+        mock_function = mock.Mock()
+        mock_function.side_effect = rbac_exceptions.RbacActionFailed
+        wrapper = decorator(mock_function)
+        self.assertRaises(exceptions.Forbidden, wrapper)
+
+    @mock.patch('patrole_tempest_plugin.rbac_auth.RbacAuthority')
+    def test_RBAC_rv_not_allowed(self, mock_auth):
+        decorator = rbac_rv.action("", "", "")
+
+        mock_function = mock.Mock()
+        wrapper = decorator(mock_function)
+
+        mock_permission = mock.Mock()
+        mock_permission.get_permission.return_value = False
+        mock_auth.return_value = mock_permission
+
+        self.assertRaises(rbac_exceptions.RbacOverPermission, wrapper)
+
+    @mock.patch('patrole_tempest_plugin.rbac_auth.RbacAuthority')
+    def test_RBAC_rv_forbidden_not_allowed(self, mock_auth):
+        decorator = rbac_rv.action("", "", "")
+
+        mock_function = mock.Mock()
+        mock_function.side_effect = exceptions.Forbidden
+        wrapper = decorator(mock_function)
+
+        mock_permission = mock.Mock()
+        mock_permission.get_permission.return_value = False
+        mock_auth.return_value = mock_permission
+
+        self.assertIsNone(wrapper())
+
+    @mock.patch('patrole_tempest_plugin.rbac_auth.RbacAuthority')
+    def test_RBAC_rv_rbac_action_failed_not_allowed(self, mock_auth):
+        decorator = rbac_rv.action("", "", "")
+
+        mock_function = mock.Mock()
+        mock_function.side_effect = rbac_exceptions.RbacActionFailed
+        wrapper = decorator(mock_function)
+
+        mock_permission = mock.Mock()
+        mock_permission.get_permission.return_value = False
+        mock_auth.return_value = mock_permission
+
+        self.assertIsNone(wrapper())
diff --git a/tests/test_rbac_utils.py b/tests/test_rbac_utils.py
new file mode 100644
index 0000000..657b389
--- /dev/null
+++ b/tests/test_rbac_utils.py
@@ -0,0 +1,198 @@
+#    Copyright 2017 AT&T Inc.
+#
+#    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 json
+import mock
+
+from tempest.tests import base
+
+from patrole_tempest_plugin import rbac_exceptions
+from patrole_tempest_plugin import rbac_utils as utils
+
+
+class RBACUtilsTest(base.TestCase):
+    def setUp(self):
+        super(RBACUtilsTest, self).setUp()
+        self.rbac_utils = utils.RbacUtils
+
+    get_response = 200
+    put_response = 204
+    delete_response = 204
+    response_data = json.dumps({"roles": []})
+
+    def _response_side_effect(self, action, *args, **kwargs):
+        response = mock.MagicMock()
+        if action == "GET":
+            response.status = self.get_response
+            response.data = self.response_data
+        if action == "PUT":
+            response.status = self.put_response
+        if action == "DELETE":
+            response.status = self.delete_response
+        return response
+
+    @mock.patch('patrole_tempest_plugin.rbac_utils.CONF')
+    @mock.patch('patrole_tempest_plugin.rbac_utils.http')
+    def test_RBAC_utils_get_roles(self, http, config):
+        self.rbac_utils.dictionary = {}
+
+        caller = mock.Mock()
+        caller.admin_client.token = "test_token"
+
+        http.request.side_effect = self._response_side_effect
+
+        self.assertEqual({'admin_role_id': None, 'rbac_role_id': None},
+                         self.rbac_utils.get_roles(caller))
+
+    @mock.patch('patrole_tempest_plugin.rbac_utils.CONF')
+    @mock.patch('patrole_tempest_plugin.rbac_utils.http')
+    def test_RBAC_utils_get_roles_member(self, http, config):
+        self.rbac_utils.dictionary = {}
+
+        caller = mock.Mock()
+        caller.admin_client.token = "test_token"
+
+        self.response_data = json.dumps({'roles': [{'name': '_member_',
+                                         'id': '_member_id'}]})
+        http.request.side_effect = self._response_side_effect
+
+        config.rbac.rbac_test_role = '_member_'
+
+        self.assertEqual({'admin_role_id': None,
+                          'rbac_role_id': '_member_id'},
+                         self.rbac_utils.get_roles(caller))
+
+    @mock.patch('patrole_tempest_plugin.rbac_utils.CONF')
+    @mock.patch('patrole_tempest_plugin.rbac_utils.http')
+    def test_RBAC_utils_get_roles_admin(self, http, config):
+        self.rbac_utils.dictionary = {}
+
+        caller = mock.Mock()
+        caller.admin_client.token = "test_token"
+
+        self.response_data = json.dumps({'roles': [{'name': 'admin',
+                                         'id': 'admin_id'}]})
+
+        http.request.side_effect = self._response_side_effect
+
+        config.rbac.rbac_test_role = 'admin'
+
+        self.assertEqual({'admin_role_id': 'admin_id',
+                          'rbac_role_id': 'admin_id'},
+                         self.rbac_utils.get_roles(caller))
+
+    @mock.patch('patrole_tempest_plugin.rbac_utils.CONF')
+    @mock.patch('patrole_tempest_plugin.rbac_utils.http')
+    def test_RBAC_utils_get_roles_admin_not_role(self, http, config):
+        self.rbac_utils.dictionary = {}
+
+        caller = mock.Mock()
+        caller.admin_client.token = "test_token"
+
+        self.response_data = json.dumps(
+            {'roles': [{'name': 'admin', 'id': 'admin_id'}]}
+        )
+        http.request.side_effect = self._response_side_effect
+
+        self.assertEqual({'admin_role_id': 'admin_id', 'rbac_role_id': None},
+                         self.rbac_utils.get_roles(caller))
+
+    def test_RBAC_utils_get_existing_roles(self):
+        self.rbac_utils.dictionary = {'admin_role_id': None,
+                                      'rbac_role_id': None}
+
+        self.assertEqual({'admin_role_id': None, 'rbac_role_id': None},
+                         self.rbac_utils.get_roles(None))
+
+    @mock.patch('patrole_tempest_plugin.rbac_utils.CONF')
+    @mock.patch('patrole_tempest_plugin.rbac_utils.http')
+    def test_RBAC_utils_get_roles_response_404(self, http, config):
+        self.rbac_utils.dictionary = {}
+
+        caller = mock.Mock()
+        caller.admin_client.token = "test_token"
+
+        http.request.side_effect = self._response_side_effect
+        self.get_response = 404
+
+        self.assertRaises(rbac_exceptions.RbacResourceSetupFailed,
+                          self.rbac_utils.get_roles, caller)
+        self.get_response = 200
+
+    def test_RBAC_utils_switch_roles_none(self):
+        self.assertIsNone(self.rbac_utils.switch_role(None))
+
+    @mock.patch('patrole_tempest_plugin.rbac_utils.CONF')
+    @mock.patch('patrole_tempest_plugin.rbac_utils.RbacUtils.get_roles')
+    @mock.patch('patrole_tempest_plugin.rbac_utils.http')
+    def test_RBAC_utils_switch_roles_member(self, http,
+                                            get_roles, config):
+        get_roles.return_value = {'admin_role_id': None,
+                                  'rbac_role_id': '_member_id'}
+
+        self.auth_provider = mock.Mock()
+        self.auth_provider.credentials.user_id = "user_id"
+        self.auth_provider.credentials.tenant_id = "tenant_id"
+        self.admin_client = mock.Mock()
+        self.admin_client.token = "admin_token"
+
+        http.request.side_effect = self._response_side_effect
+
+        self.assertIsNone(self.rbac_utils.switch_role(self, "_member_"))
+
+    @mock.patch('patrole_tempest_plugin.rbac_utils.CONF')
+    @mock.patch('patrole_tempest_plugin.rbac_utils.RbacUtils.get_roles')
+    @mock.patch('patrole_tempest_plugin.rbac_utils.http')
+    def test_RBAC_utils_switch_roles_false(self, http,
+                                           get_roles, config):
+        get_roles.return_value = {'admin_role_id': None,
+                                  'rbac_role_id': '_member_id'}
+
+        self.auth_provider = mock.Mock()
+        self.auth_provider.credentials.user_id = "user_id"
+        self.auth_provider.credentials.tenant_id = "tenant_id"
+        self.admin_client = mock.Mock()
+        self.admin_client.token = "admin_token"
+
+        http.request.side_effect = self._response_side_effect
+
+        self.assertIsNone(self.rbac_utils.switch_role(self, False))
+
+    @mock.patch('patrole_tempest_plugin.rbac_utils.CONF')
+    @mock.patch('patrole_tempest_plugin.rbac_utils.RbacUtils.get_roles')
+    @mock.patch('patrole_tempest_plugin.rbac_utils.http')
+    def test_RBAC_utils_switch_roles_get_roles_fails(self, http,
+                                                     get_roles, config):
+        get_roles.return_value = {'admin_role_id': None,
+                                  'rbac_role_id': '_member_id'}
+
+        self.auth_provider = mock.Mock()
+        self.auth_provider.credentials.user_id = "user_id"
+        self.auth_provider.credentials.tenant_id = "tenant_id"
+        self.admin_client = mock.Mock()
+        self.admin_client.token = "admin_token"
+
+        self.get_response = 404
+
+        self.assertRaises(rbac_exceptions.RbacResourceSetupFailed,
+                          self.rbac_utils.switch_role, self, False)
+
+        self.get_response = 200
+
+    @mock.patch('patrole_tempest_plugin.rbac_utils.RbacUtils.get_roles')
+    def test_RBAC_utils_switch_roles_exception(self, get_roles):
+        get_roles.return_value = {'admin_role_id': None,
+                                  'rbac_role_id': '_member_id'}
+        self.assertRaises(AttributeError, self.rbac_utils.switch_role,
+                          self, "admin")