Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add request/response hooks to adapter utility #114

Merged
merged 5 commits into from
May 11, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ Changes
Unreleased
==========

Changes:

* Add request and response hooks operations to adapter allowing derived implementations to modify OWS proxied requests
and returned responses from the service. The default adapter applies no modifications to the original definitions.

0.6.2 (2021-12-01)
==================

Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ pyramid_oauthlib>=0.4.1
oauthlib<3
requests_oauthlib<1.2.0
PyJWT>=2
# typing extension required for TypedDict
typing_extensions; python_version < "3.8"
35 changes: 33 additions & 2 deletions twitcher/adapter/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

from typing import TYPE_CHECKING
if TYPE_CHECKING:
from twitcher.typedefs import AnySettingsContainer, JSON
from twitcher.interface import OWSSecurityInterface, OWSRegistryInterface
from pyramid.config import Configurator
from pyramid.request import Request
from pyramid.response import Response

from twitcher.interface import OWSSecurityInterface, OWSRegistryInterface
from twitcher.models.service import ServiceConfig
from twitcher.typedefs import AnySettingsContainer, JSON


class AdapterInterface(object):
Expand Down Expand Up @@ -54,3 +57,31 @@ def owsproxy_config(self, container):
Returns the 'owsproxy' implementation of the adapter.
"""
raise NotImplementedError

def request_hook(self, request, service):
# type: (Request, ServiceConfig) -> Request
"""
Apply modifications onto the request before sending it.

.. versionadded:: 0.7.0

Request members employed after this hook is called include:
- :meth:`Request.headers`
- :meth:`Request.method`
- :meth:`Request.body`

This method can modified those members to adapt the request for specific service logic.
"""
raise NotImplementedError

def response_hook(self, response, service):
# type: (Response, ServiceConfig) -> Response
"""
Apply modifications onto the response from sent request.

.. versionadded:: 0.7.0

The received response from the proxied service is normally returned directly.
This method can modify the response to adapt it for specific service logic.
"""
raise NotImplementedError
6 changes: 6 additions & 0 deletions twitcher/adapter/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,9 @@ def owsproxy_config(self, container):
if not isinstance(container, Configurator):
container = self.configurator_factory(container)
owsproxy_defaultconfig(container)

def pre_request_hook(self, request, service):
return request

def post_request_hook(self, response, service):
return response
21 changes: 18 additions & 3 deletions twitcher/models/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,23 @@
String,
)
from sqlalchemy.ext.hybrid import hybrid_property
from typing import Union
from typing import TYPE_CHECKING, Union

from .meta import Base

if TYPE_CHECKING:
from twitcher.typedefs import TypedDict

ServiceConfig = TypedDict("ServiceConfig", {
"url": str,
"name": str,
"type": str,
"purl": str,
"auth": str,
"public": bool,
"verify": bool
}, total=True)


class Service(Base):
__tablename__ = 'services'
Expand All @@ -21,20 +34,22 @@ class Service(Base):

@hybrid_property
def verify(self):
# type: () -> bool
if self._verify == 1:
return True
return False

@verify.setter
def verify(self, verify: Union[bool, int]):
def verify(self, verify: Union[bool, int]) -> None:
self._verify = int(verify)

@property
def public(self):
def public(self) -> bool:
"""Return true if public access."""
return self.auth not in ['token', 'cert']

def json(self):
# type: () -> ServiceConfig
return {
'url': self.url,
'name': self.name,
Expand Down
36 changes: 25 additions & 11 deletions twitcher/owsproxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,20 @@
from typing import TYPE_CHECKING

from twitcher.owsexceptions import OWSAccessForbidden, OWSAccessFailed, OWSException, OWSNoApplicableCode
from twitcher.utils import (
replace_caps_url,
get_settings,
get_twitcher_url,
is_valid_url)
from twitcher.utils import get_settings, get_twitcher_url, is_valid_url, replace_caps_url

import logging
LOGGER = logging.getLogger('TWITCHER')

if TYPE_CHECKING:
from twitcher.typedefs import AnySettingsContainer # noqa: F401
from pyramid.config import Configurator # noqa: F401
from typing import AnyStr # noqa: F401
from typing import Optional

from pyramid.config import Configurator
from pyramid.request import Request

from twitcher.adapter.base import AdapterInterface
from twitcher.models.service import ServiceConfig
from twitcher.typedefs import AnySettingsContainer


allowed_content_types = (
Expand Down Expand Up @@ -64,6 +65,7 @@ def __iter__(self):


def _send_request(request, service, extra_path=None, request_params=None):
# type: (Request, ServiceConfig, Optional[str], Optional[str]) -> Response

# TODO: fix way to build url
url = service['url']
Expand Down Expand Up @@ -141,19 +143,20 @@ def _send_request(request, service, extra_path=None, request_params=None):


def owsproxy_base_path(container):
# type: (AnySettingsContainer) -> AnyStr
# type: (AnySettingsContainer) -> str
settings = get_settings(container)
return settings.get('twitcher.ows_proxy_protected_path', '/ows').rstrip('/').strip()


def owsproxy_base_url(container):
# type: (AnySettingsContainer) -> AnyStr
# type: (AnySettingsContainer) -> str
twitcher_url = get_twitcher_url(container)
owsproxy_path = owsproxy_base_path(container)
return twitcher_url + owsproxy_path


def owsproxy_view(request):
# type: (Request) -> Response
service_name = request.matchdict.get('service_name')
try:
extra_path = request.matchdict.get('extra_path')
Expand All @@ -167,7 +170,10 @@ def owsproxy_view(request):
try:
if not request.is_verified:
raise OWSAccessForbidden("Access to service is forbidden.")
return _send_request(request, service, extra_path, request_params=request.query_string)
request = request.adapter.request_hook(request, service)
response = _send_request(request, service, extra_path, request_params=request.query_string)
response = request.adapter.response_hook(response, service)
return response
except OWSException as exc:
LOGGER.warning("Security check failed but was not handled as expected by 'is_verified' method.", exc_info=exc)
raise
Expand All @@ -192,5 +198,13 @@ def owsproxy_defaultconfig(config):


def includeme(config):
# type: (Configurator) -> None
from twitcher.adapter import get_adapter_factory

def get_adapter(request):
# type: (Request) -> AdapterInterface
adapter = get_adapter_factory(request)
return adapter

get_adapter_factory(config).owsproxy_config(config)
config.add_request_method(get_adapter, reify=False, property=True, name="adapter")
6 changes: 6 additions & 0 deletions twitcher/typedefs.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import typing
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import AnyStr, Dict, List, Tuple, Union
Expand All @@ -10,6 +11,11 @@
from webob.headers import ResponseHeaders, EnvironHeaders
from webtest.response import TestResponse

if hasattr(typing, "TypedDict"):
from typing import TypedDict # pylint: disable=E0611,no-name-in-module
else:
from typing_extensions import TypedDict # noqa

Number = Union[int, float]
AnyValue = Union[AnyStr, Number, bool, None]
AnyKey = Union[AnyStr, int]
Expand Down