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:
OperationHandlingFeatureSetup-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 thebalderhub.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:
ExistenceForConfigThis 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:
ActionRepresents an HTTP method implementing the
balderhub.auth.lib.utils.Actioninterface.- 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:
OperationRepresents 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:
ResourceRepresents 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:
ResourceEnterErrorRepresents an error raised when a forbidden method is attempted to be used.
- class balderhub.http.contrib.auth.utils.UnresolvedHttpResource(url_schema: Url, **kwargs)
Bases:
UnresolvedResource,ABCRepresents 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:
ResourceEnterErrorRepresents 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,UnresolvedHttpResourceRepresents 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.Parameteras input and returns a boolean value.- Returns:
A callable function that applies the inverse logic of the provided
ruleon an instance ofUnresolvedResource.Parameter.- Raises:
ValueError – If rule is not a callable object.