Permalink
Browse files

Refactor auth and separate API from main app

Provide a way to construct just the API as a standalone WSGI app
and include this wholesale as a view inside the main site. This
means that they can use separate authentication policies for the
following benefits:

- The API can be deployed and integrated into other applications
  completely independently.

- The API app can be configured with a simpler authentication policy
  that does not access the session.

To accomplish this:

- Provide a `create_api` function in `h.app` that can construct a WSGI
  application that serves only the API.

- Extract resource endpoint authorization into a tween. This tween sets
  the initial value of the `REMOTE_USER` environment key so that the
  provided API application use the `RemoteUserAuthenticationPolicy`.

- Integrate authentication with the API by having the callback for the
  authentication policy provide the OAuth client as a consumer role.
  By not accessing the OAuth attributes in the API views, enable the
  aforementioned integration flexibility.

- Use a normal `SessionAuthenticationPolicy` for the main site.

Isolate the Annotator Auth token handling to make it easy to migrate to
the new token system.

- Generate standard web token claims and some backwards compatibility
  claims for the Annotator Auth plugin.

- Use a tween to port the X-Annotator-Auth-Token headers to isolate the
  legacy code paths from the core authentication machinery.

The`h.oauth` package can disappear completely as everything is packed
neatly into `h.auth`.

Close #1296.
  • Loading branch information...
tilgovi committed Feb 4, 2015
1 parent 7bdab6b commit 29c1500ca73c92bed78aed8bb5a29be96e11e8b9
View
@@ -11,21 +11,7 @@
def includeme(config):
"""Include config sections and setup Jinja."""
config.include('h.authentication')
config.include('h.authorization')
config.include('h.features')
config.include('h.queue')
config.include('h.subscribers')
config.include('h.views')
if config.registry.feature('api'):
config.include('h.api')
if config.registry.feature('streamer'):
config.include('h.streamer')
if config.registry.feature('notification'):
config.include('h.notification')
config.include('.views')
config.include('pyramid_jinja2')
config.add_jinja2_renderer('.js')
View
@@ -5,6 +5,5 @@ def __init__(self, request, user):
class LogoutEvent(object):
def __init__(self, request, user):
def __init__(self, request):
self.request = request
self.user = user
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import datetime
from pyramid import security
@@ -28,8 +29,6 @@ def login(event):
user = event.user
userid = 'acct:{}@{}'.format(user.username, request.domain)
request.user = userid
# Record a login event
stats(request).get_counter('auth.local.login').increment()
View
@@ -126,8 +126,7 @@ def login(self):
return {'status': 'okay'}
def logout(self):
user = self.User.get_by_id(self.request, self.request.user)
self.request.registry.notify(LogoutEvent(self.request, user))
self.request.registry.notify(LogoutEvent(self.request))
return super(AuthController, self).logout()
@@ -284,7 +283,5 @@ def includeme(config):
config.add_route('disable_user', '/disable/{user_id}',
factory=UserFactory,
traverse="/{user_id}")
config.include('horus')
config.add_request_method(name='user') # horus override (unset property)
config.scan(__name__)
View
@@ -5,14 +5,14 @@
import logging
import time
from annotator import auth, es
from annotator import es
from annotator.auth import Consumer, User
from elasticsearch import exceptions as elasticsearch_exceptions
from pyramid.renderers import JSON
from pyramid.settings import asbool
from pyramid.view import view_config
from h import events
from h.models import Annotation, Document
from .events import AnnotationEvent
from .models import Annotation, Document
log = logging.getLogger(__name__)
@@ -35,7 +35,6 @@ def api_config(**kwargs):
@api_config(context='h.resources.APIResource')
@api_config(context='h.resources.APIResource', route_name='index')
def index(context, request):
"""Return the API descriptor document.
@@ -87,14 +86,13 @@ def search(context, request):
@api_config(context='h.resources.APIResource', name='access_token')
def access_token(context, request):
"""The OAuth 2 access token view."""
if request.grant_type is None:
request.grant_type = 'client_credentials'
return request.create_token_response()
@api_config(context='h.resources.APIResource', name='token', renderer='string')
def annotator_token(context, request):
"""The Annotator Auth token view."""
request.grant_type = 'client_credentials'
response = access_token(context, request)
return response.json_body.get('access_token', response)
@@ -204,16 +202,19 @@ def delete(context, request):
def get_user(request):
"""Create a User object for annotator-store."""
userid = request.authenticated_userid
userid = request.unauthenticated_userid
if userid is not None:
consumer = auth.Consumer(request.client.client_id)
return auth.User(userid, consumer, False)
for principal in request.effective_principals:
if principal.startswith('consumer:'):
key = principal[9:]
consumer = Consumer(key)
return User(userid, consumer, False)
return None
def _trigger_event(request, annotation, action):
"""Trigger any callback functions listening for AnnotationEvents."""
event = events.AnnotationEvent(request, annotation, action)
event = AnnotationEvent(request, annotation, action)
request.registry.notify(event)
@@ -490,8 +491,6 @@ def includeme(config):
registry = config.registry
settings = registry.settings
config.add_renderer('json', JSON(indent=4))
# Configure ElasticSearch
store_from_settings(settings)
View
@@ -1,26 +1,58 @@
# -*- coding: utf-8 -*-
"""The main h application."""
from pyramid.config import Configurator
from pyramid.path import AssetResolver
from pyramid.response import FileResponse
from pyramid.renderers import JSON
from pyramid.wsgi import wsgiapp2
from .auth import acl_authz, remote_authn, session_authn
def create_app(settings):
"""Configure and add static routes and views. Return the WSGI app."""
config = Configurator(settings=settings)
config.include('h')
favicon = AssetResolver().resolve('h:favicon.ico')
config.add_route('favicon', '/favicon.ico')
config.add_view(
lambda request: FileResponse(favicon.abspath(), request=request),
route_name='favicon'
)
config.set_authentication_policy(session_authn)
config.set_authorization_policy(acl_authz)
config.set_root_factory('h.resources.RootFactory')
config.add_subscriber('h.subscribers.add_renderer_globals',
'pyramid.events.BeforeRender')
config.add_route('ok', '/ruok')
config.add_view(lambda request: 'imok', renderer='string', route_name='ok')
config.include('.')
config.include('.features')
config.set_root_factory('h.resources.RootFactory')
if config.registry.feature('api'):
api_app = create_api(settings)
api_view = wsgiapp2(api_app)
config.add_view(api_view, name='api')
if config.registry.feature('streamer'):
config.include('.streamer')
if config.registry.feature('notification'):
config.include('.notification')
return config.make_wsgi_app()
def create_api(settings):
config = Configurator(settings=settings)
config.set_authentication_policy(remote_authn)
config.set_authorization_policy(acl_authz)
config.set_root_factory('h.resources.APIResource')
config.add_renderer('json', JSON(indent=4))
config.add_subscriber('h.subscribers.set_user_from_oauth',
'pyramid.events.ContextFound')
config.add_tween('h.tweens.annotator_tween_factory')
config.include('.api')
config.include('.auth')
config.include('.features')
if config.registry.feature('streamer'):
config.include('.streamer')
return config.make_wsgi_app()
Oops, something went wrong.

0 comments on commit 29c1500

Please sign in to comment.