Skip to content

Commit

Permalink
Merge branch 'poc-flask-views' into poc-flask-views.common-url_for-ta…
Browse files Browse the repository at this point in the history
…ke-2

Conflicts:
	ckan/config/middleware/flask_app.py
  • Loading branch information
amercader committed Jun 9, 2016
2 parents 8f0db26 + 304e47d commit bd6372b
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 216 deletions.
20 changes: 13 additions & 7 deletions ckan/config/middleware/flask_app.py
Expand Up @@ -5,8 +5,8 @@
import urlparse

from flask import Flask
from flask import abort as flask_abort
from flask import request as flask_request
from flask import abort
from flask import request
from flask import _request_ctx_stack
from flask.ctx import _AppCtxGlobals
from flask.sessions import SessionInterface
Expand All @@ -27,6 +27,7 @@
from ckan.common import c
from ckan.plugins import PluginImplementations
from ckan.plugins.interfaces import IBlueprint
from ckan.views import identify_user


import logging
Expand All @@ -41,7 +42,9 @@ def make_flask_stack(conf, **app_conf):

debug = app_conf.get('debug', True)

root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

app = flask_app = CKANFlask(__name__)
app.debug = debug
app.template_folder = os.path.join(root, 'templates')
Expand Down Expand Up @@ -109,6 +112,10 @@ def save_session(self, app, session, response):
jinja_extensions.empty_and_escape
app.jinja_env.filters['truncate'] = jinja_extensions.truncate

@app.before_request
def ckan_before_request():
identify_user()

# Template context processors
@app.context_processor
def helper_functions():
Expand All @@ -120,8 +127,7 @@ def c_object():
return dict(c=c)

# Babel
app.config['BABEL_TRANSLATION_DIRECTORIES'] = os.path.join(
os.path.dirname(__file__), '..', 'i18n')
app.config['BABEL_TRANSLATION_DIRECTORIES'] = os.path.join(root, 'i18n')
app.config['BABEL_DOMAIN'] = 'ckan'

babel = Babel(app)
Expand Down Expand Up @@ -211,11 +217,11 @@ def __init__(self, import_name, *args, **kwargs):
# A label for the app handling this request (this app).
self.app_name = None

def join_party(self, request=flask_request):
def join_party(self, request=request):
# Bootstrap, turn the view function into a 404 after registering.
if self.partyline_connected:
# This route does not exist at the HTTP level.
flask_abort(404)
abort(404)
self.invitation_context = _request_ctx_stack.top
self.partyline = request.environ.get(WSGIParty.partyline_key)
self.app_name = request.environ.get('partyline_handling_app')
Expand Down
94 changes: 2 additions & 92 deletions ckan/lib/base.py
Expand Up @@ -218,76 +218,8 @@ def _identify_user(self):
c.user = None
c.userobj = None
c.author = user's IP address (unicode)'''
# see if it was proxied first
c.remote_addr = request.environ.get('HTTP_X_FORWARDED_FOR', '')
if not c.remote_addr:
c.remote_addr = request.environ.get('REMOTE_ADDR',
'Unknown IP Address')

# Authentication plugins get a chance to run here break as soon as a
# user is identified.
authenticators = p.PluginImplementations(p.IAuthenticator)
if authenticators:
for item in authenticators:
item.identify()
if c.user:
break

# We haven't identified the user so try the default methods
if not c.user:
self._identify_user_default()

# If we have a user but not the userobj let's get the userobj. This
# means that IAuthenticator extensions do not need to access the user
# model directly.
if c.user and not c.userobj:
c.userobj = model.User.by_name(c.user)

# general settings
if c.user:
c.author = c.user
else:
c.author = c.remote_addr
c.author = unicode(c.author)

def _identify_user_default(self):
'''
Identifies the user using two methods:
a) If they logged into the web interface then repoze.who will
set REMOTE_USER.
b) For API calls they may set a header with an API key.
'''

# environ['REMOTE_USER'] is set by repoze.who if it authenticates a
# user's cookie. But repoze.who doesn't check the user (still) exists
# in our database - we need to do that here. (Another way would be
# with an userid_checker, but that would mean another db access.
# See: http://docs.repoze.org/who/1.0/narr.html#module-repoze.who\
# .plugins.sql )
c.user = request.environ.get('REMOTE_USER', '')
if c.user:
c.user = c.user.decode('utf8')
c.userobj = model.User.by_name(c.user)
if c.userobj is None or not c.userobj.is_active():
# This occurs when a user that was still logged in is deleted,
# or when you are logged in, clean db
# and then restart (or when you change your username)
# There is no user object, so even though repoze thinks you
# are logged in and your cookie has ckan_display_name, we
# need to force user to logout and login again to get the
# User object.
session['lang'] = request.environ.get('CKAN_LANG')
session.save()

