From 1d1ea0f0b827354bf26d99c1dc2fd5ca39a5b150 Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Mon, 30 Jan 2023 12:44:13 -0500 Subject: [PATCH 1/6] move ows proxy send_request function under corresponding adapter send_request method --- CHANGES.rst | 9 +++++++++ twitcher/adapter/base.py | 13 +++++++++++++ twitcher/adapter/default.py | 5 +++++ twitcher/owsproxy.py | 13 +++++++++---- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 200542b..e64e2ce 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,15 @@ Changes Unreleased ========== +Changes: + +* 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 funciton + 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``. + 0.7.0 (2022-05-11) ================== diff --git a/twitcher/adapter/base.py b/twitcher/adapter/base.py index f23c0f3..52f2d3e 100644 --- a/twitcher/adapter/base.py +++ b/twitcher/adapter/base.py @@ -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 diff --git a/twitcher/adapter/default.py b/twitcher/adapter/default.py index e66788d..83873c7 100644 --- a/twitcher/adapter/default.py +++ b/twitcher/adapter/default.py @@ -3,6 +3,7 @@ """ 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 @@ -43,3 +44,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) diff --git a/twitcher/owsproxy.py b/twitcher/owsproxy.py index 811caf8..1034e1c 100644 --- a/twitcher/owsproxy.py +++ b/twitcher/owsproxy.py @@ -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'] @@ -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) @@ -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: From 6e12f36848bbf12dea43756636d3bb31f7256c62 Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Mon, 30 Jan 2023 12:58:24 -0500 Subject: [PATCH 2/6] fix typos --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e64e2ce..3ae6e5e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,10 +7,10 @@ Unreleased Changes: * 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 funciton + 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 + arguments from other adapter methods. These parameters are directly retrieved from the ``request`` argument, which was also provided as input to ``send_request``. 0.7.0 (2022-05-11) From 10872bd04aadabe6d739e3b8dbfc932654473e67 Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Mon, 30 Jan 2023 17:31:03 -0500 Subject: [PATCH 3/6] add /ows/verify endpoint --- CHANGES.rst | 10 ++++++++++ twitcher/owsproxy.py | 25 +++++++++++++++++++++++++ twitcher/utils.py | 7 +++++-- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3ae6e5e..9567266 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,13 @@ 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 perfoming 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 targetted 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 proxing 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. @@ -13,6 +20,9 @@ Changes: 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) ================== diff --git a/twitcher/owsproxy.py b/twitcher/owsproxy.py index 1034e1c..e494307 100644 --- a/twitcher/owsproxy.py +++ b/twitcher/owsproxy.py @@ -193,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) @@ -204,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): diff --git a/twitcher/utils.py b/twitcher/utils.py index 39f2da3..8f61ac2 100644 --- a/twitcher/utils.py +++ b/twitcher/utils.py @@ -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: From 6c82854ed6ec8c4203f363a324bda467b7205e10 Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Mon, 30 Jan 2023 17:32:32 -0500 Subject: [PATCH 4/6] fix typos --- CHANGES.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9567266..d3f4a7b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,10 +8,10 @@ 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 perfoming the proxied request. This can be employed by servers and external entities to validate that + 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 targetted 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 proxing a service + 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 From 31862a1e7b3183c150bad19d40de1746c6ba25db Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Mon, 30 Jan 2023 17:39:08 -0500 Subject: [PATCH 5/6] move python3.6 workflow test to older ubuntu since not available on latest --- .github/workflows/tests.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1f733b5..9f4d622 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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: @@ -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: From 80dc1c25a5601a824f957153aa2f06a3f6389f42 Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Mon, 30 Jan 2023 20:58:38 -0500 Subject: [PATCH 6/6] fix linting --- twitcher/adapter/default.py | 7 +++++++ twitcher/owsproxy.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/twitcher/adapter/default.py b/twitcher/adapter/default.py index 83873c7..8812dfa 100644 --- a/twitcher/adapter/default.py +++ b/twitcher/adapter/default.py @@ -10,6 +10,13 @@ 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' diff --git a/twitcher/owsproxy.py b/twitcher/owsproxy.py index e494307..e17f29b 100644 --- a/twitcher/owsproxy.py +++ b/twitcher/owsproxy.py @@ -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