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

using oauth and openapi #86

Merged
merged 11 commits into from
Aug 1, 2019
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
3 changes: 3 additions & 0 deletions development.ini
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ retry.attempts = 3
# twitcher
twitcher.url = http://localhost:8000
twitcher.adapter = default
twitcher.basicauth = true
twitcher.username = demo
twitcher.password = demo
twitcher.ows_security = true
twitcher.ows_proxy = true
twitcher.ows_proxy_protected_path = /ows
twitcher.oauth = true
# available types: random_token, signed_token, custom_token
twitcher.token.type = random_token
# run "make gencert"
Expand Down
23 changes: 9 additions & 14 deletions tests/test_adapter.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from twitcher.adapter import import_adapter, get_adapter_factory, TWITCHER_ADAPTER_DEFAULT
from twitcher.adapter.base import AdapterInterface
from twitcher.adapter.default import DefaultAdapter
from twitcher.store import ServiceStoreInterface
from twitcher.interface import OWSSecurityInterface
from pyramid.testing import DummyRequest
from pathlib import Path
import pytest
Expand Down Expand Up @@ -30,15 +30,10 @@ def test_adapter_factory_none_specified():

# noinspection PyAbstractClass,PyMethodMayBeStatic
class DummyAdapter(AdapterInterface):
def servicestore_factory(self, request):
class DummyServiceStore(ServiceStoreInterface):
def save_service(self, service): return True # noqa: E704
def delete_service(self, service): pass # noqa: E704
def list_services(self): return ["test"] # noqa: E704
def fetch_by_name(self, name): return name # noqa: E704
def fetch_by_url(self, url): return url # noqa: E704
def clear_services(self): pass # noqa: E704
return DummyServiceStore(request)
def owssecurity_factory(self, request):
class DummyOWSSecurity(OWSSecurityInterface):
def verify_request(self, request): return True # noqa: E704
return DummyOWSSecurity()


# noinspection PyPep8Naming
Expand Down Expand Up @@ -103,9 +98,9 @@ def test_adapter_factory_TestAdapter_invalid_raised():


# noinspection PyTypeChecker
def test_adapter_factory_call_servicestore_factory():
def test_adapter_factory_call_owssecurity_factory():
settings = {'twitcher.adapter': DummyAdapter({}).name}
adapter = get_adapter_factory(settings)
store = adapter.servicestore_factory(DummyRequest())
assert isinstance(store, ServiceStoreInterface)
assert store.fetch_by_name("test") == "test", "Requested adapter with corresponding store should have been called."
security = adapter.owssecurity_factory(DummyRequest())
assert isinstance(security, OWSSecurityInterface)
assert security.verify_request(DummyRequest()) is True, "Requested adapter should have been called."
3 changes: 2 additions & 1 deletion tests/test_frontpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ class TestFrontpageAPI(unittest.TestCase):
def setUpClass(cls) -> None:
cls.settings = {
'twitcher.url': 'localhost',
'sqlalchemy.url': 'sqlite:///:memory:'
'sqlalchemy.url': 'sqlite:///:memory:',
'twitcher.ows_security': False,
}
cls.config = testing.setUp(settings=cls.settings)
cls.app = TestApp(main({}, **cls.config.registry.settings))
Expand Down
35 changes: 1 addition & 34 deletions twitcher/adapter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@
LOGGER = logging.getLogger("TWITCHER")

if TYPE_CHECKING:
from twitcher.store import ServiceStoreInterface
from twitcher.typedefs import AnySettingsContainer
from pyramid.request import Request
from typing import AnyStr, Type, Union
from typing import AnyStr, Type


def import_adapter(name):
Expand Down Expand Up @@ -62,34 +60,3 @@ def get_adapter_factory(container):
LOGGER.error("Adapter '{!s}' raised an exception during instantiation : '{!r}'".format(adapter_type, e))
raise
return DefaultAdapter(container)


def get_adapter_store_factory(
adapter, # type: AdapterInterface
store_name, # type: AnyStr
request, # type: Request
): # type: (...) -> Union[ServiceStoreInterface]
"""
Retrieves the adapter store by name if it is defined.

If another adapter than :class:`twitcher.adapter.default.DefaultAdapter` is provided, and that the store
cannot be found with it, `DefaultAdapter` is used as fallback to find the "default" store implementation.

:returns: found store.
:raises NotImplementedError: when the store is not available from the adapter.
:raises Exception: when store instance was found but generated an error on creation.
"""
try:
store = getattr(adapter, store_name)
return store(request)
except NotImplementedError:
if isinstance(adapter, DefaultAdapter):
LOGGER.exception("Adapter 'DefaultAdapter' doesn't implement '{!r}', no way to recover.".format(store_name))
raise
LOGGER.warning("Adapter '{!r}' doesn't implement '{!r}', falling back to 'DefaultAdapter' implementation."
.format(adapter, store_name))
return get_adapter_store_factory(DefaultAdapter(request), store_name, request)
except Exception as e:
LOGGER.error("Adapter '{!r}' raised an exception while instantiating '{!r}' : '{!r}'"
.format(adapter, store_name, e))
raise
14 changes: 7 additions & 7 deletions twitcher/adapter/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from twitcher.typedefs import AnySettingsContainer, JSON
from twitcher.store import AccessTokenStoreInterface, ServiceStoreInterface
from twitcher.interface import OWSSecurityInterface, OWSRegistryInterface
from pyramid.config import Configurator
from pyramid.request import Request

