Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
alabaster==0.7.16
astroid==3.3.8
babel==2.18.0
balderhub-auth==0.0.1b3
balderhub-data==0.0.1b7
balderhub-unit==0.0.1b0
balderhub-url==0.0.1b0
balderplugin-junit==0.0.3
baldertest==0.2.0
baldertest==0.2.3
certifi==2026.2.25
charset-normalizer==3.4.7
dill==0.4.1
Expand Down
13 changes: 12 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ project_urls =
[options]
packages =
balderhub.http
balderhub.http.contrib
balderhub.http.contrib.auth
balderhub.http.contrib.auth.setup_features
balderhub.http.contrib.auth.setup_features.client
balderhub.http.contrib.auth.setup_features.server
balderhub.http.contrib.auth.utils
balderhub.http.lib
balderhub.http.lib.scenario_features
balderhub.http.lib.scenario_features.client
Expand All @@ -43,11 +49,16 @@ packages =
install_requires =
baldertest
balderhub-url
balderhub-auth
python_requires = >=3.10
package_dir =
=src
setup_requires =
setuptools
setuptools-scm>=6.0
zip_safe = no
[options.extras_require]
auth =
balderhub-auth~=0.0.1b3
balderhub-data~=0.0.1b7
all =
balderhub-html[auth]
Empty file.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .authenticated_by_token_within_websession_feature import AuthenticatedByTokenWithinWebsessionFeature
from .operation_handling_over_websession_feature import OperationHandlingOverWebsessionFeature

