Contrib for balderhub-auth

For activating this module, you need to install the package like shown below

>>> 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 balderhub.auth.lib.scenario_features.server.ExistenceForConfig.

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 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: balderhub.http.contrib.auth.setup_features.server.SimpleHttpExistForConfig. Use it as base class and you do not need to add an implementation for get_resource_rules_that_not_exist(), because it will already be generated out of the existing resources within get_resource_rules_that_exist().

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 get_resource_rules_that_not_exist() is auto-generated by the balderhub.http.contrib.auth.setup_features.server.SimpleHttpExistForConfig and will return the following rules:

[
    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:

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 balderhub.auth.lib.utils.ResourceRuleList objects allow subtracting, so you either implement the 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:

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 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 balderhub.auth.lib.scenario_features.client.HasPermissionsForConfig for every user role our application has (and for which we want to write tests):

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 balderhub.auth.lib.scenario_features.server.AuthenticationForConfig.get_resource_rules_that_require_no_authentication() you can use subtracting for the 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 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:

# file scenario_balderhub.py
from balderhub.auth.scenarios import ScenarioAuthpermUnauthenticated

And the required setup for this:

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 balderhub.auth.lib.scenario_features.client.IsUnauthenticatedFeature to the unauthenticated client.

Additionally you can use the ready-to-use features balderhub.http.lib.setup_features.client.WebSessionWithRequestsFeature (allowing us to interact with web resources) and 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:

# file scenario_balderhub.py
from balderhub.auth.scenarios import ScenarioAuthpermUnauthenticated, ScenarioAuthpermAuthenticated
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 balderhub.auth.lib.setup_features.client.AuthenticationStateMachine (for the required scenario-level feature 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 balderhub.auth.lib.scenario_features.client.AuthenticationFeature, on which our state-machine depends. To do this, we will use the pre-built 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 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:

# 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:

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/<str:name>', data_item_type=BalderhubDataItem),
                actions=[actions.GET, actions.HEAD, actions.OPTIONS]
            ),
        ])

Note

The 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:

[
    ...
    ResourceRule(
        UnresolvedDataItemHttpResource('https://hub/balder.dev/projects/<str:name>', 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 balderhub.auth.lib.scenario_features.client.UnresolvedResourceParameterConfig, which provides the individual elements during the test. You can also use the pre-implemented factory balderhub.data.contrib.auth.setup_features.factories.AutoDataParamProviderFactory, which itself retrieves all the data from the custom balderhub.data.lib.scenario_features.InitialDataConfig. You have probably already defined this feature in your environment, when working with data items.

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

class balderhub.http.contrib.auth.setup_features.client.OperationHandlingOverWebsessionFeature(**kwargs)

Bases: OperationHandlingFeature

Setup-Level Feature implementation of the balderhub.auth.lib.scenario_features.client.OperationHandlingFeature. It can be used to test authentification / permissions for http resources by using the balderhub.http.lib.scenario_features.client.WebSessionFeature.

enter_operation(operation: Operation) bool

Enters the given operation.

Parameters:

operation – the operation to enter

Returns:

True if the operation was entered successfully, False otherwise

leave_operation(operation: Operation) bool

Leaves the given operation.

Parameters:

operation – the operation to leave

Returns:

True if the operation was left successfully, False otherwise

property unauth_redirect_schema: list[balderhub.url.lib.utils.url.Url]

This property provides the schema used for unauthenticated redirect URLs. It describes a list of Url schemas that are treated the same as an Unauthenticated error.

Returns:

a list of Url objects representing the schema for unauthenticated redirects (empty list by default).

websession = <balderhub.http.lib.scenario_features.client.web_session_feature.WebSessionFeature object>

inner feature reference to the web session feature

class balderhub.http.contrib.auth.setup_features.server.SimpleHttpExistForConfig(**kwargs)

Bases: ExistenceForConfig

This existence-for configuration feature automatically determines non-existing resources by creating a resource for every normal HttpMethod and Url, that is used in balderhub.auth.lib.scenario_features.server.ExistenceForConfig.resources_that_exist().

Subclasses can overwrite the method HttpExistenceForConfig.additional_non_existing_resources() to add additional resources that should be tested.

get_resource_rules_that_exist() ResourceRuleList

Returns the list of resource rules, that exist.

Returns:

the resource rule list

get_resource_rules_that_not_exist() ResourceRuleList

Returns the list of resource rules, that do NOT exist.

Returns:

the resource rule list

Auth Utilities

class balderhub.http.contrib.auth.utils.actions.HttpAction(method: HttpMethod | str)

Bases: Action

Represents an HTTP method implementing the balderhub.auth.lib.utils.Action interface.

property http_method: str
Returns:

the HTTP method associated with this action, as a string.

class balderhub.http.contrib.auth.utils.HttpOperation(resource: HttpResource, action: HttpAction)

Bases: Operation

Represents an HTTP operation consisting of a resource and an action.

property action: HttpAction
Returns:

the HTTP action to be performed on the resource.

property resource: HttpResource
Returns:

the HTTP resource associated with the operation.

class balderhub.http.contrib.auth.utils.HttpResource(url: Url | str)

Bases: Resource

Represents an HTTP resource with a URL.

This class is designed to encapsulate an HTTP resource by managing its URL, allowing comparison with other resources of the same type, and providing a structured string representation.

exception NotAllowedMethodError

Bases: ResourceEnterError

Represents an error raised when a forbidden method is attempted to be used.

property url: Url
Returns:

the URL of the HTTP resource

class balderhub.http.contrib.auth.utils.UnresolvedHttpResource(url_schema: Url, **kwargs)

Bases: UnresolvedResource, ABC

Represents an HTTP resource with an unresolved schema.

This class is used to manage and represent HTTP resources where the schema (such as URL structure) is not fully resolved. It provides utility methods for equality comparison, string representation, and hashing based on the URL schema. It inherits from UnresolvedResource and ensures compatibility with existing resource utilities.

exception NotAllowedMethodError

Bases: ResourceEnterError

Represents an error raised when a method is not allowed for a specific resource or context.

property url_schema
Returns:

represents the unresolved URL schema for the resource

class balderhub.http.contrib.auth.utils.UnresolvedDataItemHttpResource(url_schema: Url, data_item_type: type[balderhub.data.lib.utils.single_data_item.SingleDataItem], **kwargs)

Bases: ResourceForSpecificDataItem, UnresolvedHttpResource

Represents an HTTP resource for unresolved data items within a specific data item type.

This class combines functionality from the ResourceForSpecificDataItem and UnresolvedHttpResource base classes to handle unresolved HTTP resources. It provides mechanisms for resolving the resource into a fully defined HTTP resource based on input parameters derived from the specific data item’s fields.

get_resolved_resource(param: Parameter) HttpResource

Resolves the resource with the given parameter.

Parameters:

param – the parameter for the unresolved resource

Returns:

the resolved resource

balderhub.http.contrib.auth.utils.functions.get_reverse_rule_cb_for(rule: Callable[[Parameter], bool]) Callable[[Parameter], bool]

Returns a callable function that inverses the logic of the provided rule, which takes an argument and returns the opposite boolean value.

Parameters:

rule – A callable function that takes an instance of UnresolvedResource.Parameter as input and returns a boolean value.

Returns:

A callable function that applies the inverse logic of the provided rule on an instance of UnresolvedResource.Parameter.

Raises:

ValueError – If rule is not a callable object.