Expand Down Expand Up @@ -34,17 +34,17 @@ def configurator_factory(self, container):
"""
raise NotImplementedError

def tokenstore_factory(self, request):
# type: (Request) -> AccessTokenStoreInterface
def owssecurity_factory(self):
# type: () -> OWSSecurityInterface
"""
Returns the 'tokenstore' implementation of the adapter.
Returns the 'owssecurity' implementation of the adapter.
"""
raise NotImplementedError

def servicestore_factory(self, request):
# type: (Request) -> ServiceStoreInterface
def owsregistry_factory(self, request):
# type: (Request) -> OWSRegistryInterface
"""
Returns the 'servicestore' implementation of the adapter.
Returns the 'owsregistry' implementation of the adapter.
"""
raise NotImplementedError

Expand Down
9 changes: 7 additions & 2 deletions twitcher/adapter/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"""

from twitcher.adapter.base import AdapterInterface
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
Expand All @@ -23,8 +25,11 @@ def configurator_factory(self, container):
settings = get_settings(container)
return Configurator(settings=settings)

def servicestore_factory(self, request):
return ServiceStore(request)
def owssecurity_factory(self):
return OWSSecurity()

def owsregistry_factory(self, request):
return OWSRegistry(ServiceStore(request))

def owsproxy_config(self, container):
from twitcher.owsproxy import owsproxy_defaultconfig
Expand Down
15 changes: 10 additions & 5 deletions twitcher/basicauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Taken from:
https://docs.pylonsproject.org/projects/pyramid-cookbook/en/latest/auth/basic.html
"""
from pyramid.settings import asbool
from pyramid.authentication import BasicAuthAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.httpexceptions import HTTPForbidden
Expand All @@ -15,6 +16,8 @@
forget)
from pyramid.view import forbidden_view_config

from twitcher.utils import get_settings


@forbidden_view_config()
def forbidden_view(request):
Expand Down Expand Up @@ -45,8 +48,10 @@ class Root:


def includeme(config):
authn_policy = BasicAuthAuthenticationPolicy(check=check_credentials, debug=True)
authz_policy = ACLAuthorizationPolicy()
config.set_authorization_policy(authz_policy)
config.set_authentication_policy(authn_policy)
config.set_root_factory(lambda request: Root())
settings = get_settings(config)
if asbool(settings.get('twitcher.basicauth', True)):
authn_policy = BasicAuthAuthenticationPolicy(check=check_credentials, debug=True)
authz_policy = ACLAuthorizationPolicy()
config.set_authorization_policy(authz_policy)
config.set_authentication_policy(authn_policy)
config.set_root_factory(lambda request: Root())
36 changes: 36 additions & 0 deletions twitcher/interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
Twitcher interfaces to allow alternative implementions in adapters.
"""
from typing import Dict, List


class OWSSecurityInterface(object):
def verify_request(self, request) -> bool:
"""Verify that the service request is allowed."""
raise NotImplementedError


class OWSRegistryInterface(object):
def register_service(self, name: str, url: str, *args, **kwargs) -> Dict:
"""Register an OWS service with given ``name`` and ``url``."""
raise NotImplementedError

def unregister_service(self, name: str) -> bool:
"""Unregister an OWS service with given ``name``."""
raise NotImplementedError

def get_service_by_name(self, name: str) -> Dict:
"""Lookup OWS service with given ``name``."""
raise NotImplementedError

def get_service_by_url(self, url: str) -> Dict:
"""Lookup OWS service with given ``url``."""
raise NotImplementedError

def list_services(self) -> List:
"""List all registered OWS services."""
raise NotImplementedError

def clear_services(self) -> bool:
"""Remove all registered OWS services."""
raise NotImplementedError
77 changes: 40 additions & 37 deletions twitcher/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
from oauthlib.oauth2.rfc6749 import tokens
import jwt

from pyramid.settings import asbool

from twitcher import models
from twitcher.utils import get_settings

Expand Down Expand Up @@ -223,40 +225,41 @@ def register_client_app_view(request):

def includeme(config):
settings = get_settings(config)
config.include('pyramid_oauthlib')
# using basic auth for client app registration
config.include('twitcher.basicauth')

