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

move ows proxy send_request function under corresponding adapter method #118

Merged
merged 6 commits into from Feb 1, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/tests.yml
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
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
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
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
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
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