ev = request.environ
if 'repoze.who.plugins' in ev:
pth = getattr(ev['repoze.who.plugins']['friendlyform'],
'logout_handler_path')
h.redirect_to(pth)
else:
c.userobj = self._get_user_for_apikey()
if c.userobj is not None:
c.user = c.userobj.name
from ckan.views import identify_user
identify_user()

def __call__(self, environ, start_response):
"""Invoke the Controller"""
Expand Down Expand Up @@ -358,28 +290,6 @@ def _set_cors(self):
response.headers['Access-Control-Allow-Headers'] = \
"X-CKAN-API-KEY, Authorization, Content-Type"

def _get_user_for_apikey(self):
apikey_header_name = config.get(APIKEY_HEADER_NAME_KEY,
APIKEY_HEADER_NAME_DEFAULT)
apikey = request.headers.get(apikey_header_name, '')
if not apikey:
apikey = request.environ.get(apikey_header_name, '')
if not apikey:
# For misunderstanding old documentation (now fixed).
apikey = request.environ.get('HTTP_AUTHORIZATION', '')
if not apikey:
apikey = request.environ.get('Authorization', '')
# Forget HTTP Auth credentials (they have spaces).
if ' ' in apikey:
apikey = ''
if not apikey:
return None
self.log.debug("Received API Key: %s" % apikey)
apikey = unicode(apikey)
query = model.Session.query(model.User)
user = query.filter_by(apikey=apikey).first()
return user