# Validator callback functions are passed Pyramid request objects so
# you can access your request properties, database sessions, etc.
# The request object is populated with accessors for the properties
# referred to in the OAuthLib docs and used by its built in types.
token_type = settings.get('twitcher.token.type', 'random_token')
if token_type == 'random_token':
validator = RandomTokenValidator()
elif token_type == 'signed_token':
validator = SignedTokenValidator(
cert=settings.get('twitcher.token.certfile'),
key=settings.get('twitcher.token.keyfile'),
issuer=settings.get('twitcher.token.issuer'))
elif token_type == 'custom_token':
validator = CustomTokenValidator(
secret=settings.get('twitcher.token.secret'),
issuer=settings.get('twitcher.token.issuer'))
else: # default
validator = RandomTokenValidator()

# Register grant types to validate token requests.
config.add_grant_type('oauthlib.oauth2.ClientCredentialsGrant',
request_validator=validator)

# Register the token types to use at token endpoints.
config.add_token_type('oauthlib.oauth2.BearerToken',
request_validator=validator,
token_generator=validator.generate_access_token,
expires_in=int(settings.get('twitcher.token.expires_in', '3600')))

config.add_route('access_token', TOKEN_ENDPOINT)
config.add_view(generate_token_view, route_name='access_token')
config.add_route('client', CLIENT_APP_ENDPOINT)
config.add_view(register_client_app_view, route_name='client', renderer='json', permission='view')
if asbool(settings.get('twitcher.oauth', True)):
config.include('pyramid_oauthlib')
# using basic auth for client app registration
config.include('twitcher.basicauth')

# Validator callback functions are passed Pyramid request objects so
# you can access your request properties, database sessions, etc.
# The request object is populated with accessors for the properties
# referred to in the OAuthLib docs and used by its built in types.
token_type = settings.get('twitcher.token.type', 'random_token')
if token_type == 'random_token':
validator = RandomTokenValidator()
elif token_type == 'signed_token':
validator = SignedTokenValidator(
cert=settings.get('twitcher.token.certfile'),
key=settings.get('twitcher.token.keyfile'),
issuer=settings.get('twitcher.token.issuer'))
elif token_type == 'custom_token':
validator = CustomTokenValidator(
secret=settings.get('twitcher.token.secret'),
issuer=settings.get('twitcher.token.issuer'))
else: # default
validator = RandomTokenValidator()

# Register grant types to validate token requests.
config.add_grant_type('oauthlib.oauth2.ClientCredentialsGrant',
request_validator=validator)

# Register the token types to use at token endpoints.
config.add_token_type('oauthlib.oauth2.BearerToken',
request_validator=validator,
token_generator=validator.generate_access_token,
expires_in=int(settings.get('twitcher.token.expires_in', '3600')))

config.add_route('access_token', TOKEN_ENDPOINT)
config.add_view(generate_token_view, route_name='access_token')
config.add_route('client', CLIENT_APP_ENDPOINT)
config.add_view(register_client_app_view, route_name='client', renderer='json', permission='view')
18 changes: 2 additions & 16 deletions twitcher/owsproxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
get_settings,
get_twitcher_url,
is_valid_url)
from twitcher.owsrequest import OWSRequest

import logging
LOGGER = logging.getLogger('TWITCHER')
Expand Down Expand Up @@ -160,25 +159,11 @@ def owsproxy_view(request):
service = request.owsregistry.get_service_by_name(service_name)
except Exception:
return OWSAccessFailed("Could not find service")
if verify_request(request, service) is False:
if request.verify_ows_request is False:
Copy link
Contributor

@fmigneault fmigneault Jul 29, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming is not intuitive as it feels like a function name
instead use is_ows_request_verified or similar

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done ... but shortened to is_verified.

raise OWSAccessForbidden("Access to service is forbidden.")
return _send_request(request, service, extra_path, request_params=request.query_string)


def verify_request(request, service):
ows_request = OWSRequest(request)
if ows_request.service_allowed() is False:
return False
if service.get('public', False) is True:
return True
if ows_request.public_access() is True:
return True
if service.get('auth', '') == 'cert':
return request.headers.get('X-Ssl-Client-Verify', '') == 'SUCCESS'
else:
return request.verify_request(scopes=["compute"])


def owsproxy_defaultconfig(config):
# type: (Configurator) -> None
settings = get_settings(config)
Expand All @@ -187,6 +172,7 @@ def owsproxy_defaultconfig(config):

config.include('twitcher.oauth2')
config.include('twitcher.owsregistry')
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_view(owsproxy_view, route_name='owsproxy')
Expand Down
2 changes: 1 addition & 1 deletion twitcher/owsregistry.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,5 @@ def includeme(config):

def owsregistry(request):
adapter = get_adapter_factory(request)
return OWSRegistry(adapter.servicestore_factory(request))
return adapter.owsregistry_factory(request)
config.add_request_method(owsregistry, reify=True)
Loading