Source code for cfme.fixtures.rbac

"""RBAC Role based parametrization and checking

The purpose of this fixture is to allow tests to be run within the context of multiple different
users, without the hastle or modifying the test. To this end, the RBAC module and fixture do not
require any modifications to the test body.

The RBAC fixture starts by receiving a list of roles and associated errors from the test metadata.
This data is in YAML format and an example can be seen below.

.. code-block:: yaml

    Metadata:
        test_flag: provision
        suite: infra_provisioning
        rbac:
            roles:
                default:
                evmgroup-super_administrator:
                evmgroup-administrator:
                evmgroup-operator: NoSuchElementException
                evmgroup-auditor: NoSuchElementException

Let's assume also we have a test that looks like the following::

    def test_rbac(rbac_role):
        if rbac_role != 'evmgroup-superadministrator' or rbac_role != 'evmgroup-operator':
            1 / 0

This metadata defines the roles to be tested, and associates with them the exceptions that are
expected for that particular test, or blank if no Exception is expected. In this way we can have
5 states of test result.

 * **Test Passed** - This was expected - We do nothing to this and exit early. In the example above
   evmgroup-super_administrator fulfills this, as it expects no Exception.
 * **Test Failed** - This was expected - We consume the Exception and change the result of the test
   to be a pass. In the example, this is fulfilled by evmgroup-auditor as it was expected to fail
   with the ZeroDivisionError.
 * **Test Failed** - This was unexpected - We consume the Exception and raise another informing that
   the test should have passed. In the example above, evmgroup-administrator satisfies this
   condition as it didn't expect a failure, but got one.
 * **Test Failed** - This was expected, but the wrong Exception appeared - We consume the Exception
   throw another stating that the Exception wasn't of the expected type. In the example above, the
   default user satifies this as it receives the ZeroDivisionError, but expects MonkeyError.
 * **Test Passed** - This was unexpected - We have Exception to consume, but we raise an Exception
   of our own as the test should have failed. In the example above, evmgroup-operator satisfies
   this as it should have received the ZeroDivisionError, but actually passes with no error.

When a test is configured to run against the RBAC suite, it will first parametrize the test with
the associated roles from the metadata. The test will then be wrapped and before it begins
we login as the *new* user. This process is also two fold. The ``pytest_store`` holds the current
user, and logging in is performed with whatever this user value is set to. So we first replace this
value with our new user. This ensures that if the browser fails during a navigation, we get
the opportunity to log in again with the *right* user. Once the user is set, we attempt to login.

When the test finishes, we set the user back to ``default`` before moving on to handling the outcome
of the test with the wrapped hook handler. This ensures that the next test will have the correct
user at login, even if the test fails horribly, and even if the inspection of the outcome should
fail.

To configure a test to use RBAC is simple. We simply need to add ``rbac_role`` to the list of
fixtures and the addition and the ldap configuration fixture also. Below is a complete
example of adding RBAC to a test.


.. code-block:: python

    import pytest

    def test_rbac(rbac_role):
    \"\"\" Tests provisioning from a template

    Metadata:
        rbac:
            roles:
                default:
                evmgroup-super_administrator:
                evmgroup-administrator:
                evmgroup-operator: NoSuchElementException
                evmgroup-auditor: NoSuchElementException
    \"\"\"
        if rbac_role != 'evmgroup-superadministrator' or rbac_role != 'evmgroup-operator':
            1 / 0

Exception matching is done with a simple string startswith match.

Currently there is no provision for skipping a role for a certain test, though this is easy to
implement. There is also no provision, for tests that have multiple parameters, to change the
expectation of the test, with relation to a parameter. For example, if there was a parameter
called *rhos* and one called *ec2* we could not change the expected exception to be different
depending on if the test was run against *rhos* or *ec2*.

"""
import traceback

import pytest

from cfme.utils import conf, testgen
from cfme.utils.appliance import current_appliance
from cfme.utils.browser import browser, ensure_browser_open
from cfme.utils.browser import take_screenshot
from cfme.utils.log import logger
from cfme.fixtures.artifactor_plugin import fire_art_test_hook
from cfme.fixtures.pytest_store import store

enable_rbac = False


