diff --git a/docs/source/conf.py b/docs/source/conf.py index c87286c..3075c68 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -21,7 +21,18 @@ # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = ['sphinx.ext.autodoc'] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.autosectionlabel', + 'sphinx.ext.intersphinx' +] + +# === Intersphinx Mapping === +intersphinx_mapping = { + 'balderhub-auth': ('https://hub.balder.dev/projects/auth/en/latest/', None), + 'balderhub-data': ('https://hub.balder.dev/projects/data/en/latest/', None), + 'balderhub-url': ('https://hub.balder.dev/projects/url/en/latest/', None), +} templates_path = ['_templates'] exclude_patterns = [] diff --git a/docs/source/contrib.rst b/docs/source/contrib.rst new file mode 100644 index 0000000..cd63f86 --- /dev/null +++ b/docs/source/contrib.rst @@ -0,0 +1,16 @@ +Contribution To Other BalderHub +******************************* + +This package provides contribution code to some other BalderHub projects. Find more about it, within this module. + +If you want to install this package with support for all CONTRIB packages, you can install it with + +.. code-block:: none + + >>> pip install balderhub-http[all] + + +.. toctree:: + :caption: Contributions + + contrib/auth.rst diff --git a/docs/source/contrib/auth.rst b/docs/source/contrib/auth.rst new file mode 100644 index 0000000..e591c8d --- /dev/null +++ b/docs/source/contrib/auth.rst @@ -0,0 +1,407 @@ +Contrib for ``balderhub-auth`` +****************************** + +For activating this module, you need to install the package like shown below + +.. code-block:: none + + >>> pip install balderhub-http[auth] + +Once installed you can use it. + +Authentication / Permission Tests +================================= + +This contrib section contains ready-to-use features for testing the authentication and permission of HTTP resources +with `permission scenarios defined within balderhub-auth `_. +This is very useful for validating permissions for different user groups and making sure that your HTTP endpoints are only +available to the right users. + +Defining Resources that Exist +----------------------------- + +Testing resources also includes testing resources that do not exist, but there is a possibility that they might +exist. The features of this project allow you to define any resources and methods (actions) you would like to have. + +First of all, you should define a common valid feature version of the +:class:`balderhub.auth.lib.scenario_features.server.ExistenceForConfig`. + +.. code-block:: python + + from __future__ import annotations + + import balderhub.http.contrib.auth.setup_features.server + from balderhub.auth.lib.utils import ResourceRule, ResourceRuleList + from balderhub.http.contrib.auth.utils import HttpResource, UnresolvedDataItemHttpResource, actions + + + class MyExistenceWebConfig(balderhub.auth.lib.scenario_features.server.ExistenceForConfig): + + def get_resource_rules_that_exist(self): + + return ResourceRuleList([ + ResourceRule(HttpResource(Url('https://balder.dev/en/stable/')), actions=[actions.GET, actions.HEAD, actions.OPTIONS]), + ResourceRule(HttpResource(Url('https://balder.dev/en/stable/search.html')), actions=[actions.GET, actions.HEAD, actions.OPTIONS]), + .. + ]) + + def get_resource_rules_that_not_exist(self): + + return ResourceRuleList([ + ResourceRule(HttpResource(Url('https://balder.dev/en/stable/')), actions=[actions.POST, actions.PUT, actions.PATCH, actions.DELETE]), + ResourceRule(HttpResource(Url('https://balder.dev/en/stable/search.html')), actions=[actions.POST, actions.PUT, actions.PATCH, actions.DELETE]), + ... + ]) + +.. note:: + Instead of redefining the urls, you can use a inner-feature reference to a own definition of the + :class:`balderhub.url.lib.scenario_features.SitemapConfig`. + + +For HTTP resources it is often a good idea to test for non-existing HTTP operations with methods that should not +exist for real existing endpoints. This package provides a ready-to-use feature for that: +:class:`balderhub.http.contrib.auth.setup_features.server.SimpleHttpExistForConfig`. Use it as base class and you do not +need to add an implementation for +:meth:`~balderhub.auth.lib.scenario_features.server.ExistenceForConfig.get_resource_rules_that_not_exist`, because it +will already be generated out of the existing resources within +:meth:`~balderhub.auth.lib.scenario_features.server.ExistenceForConfig.get_resource_rules_that_exist`. + +.. code-block:: python + + from balderhub.auth.lib.utils import ResourceRule, ResourceRuleList + import balderhub.http.contrib.auth.setup_features.server + from balderhub.http.contrib.auth.utils import HttpResource, actions + + + class MyExistenceWebConfig(balderhub.http.contrib.auth.setup_features.server.SimpleHttpExistForConfig): + + def get_resource_rules_that_exist(self): + + return ResourceRuleList([ + ResourceRule(HttpResource(Url('https://balder.dev/en/stable/')), actions=[actions.GET, actions.HEAD, actions.OPTIONS]), + ResourceRule(HttpResource(Url('https://balder.dev/en/stable/search.html')), actions=[actions.GET, actions.HEAD, actions.OPTIONS]), + ... + ]) + +The method :meth:`~balderhub.auth.lib.scenario_features.server.ExistenceForConfig.get_resource_rules_that_not_exist` +is auto-generated by the :class:`balderhub.http.contrib.auth.setup_features.server.SimpleHttpExistForConfig` and will +return the following rules: + +.. code-block:: python + + [ + ResourceRule(HttpResource(Url('https://balder.dev/en/stable/')), actions=[actions.POST, actions.PUT, actions.PATCH, actions.DELETE]), + ResourceRule(HttpResource(Url('https://balder.dev/en/stable/search.html')), actions=[actions.POST, actions.PUT, actions.PATCH, actions.DELETE]), + ... + ] + +Defining Resources that need Authentication +------------------------------------------- + +The next step is to define a global feature that describes all resources that are not publicly available and need a +form of authentication: + +.. code-block:: python + + from __future__ import annotations + + import balderhub.auth.lib.scenario_features.server + from balderhub.auth.lib.utils import ResourceRuleList, ResourceRule + from balderhub.http.contrib.auth.utils import HttpResource, actions + + + class MyAuthForWebConfig(balderhub.auth.lib.scenario_features.server.AuthenticationForConfig): + exists = MyExistenceWebConfig() + + def get_resource_rules_that_require_authentication(self) -> ResourceRuleList: + return ResourceRuleList([ + ResourceRule(HttpResource(Url('https//balder.dev/secret_area')), actions=[actions.GET, actions.HEAD, actions.OPTIONS]), + ... + ]) + + def get_resource_rules_that_require_no_authentication(self) -> ResourceRuleList: + ... + +The :class:`balderhub.auth.lib.utils.ResourceRuleList` objects allow subtracting, so you either implement the +:meth:`balderhub.auth.lib.scenario_features.server.AuthenticationForConfig.get_resource_rules_that_require_no_authentication` +by yourself or just subtract the existing resources from the resources that need authentication: + +.. code-block:: python + + from __future__ import annotations + + import balderhub.auth.lib.scenario_features.server + from balderhub.auth.lib.utils import ResourceRuleList, ResourceRule + from balderhub.http.contrib.auth.utils import HttpResource, actions + + + class MyAuthForWebConfig(balderhub.auth.lib.scenario_features.server.AuthenticationForConfig): + exists = MyExistenceWebConfig() + + def get_resource_rules_that_require_authentication(self) -> ResourceRuleList: + return ResourceRuleList([ + ResourceRule(HttpResource(Url('https//balder.dev/secret_area')), actions=[actions.GET, actions.HEAD, actions.OPTIONS]), + ... + ]) + + def get_resource_rules_that_require_no_authentication(self) -> ResourceRuleList: + return self.exists.get_resource_rules_that_exist() - self.get_resource_rules_that_require_authentication() + + +.. note:: + Instead of redefining the urls, you can use a inner-feature reference to a own definition of the + :class:`balderhub.url.lib.scenario_features.SitemapConfig`. + + +Defining Permissions for Different User-Groups +---------------------------------------------- + +Now we have defined which resources exist and which resources require authentication. Finally, we need to +create a :class:`balderhub.auth.lib.scenario_features.client.HasPermissionsForConfig` for every user role our +application has (and for which we want to write tests): + +.. code-block:: python + + import balder + + import balderhub.auth.lib.scenario_features.client + from balderhub.auth.lib.utils import ResourceRule, ResourceRuleList + + from lib.setup_features import server + from balderhub.http.contrib.auth.utils import HttpResource, actions + + + class AdminUserHasPermissionForConfig(balderhub.auth.lib.scenario_features.client.HasPermissionsForConfig): + + class Webserver(balder.VDevice): + auth = MyAuthForWebConfig() + + def get_resource_rules_with_permissions(self) -> ResourceRuleList: + return ResourceRuleList([ + ResourceRule(HttpResource(Url('https//balder.dev/secret_area/dashboard')), actions=[actions.GET, actions.HEAD, actions.OPTIONS]), + ]) + + def get_resource_rules_without_permissions(self) -> ResourceRuleList: + return self.ServerWeb.auth.get_resource_rules_that_require_authentication() - self.get_resource_rules_with_permissions() + +Similar to the implementation of :meth:`balderhub.auth.lib.scenario_features.server.AuthenticationForConfig.get_resource_rules_that_require_no_authentication` +you can use subtracting for the :class:`balderhub.auth.lib.scenario_features.client.HasPermissionsForConfig` feature +like shown in the example above. + +.. note:: + Instead of redefining the urls, you can use a inner-feature reference to a own definition of the + :class:`balderhub.url.lib.scenario_features.SitemapConfig`. + + +Simple To Use Setup for Normal Permission Tests +----------------------------------------------- + +Finally, you need to import the scenarios and define your setup. Let's start with the unauthenticated scenario: + +.. code-block:: python + + # file scenario_balderhub.py + from balderhub.auth.scenarios import ScenarioAuthpermUnauthenticated + +And the required setup for this: + +.. code-block:: python + + import balder + import balder.connections + import balderhub.auth.lib.scenario_features.client + import balderhub.http.lib.setup_features.client + import balderhub.http.contrib.auth.setup_features.client + + class SetupAuthPerm(balder.Setup): + + class Webserver(balder.Device): + exists = MyExistenceWebConfig() + auth = MyAuthForWebConfig() + + @balder.connect(Webserver, over_connection=balder.connections.HttpConnection) + class Unauth(balder.Device): + _is_unauth = balderhub.auth.lib.scenario_features.client.IsUnauthenticatedFeature() + websession = balderhub.http.lib.setup_features.client.WebSessionWithRequestsFeature() + op_handler = balderhub.http.contrib.auth.setup_features.client.OperationHandlingOverWebsessionFeature() + +Assign the existence and the new authentication feature to the server device, and the autonomous feature +:class:`balderhub.auth.lib.scenario_features.client.IsUnauthenticatedFeature` to the unauthenticated client. + +Additionally you can use the ready-to-use features +:class:`balderhub.http.lib.setup_features.client.WebSessionWithRequestsFeature` (allowing us to interact with +web resources) and :class:`balderhub.http.contrib.auth.setup_features.client.OperationHandlingOverWebsessionFeature` +(describing how resources are entered and left). + +**Add Authenticated Scenario too:** + +The balderhub-auth package also provides a scenario for testing permissions/authentication of authenticated clients: + +.. code-block:: python + + # file scenario_balderhub.py + from balderhub.auth.scenarios import ScenarioAuthpermUnauthenticated, ScenarioAuthpermAuthenticated + + +.. code-block:: python + + import balder + import balder.connections + + class SetupAuthPerm(balder.Setup): + + class Webserver(balder.Device): + exists = MyExistenceWebConfig() + auth = MyAuthForWebConfig() + + @balder.connect(Webserver, over_connection=balder.connections.HttpConnection) + class Unauth(balder.Device): + ... + + @balder.connect(Webserver, over_connection=balder.connections.HttpConnection) + class Admin(balder.Device): + token = AdminTokenRole() + websession = balderhub.http.lib.setup_features.client.WebSessionWithRequestsFeature() + websession_auth = balderhub.http.contrib.auth.setup_features.client.AuthenticatedByTokenWithinWebsessionFeature() + sm_auth = balderhub.auth.lib.setup_features.client.AuthenticationStateMachine() + op_handler = balderhub.http.contrib.auth.setup_features.client.OperationHandlingOverWebsessionFeature() + perm = AdminUserHasPermissionForConfig(Webserver='Webserver') + +Besides our newly defined features for existence, authentication, and permission, as well as the features for web +session and operation handling, we have also added the ready-to-use implementation +:class:`balderhub.auth.lib.setup_features.client.AuthenticationStateMachine` (for the required scenario-level feature +:class:`balderhub.auth.lib.scenario_features.client.AuthenticationStateMachine`). In order for this feature to +authenticate the user correctly, we still need to implement the scenario-level +:class:`balderhub.auth.lib.scenario_features.client.AuthenticationFeature`, on which our state-machine depends. +To do this, we will use the pre-built +:class:`balderhub.http.contrib.auth.setup_features.client.AuthenticatedByTokenWithinWebsessionFeature`. + +Testing Object Permissions +-------------------------- + +To test permissions on a per-object level, you can use the specific class +:class:`balderhub.http.contrib.auth.utils.UnresolvedDataItemHttpResource`. This class allows you to dynamically +inject the required data item for the resource during the execution of the test. This is useful when the items you want +to test are not known statically beforehand, such as testing an API endpoint that handles an object ID generated at +runtime or if the permissions of accessing such resources is defined on object level. + +Adding the related scenarios: + +.. code-block:: python + + # file scenario_balderhub.py + ... + from balderhub.auth.scenarios import ScenarioAuthpermUnauthenticatedObjperm, ScenarioAuthpermAuthenticatedObjperm + + +You can register this class the exact same way you register standard ``HttpResource`` instances. It acts as an +unresolved placeholder that will be evaluated right before it's being used: + +.. code-block:: python + + from balderhub.auth.lib.utils import ResourceRule, ResourceRuleList + import balderhub.http.contrib.auth.setup_features.server + from balderhub.http.contrib.auth.utils import HttpResource, UnresolvedDataItemHttpResource, actions + + + class MyExistenceWebConfig(balderhub.http.contrib.auth.setup_features.server.SimpleHttpExistForConfig): + + def get_resource_rules_that_exist(self): + + return ResourceRuleList([ + ResourceRule( + HttpResource(Url('https://balder.dev/en/stable/')), + actions=[actions.GET, actions.HEAD, actions.OPTIONS] + ), + ResourceRule( + HttpResource(Url('https://balder.dev/en/stable/search.html')), + actions=[actions.GET, actions.HEAD, actions.OPTIONS] + ), + ResourceRule( + UnresolvedDataItemHttpResource('https://hub/balder.dev/projects/', data_item_type=BalderhubDataItem), + actions=[actions.GET, actions.HEAD, actions.OPTIONS] + ), + ]) + +.. note:: + The :class:`balderhub.auth.lib.utils.ResourceRule` has an optional attribute `rule`, that can be used for + unresolved resources to define a rule, that needs to be applicable to define the validity of this rule in the + given context. + + If you have a user with access to private balderhub projects that where created by himself, you could define + something like shown below: + + .. code-block:: python + + [ + ... + ResourceRule( + UnresolvedDataItemHttpResource('https://hub/balder.dev/projects/', data_item_type=BalderhubDataItem), + actions=[actions.GET, actions.HEAD, actions.OPTIONS], + rule=lambda bh: bh.created_from==self.user + ), + ] + +Just don't forget to define a feature implementation for the +:class:`balderhub.auth.lib.scenario_features.client.UnresolvedResourceParameterConfig`, which provides the individual +elements during the test. You can also use the pre-implemented factory +:class:`balderhub.data.contrib.auth.setup_features.factories.AutoDataParamProviderFactory`, which itself retrieves all +the data from the custom :class:`balderhub.data.lib.scenario_features.InitialDataConfig`. You have probably +already defined this feature in your environment, when working with data items. + +.. code-block:: python + + import balder + import balder.connections + + class SetupAuthPerm(balder.Setup): + + class Webserver(balder.Device): + exists = MyExistenceWebConfig() + auth = MyAuthForWebConfig() + + @balder.connect(Webserver, over_connection=balder.connections.HttpConnection) + class Unauth(balder.Device): + param = balderhub.data.contrib.auth.setup_features.factories.AutoDataParamProviderFactory.get_for(BalderhubDataItem)(Server='Server') + ... + + @balder.connect(Webserver, over_connection=balder.connections.HttpConnection) + class Admin(balder.Device): + token = AdminTokenRole() + param = balderhub.data.contrib.auth.setup_features.factories.AutoDataParamProviderFactory.get_for(BalderhubDataItem)(Server='Server') + ... + + +Contrib ``auth`` API +==================== + +Auth Setup Features +------------------- + +.. autoclass:: balderhub.http.contrib.auth.setup_features.client.OperationHandlingOverWebsessionFeature + :members: + +.. autoclass:: balderhub.http.contrib.auth.setup_features.server.SimpleHttpExistForConfig + :members: + +Auth Utilities +-------------- + +.. autoclass:: balderhub.http.contrib.auth.utils.actions.HttpAction + :members: + +.. autoclass:: balderhub.http.contrib.auth.utils.HttpOperation + :members: + +.. autoclass:: balderhub.http.contrib.auth.utils.HttpResource + :members: + +.. autoclass:: balderhub.http.contrib.auth.utils.UnresolvedHttpResource + :members: + +.. autoclass:: balderhub.http.contrib.auth.utils.UnresolvedDataItemHttpResource + :members: + + +.. autofunction:: balderhub.http.contrib.auth.utils.functions.get_reverse_rule_cb_for + diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 84b8074..facaeba 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -5,6 +5,10 @@ This package provides a feature to interact by a session-based web session. It a implementation with :class:`balderhub.http.lib.setup_features.client.WebSessionWithRequestsFeature` using the `python requests package `_. +.. note:: + If you want to implement permission / authentification tests, have a look into the + `contrib section of this documentation `_. + Creating universal Scenarios ============================ diff --git a/docs/source/index.rst b/docs/source/index.rst index cf428b3..f10448e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -19,4 +19,5 @@ out the `official documentation `_ first. scenarios.rst features.rst examples.rst + contrib.rst utilities.rst