__all__ = [
'AuthenticatedByTokenWithinWebsessionFeature',
'OperationHandlingOverWebsessionFeature'
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import balderhub.auth.lib.scenario_features.client.role
import balderhub.http.lib.setup_features.client


class AuthenticatedByTokenWithinWebsessionFeature(balderhub.auth.lib.scenario_features.client.AuthenticationFeature):
"""
Enables and manages token-based authentication within a web session.

This feature provides mechanisms to handle token authentication by managing
the HTTP headers within a web session. The authentication state is verified
by checking the presence and correctness of a token in the session's headers.
"""
#: the key name used for the authentication header.
HEADER_KEY_NAME = 'Authorization'
#: the prefix added to the token value in the header.
TOKEN_PREFIX = 'Token'

#: inner-feature reference to the configuration object for the user's token role.
user_config = balderhub.auth.lib.scenario_features.client.role.TokenRoleFeature()
#: inner-feature reference to the web session object where authentication headers
websession = balderhub.http.lib.setup_features.client.WebSessionWithRequestsFeature()

def get_full_header_value(self):
"""
Constructs and returns the full header value by combining the token prefix and
the user's token.

:return: A string representing the complete header value, consisting of the
``TOKEN_PREFIX`` and the user's token separated by a space.
"""
return f'{self.TOKEN_PREFIX} {self.user_config.token}'

@property
def is_authenticated(self) -> bool:
return self.HEADER_KEY_NAME in self.websession.session.headers and \
self.websession.session.headers[self.HEADER_KEY_NAME] == self.get_full_header_value()

def authenticate(self):
# TODO provide native method in feature directly to manage header
self.websession.session.headers[self.HEADER_KEY_NAME] = self.get_full_header_value()

def unauthenticate(self):
# TODO provide native method in feature directly to manage header
del self.websession.session.headers[self.HEADER_KEY_NAME]
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import balderhub.auth.lib.scenario_features.client
from balderhub.auth.lib.utils import Operation
from balderhub.url.lib.utils import Url

from balderhub.http.contrib.auth.utils.actions import HttpAction
from balderhub.http.contrib.auth.utils.http_resource import HttpResource
from balderhub.http.lib.scenario_features.client.web_session_feature import WebSessionFeature


class OperationHandlingOverWebsessionFeature(balderhub.auth.lib.scenario_features.client.OperationHandlingFeature):
"""
Setup-Level Feature implementation of the
:class:`balderhub.auth.lib.scenario_features.client.OperationHandlingFeature`. It can be used to test
authentification / permissions for http resources by using the
:class:`balderhub.http.lib.scenario_features.client.WebSessionFeature`.
"""
#: inner feature reference to the web session feature
websession = WebSessionFeature()

@property
def unauth_redirect_schema(self) -> list[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.

:return: a list of `Url` objects representing the schema for unauthenticated
redirects (empty list by default).
"""
return []

def enter_operation(self, operation: Operation) -> bool:
resource = operation.resource
action = operation.action
if not isinstance(resource, HttpResource):
raise TypeError(f'can not operation with resource from type `{resource.__class__}`'
f' - this feature only work with resources from type `{HttpResource.__name__}`')
if not isinstance(action, HttpAction):
raise TypeError(f'expect http action type `{HttpAction.__name__}`, '
f'got {type(action)} instead')

response = self.websession.request(method=action.http_method, url=resource.url) # TODO should we send data?

if len(response.history) > 0:
# all history elements need to have status code 302
if {r.status_code for r in response.history} != {302}:
raise ValueError(f'got non 302 status codes in history responses: {response.history}')

if self.unauth_redirect_schema:
# redirects to provided schema should be handled as unauth error too
for cur_schema in self.unauth_redirect_schema:

if cur_schema.compare(response.url, allow_schemas=True):
# redirect to login page happened -> return UnauthorizedError
raise HttpResource.UnauthorizedError(
f'web request has an redirect to unauth URL schema `{cur_schema}`'
)
return False

if response.status_code == 401:
raise HttpResource.UnauthorizedError(f'web request returned 401: `{response.content}`')
if response.status_code == 403:
raise HttpResource.NoPermissionError(f'web request returned 403: `{response.content}`')
if response.status_code == 404:
raise HttpResource.DoesNotExistError(f'web request returned 404: `{response.content}`')
if response.status_code == 405:
raise HttpResource.NotAllowedMethodError(f'web request returned 405: `{response.content}`')
if response.status_code != 200:
raise HttpResource.ResourceEnterError(f'web request returned unexpected status code '
f'{response.status_code}: `{response.content}`')
return True


def leave_operation(self, operation: Operation) -> bool:
return True
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .simple_http_exist_for_config import SimpleHttpExistForConfig

__all__ = [
'SimpleHttpExistForConfig'
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from __future__ import annotations

import balderhub.auth.lib.scenario_features.server
from balderhub.auth.lib.utils import ResourceRule, ResourceRuleList

from balderhub.http.contrib.auth.utils import actions
from ...utils import UnresolvedHttpResource
from ...utils.functions import get_reverse_rule_cb_for
from ...utils.http_resource import HttpResource

class SimpleHttpExistForConfig(balderhub.auth.lib.scenario_features.server.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
:meth:`balderhub.auth.lib.scenario_features.server.ExistenceForConfig.resources_that_exist`.

Subclasses can overwrite the method :meth:`HttpExistenceForConfig.additional_non_existing_resources` to add
additional resources that should be tested.
"""
USE_HTTP_ACTIONS = [actions.GET, actions.POST, actions.PUT, actions.PATCH, actions.DELETE]

def get_resource_rules_that_exist(self) -> ResourceRuleList:
raise NotImplementedError

def get_resource_rules_that_not_exist(self) -> ResourceRuleList:
existing_rules = self.get_resource_rules_that_exist()

result = []

resolved_resources_dict = {}
for cur_existing_rule in existing_rules:
resource = cur_existing_rule.resource
if isinstance(resource, HttpResource):
if resource.url not in resolved_resources_dict.keys():
resolved_resources_dict[resource.url] = []
for action in cur_existing_rule.actions:
resolved_resources_dict[resource.url].append(action)
elif isinstance(resource, UnresolvedHttpResource):
cur_rule = cur_existing_rule.cb_rule
# otherwise rule is none -> every item is active - no item is in reverse-rule
if cur_rule:
reverse_rule = get_reverse_rule_cb_for(cur_rule)
new_rule = cur_existing_rule.copy()
new_rule.update_rule(reverse_rule)
result.append(new_rule)
else:
raise TypeError(f'unexpected resource type `{type(resource)}`')

for resource_url, existing_actions in resolved_resources_dict.items():
not_existing_actions = list(set(self.USE_HTTP_ACTIONS) - set(existing_actions))
if len(not_existing_actions) > 0:
result.append(ResourceRule(HttpResource(resource_url), actions=not_existing_actions))
return ResourceRuleList(result)
12 changes: 12 additions & 0 deletions src/balderhub/http/contrib/auth/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from . import actions
from .http_operation import HttpOperation
from .http_resource import HttpResource
from .unresolved_http_resource import UnresolvedHttpResource
from .unresolved_http_resource_for_specific_data_item import UnresolvedDataItemHttpResource

__all__ = [
'HttpOperation',
'HttpResource',
'UnresolvedHttpResource',
'UnresolvedDataItemHttpResource',
]
40 changes: 40 additions & 0 deletions src/balderhub/http/contrib/auth/utils/actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from typing import Union

from balderhub.auth.lib.utils import Action

from balderhub.http.lib.utils import HttpMethod


class HttpAction(Action):
"""
Represents an HTTP method implementing the :class:`balderhub.auth.lib.utils.Action` interface.
"""
def __init__(self, method: Union[HttpMethod, str]):
super().__init__()
self._method_str = method if isinstance(method, str) else method.value

def __str__(self):
return f"HttpAction<{self._method_str}>"

def __hash__(self):
return hash(self.__class__) + hash(self._method_str)

def __eq__(self, other):
return self.__class__ == other.__class__ and self.http_method == other.http_method

@property
def http_method(self) -> str:
"""
:return: the HTTP method associated with this action, as a string.
"""
return self._method_str


GET = HttpAction('GET')
POST = HttpAction('POST')
PUT = HttpAction('PUT')
PATCH = HttpAction('PATCH')
DELETE = HttpAction('DELETE')
HEAD = HttpAction('HEAD')
OPTIONS = HttpAction('OPTIONS')
TRACE = HttpAction('TRACE')
21 changes: 21 additions & 0 deletions src/balderhub/http/contrib/auth/utils/functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from collections.abc import Callable

from balderhub.auth.lib.utils.unresolved_resource import UnresolvedResource


def get_reverse_rule_cb_for(
rule: Callable[[UnresolvedResource.Parameter], bool]
) -> Callable[[UnresolvedResource.Parameter], bool]:
"""
Returns a callable function that inverses the logic of the provided rule,
which takes an argument and returns the opposite boolean value.

:param rule: A callable function that takes an instance of
``UnresolvedResource.Parameter`` as input and returns a boolean value.
:return: 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.
"""
if not callable(rule):
raise ValueError(f'rule must be callable, but is {rule}')
return lambda x: not rule(x)
31 changes: 31 additions & 0 deletions src/balderhub/http/contrib/auth/utils/http_operation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import balderhub.auth.lib.utils

from .actions import HttpAction
from .http_resource import HttpResource


class HttpOperation(balderhub.auth.lib.utils.Operation):
"""
Represents an HTTP operation consisting of a resource and an action.
"""
def __init__(self, resource: HttpResource, action: HttpAction):

if not isinstance(resource, HttpResource):
raise TypeError(f'resource must be of type {HttpResource.__name__}')
if not isinstance(action, HttpAction):
raise TypeError(f'action must be of type {HttpAction.__name__}')
super().__init__(resource, action)

@property
def resource(self) -> HttpResource:
"""
:return: the HTTP resource associated with the operation.
"""
return self._resource

@property
def action(self) -> HttpAction:
"""
:return: the HTTP action to be performed on the resource.
"""
return self._action
43 changes: 43 additions & 0 deletions src/balderhub/http/contrib/auth/utils/http_resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from typing import Union

import balderhub.auth.lib.utils
from balderhub.url.lib.utils import Url


class HttpResource(balderhub.auth.lib.utils.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.
"""
class NotAllowedMethodError(balderhub.auth.lib.utils.Resource.ResourceEnterError):
"""Represents an error raised when a forbidden method is attempted to be used."""

def __init__(self, url: Union[Url, str]):
"""
Initialize a HttpResource object.

:param url: the URL to which the resource is being requested
"""
super().__init__()
self._url = url if isinstance(url, Url) else Url(url)

def __str__(self):
return f"{self.__class__.__name__}<{self._url}>"

def __eq__(self, other):
if self.__class__ != other.__class__:
return False
return self.url == other.url

def __hash__(self):
return hash(self.__class__) + hash(self.url)

@property
def url(self) -> Url:
"""
:return: the URL of the HTTP resource
"""
return self._url
Loading
Loading