def _get_page_number(self, params, key='page', default=1):
"""
Returns the page number from the provided params after
Expand Down
147 changes: 145 additions & 2 deletions ckan/tests/config/test_middleware.py
Expand Up @@ -2,12 +2,15 @@

import mock
import wsgiref
from nose.tools import assert_equals, assert_not_equals, eq_
from nose.tools import assert_not_equals, eq_
from routes import url_for
from flask import Blueprint

import ckan.model as model
import ckan.plugins as p
import ckan.tests.helpers as helpers
import ckan.tests.factories as factories
from ckan.common import c

from ckan.config.middleware import AskAppDispatcherMiddleware
from ckan.config.middleware.flask_app import CKANFlask
Expand All @@ -27,7 +30,7 @@ def test_homepage_with_middleware_activated(self):
app = self._get_test_app()
response = app.get(url=url_for(controller='home', action='index'))

assert_equals(200, response.status_int)
eq_(200, response.status_int)
# make sure we haven't overwritten the response too early.
assert_not_equals(
'response cleared by pylons response cleanup middleware',
Expand Down Expand Up @@ -406,6 +409,146 @@ def test_flask_core_and_pylons_core_route_is_served_by_flask(self):
eq_(res.environ['ckan.app'], 'flask_app')


class TestFlaskUserIdentifiedInRequest(helpers.FunctionalTestBase):

'''Flask identifies user during each request.
API route has been migrated to Flask, so using as an example.
'''

def test_user_objects_in_g_normal_user(self):
'''
A normal logged in user request will have expected user objects added
to request.
'''
self.app = helpers._get_test_app()
flask_app = helpers.find_flask_app(self.app)
user = factories.User()
test_user_obj = model.User.by_name(user['name'])

with flask_app.test_request_context('/api/action/status_show'):
self.app.get(
'/api/action/status_show',
extra_environ={'REMOTE_USER': user['name'].encode('ascii')},)
eq_(c.user, user['name'])
eq_(c.userobj, test_user_obj)
eq_(c.author, user['name'])
eq_(c.remote_addr, 'Unknown IP Address')

def test_user_objects_in_g_anon_user(self):
'''
An anon user request will have expected user objects added to request.
'''
self.app = helpers._get_test_app()
flask_app = helpers.find_flask_app(self.app)

with flask_app.test_request_context('/api/action/status_show'):
self.app.get(
'/api/action/status_show',
extra_environ={'REMOTE_USER': ''},)
eq_(c.user, '')
eq_(c.userobj, None)
eq_(c.author, 'Unknown IP Address')
eq_(c.remote_addr, 'Unknown IP Address')

def test_user_objects_in_g_sysadmin(self):
'''
A sysadmin user request will have expected user objects added to
request.
'''
self.app = helpers._get_test_app()
flask_app = helpers.find_flask_app(self.app)
user = factories.Sysadmin()
test_user_obj = model.User.by_name(user['name'])

with flask_app.test_request_context('/api/action/status_show'):
self.app.get(
'/api/action/status_show',
extra_environ={'REMOTE_USER': user['name'].encode('ascii')},)
eq_(c.user, user['name'])
eq_(c.userobj, test_user_obj)
eq_(c.author, user['name'])
eq_(c.remote_addr, 'Unknown IP Address')


class TestPylonsUserIdentifiedInRequest(helpers.FunctionalTestBase):

'''Pylons identifies user during each request.
Using a route setup via an extension to ensure we're always testing a
Pylons-flavoured request.
'''

def test_user_objects_in_c_normal_user(self):
'''
A normal logged in user request will have expected user objects added
to request.
'''
if not p.plugin_loaded('test_routing_plugin'):
p.load('test_routing_plugin')

app = self._get_test_app()
user = factories.User()
test_user_obj = model.User.by_name(user['name'])

resp = app.get(
'/from_pylons_extension_before_map',
extra_environ={'REMOTE_USER': user['name'].encode('ascii')})

# tmpl_context available on response
eq_(resp.tmpl_context.user, user['name'])
eq_(resp.tmpl_context.userobj, test_user_obj)
eq_(resp.tmpl_context.author, user['name'])
eq_(resp.tmpl_context.remote_addr, 'Unknown IP Address')

p.unload('test_routing_plugin')

def test_user_objects_in_c_anon_user(self):
'''
An anon user request will have expected user objects added to request.
'''
if not p.plugin_loaded('test_routing_plugin'):
p.load('test_routing_plugin')

app = self._get_test_app()

resp = app.get(
'/from_pylons_extension_before_map',
extra_environ={'REMOTE_USER': ''})

# tmpl_context available on response
eq_(resp.tmpl_context.user, '')
eq_(resp.tmpl_context.userobj, None)
eq_(resp.tmpl_context.author, 'Unknown IP Address')
eq_(resp.tmpl_context.remote_addr, 'Unknown IP Address')

p.unload('test_routing_plugin')

def test_user_objects_in_c_sysadmin(self):
'''
A sysadmin user request will have expected user objects added to
request.
'''
if not p.plugin_loaded('test_routing_plugin'):
p.load('test_routing_plugin')

app = self._get_test_app()
user = factories.Sysadmin()
test_user_obj = model.User.by_name(user['name'])

resp = app.get(
'/from_pylons_extension_before_map',
extra_environ={'REMOTE_USER': user['name'].encode('ascii')})

# tmpl_context available on response
eq_(resp.tmpl_context.user, user['name'])
eq_(resp.tmpl_context.userobj, test_user_obj)
eq_(resp.tmpl_context.author, user['name'])
eq_(resp.tmpl_context.remote_addr, 'Unknown IP Address')

p.unload('test_routing_plugin')


class MockRoutingPlugin(p.SingletonPlugin):

p.implements(p.IRoutes)
Expand Down
8 changes: 5 additions & 3 deletions ckan/tests/config/test_sessions.py
@@ -1,3 +1,5 @@
# encoding: utf-8

from nose.tools import ok_

from flask import Blueprint
Expand Down Expand Up @@ -58,11 +60,12 @@ def test_flash_populated_in_flask_view_redirect_to_pylons(self):

class FlashMessagePlugin(p.SingletonPlugin):
'''
A Flask and Pylons compatible IRoutes plugin to add Flask views and Pylons
actions to display flash messages.
A Flask and Pylons compatible IRoutes/IBlueprint plugin to add Flask views
and Pylons actions to display flash messages.
'''

p.implements(p.IRoutes, inherit=True)
p.implements(p.IBlueprint)

def flash_message_view(self):
'''Flask view that renders the flash message html template.'''
Expand All @@ -83,7 +86,6 @@ def get_blueprint(self):

# Create Blueprint for plugin
blueprint = Blueprint(self.name, self.__module__)
blueprint.template_folder = 'templates'
# Add plugin url rules to Blueprint object
rules = [
('/flask_add_flash_message_redirect_to_flask', 'add_flash_message',
Expand Down

0 comments on commit bd6372b

Please sign in to comment.