[docs]def save_traceback_file(node, contents): """A convenience function for artifactor file sending This function simply takes the nodes id and the contents of the file and processes them and sends them to artifactor Args: node: A pytest node contents: The contents of the traceback file """ fire_art_test_hook( node, 'filedump', description="RBAC Traceback", contents=contents, file_type="rbac", group_id="RBAC", slaveid=store.slaveid)
[docs]def save_screenshot(node, ss, sse): if ss: fire_art_test_hook( node, 'filedump', description="RBAC Screenshot", file_type="rbac_screenshot", mode="wb", contents_base64=True, contents=ss, display_glyph="camera", group_id="RBAC", slaveid=store.slaveid) if sse: fire_art_test_hook( node, 'filedump', description="RBAC Screenshot error", file_type="rbac_screenshot_error", mode="w", contents_base64=False, contents=sse, display_type="danger", group_id="RBAC", slaveid=store.slaveid)
[docs]def really_logout(): """A convenience function logging out This function simply ensures that we are logged out and that a new browser is loaded ready for use. """ try: current_appliance.server.logout() except AttributeError: try: browser().quit() except AttributeError: ensure_browser_open()
@pytest.mark.hookwrapper
[docs]def pytest_pyfunc_call(pyfuncitem): """Inspects and consumes certain exceptions The guts of this function are explained above in the module documentation. Args: pyfuncitem: A pytest test item. """ # do whatever you want before the next hook executes if not enable_rbac: yield return # Login as the "new" user to run the test under if 'rbac_role' in pyfuncitem.fixturenames: user = pyfuncitem._request.getfuncargvalue('rbac_role') really_logout() logger.info("setting user to {}".format(user)) user_obj = current_appliance.collections.users.instantiate( username=conf.credentials[user]['username'], password=conf.credentials[user]['password'] ) # Actually perform the test. outcome is set to be a result object from the test with user_obj: outcome = yield screenshot, screenshot_error = take_screenshot() # Handle the Exception logger.error(pyfuncitem.location[0]) loc = "{}/{}".format(pyfuncitem.location[0], pyfuncitem.location[2]) # loc = loc[:min([loc.rfind('['), len(loc)])] logger.error(loc) # errors = [v for k, v in tests.items() if loc.startswith(k)] errors = pyfuncitem.function.meta.kwargs['from_docs']['rbac']['roles'] if errors: # errors = errors[0] user = pyfuncitem.funcargs['rbac_role'] if errors[user]: if not outcome.excinfo: logger.error("RBAC: Test should fail!") raise Exception("RBAC: You should fail!") else: if outcome.excinfo[1].__repr__().startswith(errors[user]): logger.info("RBAC: Test failed as expected") outcome.force_result(True) else: contents = "".join(traceback.format_list( traceback.extract_tb(outcome.excinfo[2]))) save_traceback_file(pyfuncitem, contents) save_screenshot(pyfuncitem, screenshot, screenshot_error) logger.error("RBAC: You blithering idiot, " "you failed with the wrong exception") raise Exception("RBAC: You should fail with {}!".format(errors[user])) else: if not outcome.excinfo: logger.info("RBAC: Test passed as expected") else: logger.error("RBAC: Test should have passed!") contents = "".join(traceback.format_list( traceback.extract_tb(outcome.excinfo[2]))) save_traceback_file(pyfuncitem, contents) save_screenshot(pyfuncitem, screenshot, screenshot_error) raise Exception("RBAC: Test should have passed!")
@pytest.mark.hookwrapper
[docs]def pytest_generate_tests(metafunc): yield if 'rbac_role' in metafunc.fixturenames: if enable_rbac: try: meta_data = metafunc.function.meta roles = meta_data.kwargs['from_docs']['rbac']['roles'].keys() except: raise Exception("Test {} should have metadata describing RBAC roles") else: roles = ['default'] testgen.parametrize(metafunc, 'rbac_role', roles)
[docs]def pytest_addoption(parser): # Create the cfme option group for use in other plugins parser.getgroup('cfme') parser.addoption("--rbac", action="store_true", default=False, help="enable rbac testing")
[docs]def pytest_configure(config): """ Filters the list of providers as part of pytest configuration. """ global enable_rbac if config.getoption('rbac'): enable_rbac = True