Skip to content

Commit

Permalink
Merge pull request #118 from bird-house/adapter-send-request
Browse files Browse the repository at this point in the history
  • Loading branch information
fmigneault authored Feb 1, 2023
2 parents 8c2bf25 + 80dc1c2 commit 9aa9956
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 8 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
python-version: ["3.6", "3.7", "3.8"]
python-version: ["3.7", "3.8"]
allow-failure: [false]
test-case: [test-local]
include:
Expand All @@ -61,6 +61,11 @@ jobs:
python-version: None # doesn't matter which one (in docker), but match default of repo
allow-failure: false
test-case: docker-test
# deprecated versions
- os: ubuntu-20.04
python-version: 3.6
allow-failure: false
test-case: test-local
steps:
- uses: actions/checkout@v2
with:
Expand Down
19 changes: 19 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ Changes
Unreleased
==========

Changes:

* Add ``/ows/verify/{service_name}[/{extra_path}]`` endpoint analoguous to ``/ows/proxy/{service_name}[/{extra_path}]``
to only verify if access is granted to this service, for that specific resource path, and for the authenticated user,
without performing the proxied request. This can be employed by servers and external entities to validate that
authorization will be granted for the user without executing potentially heavy computation or large data transfers
from the targeted resource that would otherwise be performed by requesting the ``/ows/proxy`` equivalent location.
One usage example of this feature is using |nginx-auth|_ to verify an alternate resource prior to proxying a service
request that needs authenticated access to the first resource.
* Add the OWS proxy ``send_request`` operation under the ``twitcher.adapter`` interface to allow it applying relevant
proxying adjustments when using derived implementation. The ``DefaultAdapater`` simply calls the original function
that was previously called directly instead of using the adapter's method.
* Removed the ``extra_path`` and ``request_params`` arguments from OWS proxy ``send_request`` to better align them with
arguments from other adapter methods. These parameters are directly retrieved from the ``request`` argument, which was
also provided as input to ``send_request``.

.. _nginx-auth: https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/
.. |nginx-auth| replace:: NGINX Authentication Based on Subrequest Result

0.7.0 (2022-05-11)
==================

Expand Down
13 changes: 13 additions & 0 deletions twitcher/adapter/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,16 @@ def response_hook(self, response, service):
This method can modify the response to adapt it for specific service logic.
"""
raise NotImplementedError

def send_request(self, request, service):
# type: (Request, ServiceConfig) -> Response
"""
Performs the provided request in order to obtain a proxied response.
.. versionadded:: 0.8.0
The operation should consider the service definition to resolve where the
request redirection should be proxied to, and handle any relevant response
errors, such as an unauthorized access or an unreachable service.
"""
raise NotImplementedError
12 changes: 12 additions & 0 deletions twitcher/adapter/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@
"""

from twitcher.adapter.base import AdapterInterface
from twitcher.owsproxy import send_request
from twitcher.owssecurity import OWSSecurity
from twitcher.owsregistry import OWSRegistry
from twitcher.store import ServiceStore
from twitcher.utils import get_settings
from pyramid.config import Configurator

from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pyramid.request import Request
from pyramid.response import Response

from twitcher.models.service import ServiceConfig

TWITCHER_ADAPTER_DEFAULT = 'default'


Expand Down Expand Up @@ -43,3 +51,7 @@ def request_hook(self, request, service):

def response_hook(self, response, service):
return response

def send_request(self, request, service):
# type: (Request, ServiceConfig) -> Response
return send_request(request, service)
40 changes: 35 additions & 5 deletions twitcher/owsproxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
LOGGER = logging.getLogger('TWITCHER')

if TYPE_CHECKING:
from typing import Iterator, Optional
from typing import Iterator

from pyramid.config import Configurator
from pyramid.request import Request
Expand Down Expand Up @@ -67,8 +67,14 @@ def __iter__(self):
return self.resp.iter_content(64 * 1024)


def _send_request(request, service, extra_path=None, request_params=None):
# type: (Request, ServiceConfig, Optional[str], Optional[str]) -> Response
def send_request(request, service):
# type: (Request, ServiceConfig) -> Response
"""
Send the request to the proxied service and handle its response.
"""

extra_path = request.matchdict.get('extra_path')
request_params = request.query_string

# TODO: fix way to build url
url = service['url']
Expand Down Expand Up @@ -162,7 +168,6 @@ def owsproxy_view(request):
# type: (Request) -> Response
service_name = request.matchdict.get('service_name')
try:
extra_path = request.matchdict.get('extra_path')
service = request.owsregistry.get_service_by_name(service_name)
if not service:
LOGGER.debug("No error raised but service was not found: %s", service_name)
Expand All @@ -177,7 +182,7 @@ def owsproxy_view(request):
# in order to ensure both request/response operations are handled by the same logic
adapter = request.adapter
request = adapter.request_hook(request, service)
response = _send_request(request, service, extra_path, request_params=request.query_string)
response = adapter.send_request(request, service)
response = adapter.response_hook(response, service)
return response
except OWSException as exc:
Expand All @@ -188,6 +193,27 @@ def owsproxy_view(request):
raise OWSNoApplicableCode("Unhandled error: {!s}".format(exc))


def owsverify_view(request):
# type: (Request) -> Response
"""
Verifies if request access is allowed, but without performing the proxied request and response handling.
"""
message, status, access = "forbidden", 403, False
try:
service_name = request.matchdict.get('service_name')
service = request.owsregistry.get_service_by_name(service_name)
if service and request.is_verified:
message, status, access = "allowed", 200, True
except Exception as exc:
LOGGER.exception("Security check failed due to unhandled error.", exc_info=exc)
pass
return Response(
json={"description": "Access to service is {!s}.".format(message), "access": access},
status=status,
request=request,
)


def owsproxy_defaultconfig(config):
# type: (Configurator) -> None
settings = get_settings(config)
Expand All @@ -199,8 +225,12 @@ def owsproxy_defaultconfig(config):
config.include('twitcher.owssecurity')
config.add_route('owsproxy', protected_path + '/proxy/{service_name}')
config.add_route('owsproxy_extra', protected_path + '/proxy/{service_name}/{extra_path:.*}')
config.add_route('owsverify', protected_path + '/verify/{service_name}')
config.add_route('owsverify_extra', protected_path + '/verify/{service_name}/{extra_path:.*}')
config.add_view(owsproxy_view, route_name='owsproxy')
config.add_view(owsproxy_view, route_name='owsproxy_extra')
config.add_view(owsverify_view, route_name='owsverify')
config.add_view(owsverify_view, route_name='owsverify_extra')


def includeme(config):
Expand Down
7 changes: 5 additions & 2 deletions twitcher/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,15 @@ def is_json_serializable(item):


def parse_service_name(url, protected_path):
# type: (str, str) -> Optional[str]
parsed_url = urlparse.urlparse(url)
service_name = None
if parsed_url.path.startswith(protected_path):
parts_without_protected_path = parsed_url.path[len(protected_path)::].strip('/').split('/')
if 'proxy' in parts_without_protected_path:
parts_without_protected_path.remove('proxy')
# use ranges to avoid index error in case the path parts list is empty
# the expected part must be exactly the first one after the protected path, then followed by the service name
if any(part in parts_without_protected_path[:1] for part in ['proxy', 'verify']):
parts_without_protected_path = parts_without_protected_path[1:]
if len(parts_without_protected_path) > 0:
service_name = parts_without_protected_path[0]
if not service_name:
Expand Down

0 comments on commit 9aa9956

Please sign in to comment.