Skip to content
Snippets Groups Projects
Commit 1a9e206b authored by Douglas Hall's avatar Douglas Hall
Browse files

Modified the middleware to support multiple LTI launches in a single session

parent ea2125a4
No related branches found
No related tags found
No related merge requests found
......@@ -4,3 +4,4 @@ build
dist
*.egg-info
*.egg
.idea
from django.contrib import auth
import logging
import json
import django_auth_lti.patch_reverse
from collections import OrderedDict
from django.contrib import auth
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
from timer import Timer
import logging
from .thread_local import set_current_request
logger = logging.getLogger(__name__)
......@@ -18,11 +25,19 @@ class LTIAuthMiddleware(object):
If authentication is successful, the user is automatically logged in to
persist the user in the session.
If the request is not an LTI launch request, do nothing.
The LTI launch parameter dict is stored invthe session keyed with the
resource_link_id to uniquely identify LTI launches of the LTI producer.
The LTI launch parameter dict is also set as the 'LTI' attribute on the
current request object to simplify access to the parameters.
The current request object is set as a thread local attribute so that the
monkey-patching of django's reverse() function (see ./__init__.py) can access
it in order to retrieve the current resource_link_id.
"""
def process_request(self, request):
logger.debug('inside process_request %s' % request.path)
# AuthenticationMiddleware is required so that request.user exists.
if not hasattr(request, 'user'):
logger.debug('improperly configured: requeset has no user attr')
......@@ -33,8 +48,8 @@ class LTIAuthMiddleware(object):
" 'django.contrib.auth.middleware.AuthenticationMiddleware'"
" before the PINAuthMiddleware class.")
resource_link_id = None
if request.method == 'POST' and request.POST.get('lti_message_type') == 'basic-lti-launch-request':
logger.debug('received a basic-lti-launch-request - authenticating the user')
# authenticate and log the user in
......@@ -53,11 +68,12 @@ class LTIAuthMiddleware(object):
logger.debug('login() took %s s' % t.secs)
resource_link_id = request.POST.get('resource_link_id', None)
lti_launch = {
'custom_canvas_account_id': request.POST.get('custom_canvas_account_id', None),
'lis_outcome_service_url': request.POST.get('lis_outcome_service_url', None),
'lti_message_type': request.POST.get('lti_message_type', None),
'resource_link_id': request.POST.get('resource_link_id', None),
'resource_link_id': resource_link_id,
'user_image': request.POST.get('user_image', None),
'lis_outcome_service_url': request.POST.get('lis_outcome_service_url', None),
'lis_course_offering_sourcedid': request.POST.get('lis_course_offering_sourcedid', None),
......@@ -99,11 +115,28 @@ class LTIAuthMiddleware(object):
custom_roles = request.POST.get(settings.LTI_CUSTOM_ROLE_KEY, '').split(',')
lti_launch['roles'] += filter(None, custom_roles) # Filter out any empty roles
request.session['LTI_LAUNCH'] = lti_launch
lti_launches = request.session.get('LTI_LAUNCH')
if not lti_launches:
lti_launches = OrderedDict()
request.session['LTI_LAUNCH'] = lti_launches
# Limit the number of LTI launches stored in the session
if len(lti_launches.keys()) >= getattr(settings, 'LTI_AUTH_MAX_LAUNCHES', 10):
invalidated_launch = lti_launches.popitem(last=False)
logger.info("LTI launch invalidated: %s", json.dumps(invalidated_launch, indent=4))
lti_launches[resource_link_id] = lti_launch
logger.info("LTI launch added to session: %s", json.dumps(lti_launch, indent=4))
else:
# User could not be authenticated!
logger.warning('user could not be authenticated via LTI params; let the request continue in case another auth plugin is configured')
else:
resource_link_id = request.GET.get('resource_link_id', None)
setattr(request, 'LTI', request.session.get('LTI_LAUNCH', {}).get(resource_link_id, {}))
set_current_request(request)
if not request.LTI:
logger.warning("Could not find LTI launch for resource_link_id %s", resource_link_id)
def clean_username(self, username, request):
"""
......
......@@ -7,7 +7,7 @@ from django_auth_lti.verification import is_allowed
class LTIUtilityMixin(object):
def get_lti_param(self, keyword, default=None):
return self.request.session['LTI_LAUNCH'].get(keyword, default)
return self.request.LTI.get(keyword, default)
def current_user_roles(self):
return self.get_lti_param('roles', [])
......
"""
Monkey-patch django.core.urlresolvers.reverse to add resource_link_id to all URLs
"""
from django.core import urlresolvers
from .thread_local import get_current_request
django_reverse = None
def reverse(*args, **kwargs):
"""
Call django's reverse function and append the current resource_link_id as a query parameter
"""
request = get_current_request()
url = django_reverse(*args, **kwargs)
if '?' not in url:
url += '?'
return "%s&resource_link_id=%s" % (url, request.LTI.get('resource_link_id'))
def patch_reverse():
"""
Monkey-patches the django.core.urlresolvers.reverse function. Will not patch twice.
"""
global django_reverse
if urlresolvers.reverse is not reverse:
django_reverse = urlresolvers.reverse
urlresolvers.reverse = reverse
patch_reverse()
window.globals.append_resource_link_id = function(url){
if (!url.match(/resource_link_id/)) {
if (!url.match(/\?/g)) {
url += '?';
}
return url + '&resource_link_id=' + window.globals.RESOURCE_LINK_ID;
}
};
$(document).ajaxSend(function(event, jqxhr, settings){
settings.url = window.globals.append_resource_link_id(settings.url);
});
{% load static %}
<script>
window.globals = {
RESOURCE_LINK_ID: '{{ request.LTI.resource_link_id }}'
};
</script>
<script src="{% static 'django_auth_lti/js/resource_link_id.js' %}"></script>
\ No newline at end of file
......@@ -19,6 +19,9 @@ class TestLTIAuthMiddleware(unittest.TestCase):
"""
# Add message type to post data
post_data.update(lti_message_type='basic-lti-launch-request')
# Add resource_link_id to post data
post_data.update(resource_link_id='d202fb112a14f27107149ed874bf630aa8e029a5')
request = RequestFactory().post('/fake/lti/launch', post_data)
request.user = mock.Mock(name='User', spec=models.User)
request.session = {}
......@@ -36,7 +39,7 @@ class TestLTIAuthMiddleware(unittest.TestCase):
})
with patch('django_auth_lti.middleware.settings', LTI_CUSTOM_ROLE_KEY='test_custom_role_key'):
self.mw.process_request(request)
self.assertEqual(request.session['LTI_LAUNCH']['roles'], ['RoleOne', 'RoleTwo', 'My', 'Custom', 'Roles'])
self.assertEqual(request.LTI.get('roles'), ['RoleOne', 'RoleTwo', 'My', 'Custom', 'Roles'])
@patch('django_auth_lti.middleware.auth')
def test_roles_merge_with_empty_custom_roles(self, mock_auth, mock_logger):
......@@ -49,7 +52,7 @@ class TestLTIAuthMiddleware(unittest.TestCase):
})
with patch('django_auth_lti.middleware.settings', LTI_CUSTOM_ROLE_KEY='test_custom_role_key'):
self.mw.process_request(request)
self.assertEqual(request.session['LTI_LAUNCH']['roles'], ['RoleOne', 'RoleTwo'])
self.assertEqual(request.LTI.get('roles'), ['RoleOne', 'RoleTwo'])
@patch('django_auth_lti.middleware.auth')
def test_roles_not_merged_with_no_role_key(self, mock_auth, mock_logger):
......@@ -61,5 +64,4 @@ class TestLTIAuthMiddleware(unittest.TestCase):
'test_custom_role_key': 'My,Custom,Roles',
})
self.mw.process_request(request)
self.assertEqual(request.session['LTI_LAUNCH']['roles'], ['RoleOne', 'RoleTwo'])
self.assertEqual(request.LTI.get('roles'), ['RoleOne', 'RoleTwo'])
......@@ -6,37 +6,37 @@ from django.core.exceptions import ImproperlyConfigured, PermissionDenied
class TestVerification(TestCase):
def test_is_allowed_config_failure(self):
request = MagicMock(session={})
request = MagicMock(LTI={})
allowed_roles = ["admin", "student"]
self.assertRaises(ImproperlyConfigured, is_allowed,
request, allowed_roles, False)
def test_is_allowed_success(self):
request = MagicMock(session={"LTI_LAUNCH": {"roles":["admin"]}})
request = MagicMock(LTI={"roles": ["admin"]})
allowed_roles = ["admin", "student"]
user_is_allowed = is_allowed(request, allowed_roles, False)
self.assertTrue(user_is_allowed)
def test_is_allowed_success_one_role(self):
request = MagicMock(session={"LTI_LAUNCH": {"roles":["admin"]}})
request = MagicMock(LTI={"roles": ["admin"]})
allowed_roles = "admin"
user_is_allowed = is_allowed(request, allowed_roles, False)
self.assertTrue(user_is_allowed)
def test_is_allowed_failure(self):
request = MagicMock(session={"LTI_LAUNCH": {"roles":[]}})
request = MagicMock(LTI={"roles":[]})
allowed_roles = ["admin", "student"]
user_is_allowed = is_allowed(request, allowed_roles, False)
self.assertFalse(user_is_allowed)
def test_is_allowed_failure_one_role(self):
request = MagicMock(session={"LTI_LAUNCH": {"roles":[]}})
request = MagicMock(LTI={"roles":[]})
allowed_roles = "admin"
user_is_allowed = is_allowed(request, allowed_roles, False)
self.assertFalse(user_is_allowed)
def test_is_allowed_exception(self):
request = MagicMock(session={"LTI_LAUNCH": {"roles":["TF"]}})
request = MagicMock(LTI={"roles":["TF"]})
allowed_roles = ["admin", "student"]
self.assertRaises(PermissionDenied, is_allowed,
request, allowed_roles, True)
"""
Allow for setting/getting of the current request in thread local
"""
import threading
_thread_local = threading.local()
def set_current_request(request):
setattr(_thread_local, 'request', request)
def get_current_request():
return getattr(_thread_local, 'request', None)
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
def is_allowed(request, allowed_roles, raise_exception):
# allowed_roles can either be a string (for just one)
# or a tuple or list (for several)
......@@ -8,13 +9,13 @@ def is_allowed(request, allowed_roles, raise_exception):
else:
allowed = allowed_roles
lti_launch = request.session.get('LTI_LAUNCH', None)
if not isinstance(lti_launch, dict):
if not request.LTI:
# If this is raised, then likely the project doesn't have
# the correct settings or is being run outside of an lti context
raise ImproperlyConfigured("No LTI_LAUNCH found in session")
user_roles = lti_launch.get('roles', [])
is_user_allowed = set(allowed) & set(user_roles)
raise ImproperlyConfigured("Missing LTI launch parameters")
user_roles = request.LTI.get('roles', [])
is_user_allowed = set(allowed) & set(user_roles)
if not is_user_allowed and raise_exception:
raise PermissionDenied
......
......@@ -8,7 +8,7 @@ os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
setup(
name='django-auth-lti',
version='1.0',
version='2.0',
packages=['django_auth_lti'],
include_package_data=True,
license='TBD License', # example license
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment