Skip to content

Commit

Permalink
Merge pull request #45 from Harvard-University-iCommons/task/python3
Browse files Browse the repository at this point in the history
Python 3 and Django 2 Updates
  • Loading branch information
Chris-Thornton-Harvard authored Oct 28, 2019
2 parents 1ec9756 + 8356c01 commit 0580633
Show file tree
Hide file tree
Showing 16 changed files with 114 additions and 63 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ dist
.idea
.tox/
.eggs/
.vscode/
16 changes: 7 additions & 9 deletions django_auth_lti/backends.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import logging
from time import time

import oauth2
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.core.exceptions import PermissionDenied
from ims_lti_py.tool_provider import DjangoToolProvider
from lti.contrib.django import DjangoToolProvider
from .request_validator import LTIRequestValidator

logger = logging.getLogger(__name__)



class LTIAuthBackend(ModelBackend):

"""
Expand Down Expand Up @@ -47,7 +46,7 @@ def authenticate(self, request):
raise PermissionDenied

logger.debug('using key/secret %s/%s' % (request_key, secret))
tool_provider = DjangoToolProvider(request_key, secret, request.POST.dict())
tool_provider = DjangoToolProvider.from_django_request(secret=secret, request=request)

postparams = request.POST.dict()

Expand All @@ -63,9 +62,10 @@ def authenticate(self, request):
logger.info("about to check the signature")

try:
request_is_valid = tool_provider.is_valid_request(request)
except oauth2.Error:
logger.exception(u'error attempting to validate LTI launch %s',
validator = LTIRequestValidator()
request_is_valid = tool_provider.is_valid_request(validator)
except:
logger.exception('error attempting to validate LTI launch %s',
postparams)
request_is_valid = False

Expand All @@ -84,8 +84,6 @@ def authenticate(self, request):

logger.info("done checking the timestamp")

# (this is where we should check the nonce)

# if we got this far, the user is good

user = None
Expand Down
4 changes: 2 additions & 2 deletions django_auth_lti/decorators.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from functools import wraps
from django.utils.decorators import available_attrs
from django.shortcuts import redirect
from django.core.urlresolvers import reverse_lazy
from django.urls import reverse_lazy
from django_auth_lti.verification import is_allowed


Expand All @@ -11,7 +11,7 @@ def decorator(view_func):
def _wrapped_view(request, *args, **kwargs):
if is_allowed(request, allowed_roles, raise_exception):
return view_func(request, *args, **kwargs)

return redirect(redirect_url)
return _wrapped_view
return decorator
4 changes: 2 additions & 2 deletions django_auth_lti/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings

from timer import Timer
from .timer import Timer

try:
from django.utils.deprecation import MiddlewareMixin
Expand Down Expand Up @@ -105,7 +105,7 @@ def process_request(self, request):
# If a custom role key is defined in project, merge into existing role list
if hasattr(settings, 'LTI_CUSTOM_ROLE_KEY'):
custom_roles = request.POST.get(settings.LTI_CUSTOM_ROLE_KEY, '').split(',')
lti_launch['roles'] += filter(None, custom_roles) # Filter out any empty roles
lti_launch['roles'] += [_f for _f in custom_roles if _f] # Filter out any empty roles

request.session['LTI_LAUNCH'] = lti_launch

Expand Down
10 changes: 5 additions & 5 deletions django_auth_lti/middleware_patched.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings

from timer import Timer
from .timer import Timer

from .thread_local import set_current_request

Expand Down Expand Up @@ -117,7 +117,7 @@ def process_request(self, request):
# If a custom role key is defined in project, merge into existing role list
if hasattr(settings, 'LTI_CUSTOM_ROLE_KEY'):
custom_roles = request.POST.get(settings.LTI_CUSTOM_ROLE_KEY, '').split(',')
lti_launch['roles'] += filter(None, custom_roles) # Filter out any empty roles
lti_launch['roles'] += [_f for _f in custom_roles if _f] # Filter out any empty roles

lti_launches = request.session.get('LTI_LAUNCH')
if not lti_launches:
Expand All @@ -133,13 +133,13 @@ def process_request(self, request):

# Limit the number of LTI launches stored in the session
max_launches = getattr(settings, 'LTI_AUTH_MAX_LAUNCHES', 10)
logger.info("LTI launch count %s [max=%s]" % (len(lti_launches.keys()), max_launches))
if len(lti_launches.keys()) >= max_launches:
logger.info("LTI launch count %s [max=%s]" % (len(list(lti_launches.keys())), max_launches))
if len(list(lti_launches.keys())) >= max_launches:
# If the current resource is being re-launched, then we should just invalidate the old launch,
# otherwise we should evict the oldest launch (FIFO).
remove_resource_link_id = resource_link_id
if remove_resource_link_id not in lti_launches:
ordered_launches = sorted(lti_launches.items(), key=lambda item: item[1].get('_order', -1))
ordered_launches = sorted(list(lti_launches.items()), key=lambda item: item[1].get('_order', -1))
remove_resource_link_id = ordered_launches[0][0]
invalidated_launch = lti_launches.pop(remove_resource_link_id)
logger.info("LTI launch invalidated: %s", invalidated_launch)
Expand Down
8 changes: 4 additions & 4 deletions django_auth_lti/mixins.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.core.urlresolvers import reverse_lazy
from django.urls import reverse_lazy
from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import redirect
from braces.views import LoginRequiredMixin
Expand All @@ -17,16 +17,16 @@ class LTIRoleRestrictionMixin(LTIUtilityMixin):
allowed_roles = None
redirect_url = reverse_lazy('not_authorized')
raise_exception = False

def dispatch(self, request, *args, **kwargs):
if self.allowed_roles is None:
raise ImproperlyConfigured(
"'LTIRoleRestrictionMixin' requires "
"'allowed_roles' attribute to be set.")

if is_allowed(request, self.allowed_roles, self.raise_exception):
return super(LTIRoleRestrictionMixin, self).dispatch(request, *args, **kwargs)

return redirect(self.redirect_url)


Expand Down
17 changes: 8 additions & 9 deletions django_auth_lti/patch_reverse.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
"""
Monkey-patch django's reverse function to add resource_link_id to all URLs.
"""
from urlparse import urlparse, urlunparse, parse_qs
from urllib import urlencode

from django.core import urlresolvers
from urllib.parse import urlparse, urlunparse, parse_qs
from urllib.parse import urlencode

from .thread_local import get_current_request

Expand Down Expand Up @@ -32,7 +30,7 @@ def reverse(*args, **kwargs):
# was not passed or is False
parsed = urlparse(url)
query = parse_qs(parsed.query)
if 'resource_link_id' not in query.keys():
if 'resource_link_id' not in list(query.keys()):
query['resource_link_id'] = request.LTI.get('resource_link_id')
url = urlunparse(
(parsed.scheme, parsed.netloc, parsed.path, parsed.params,
Expand All @@ -46,9 +44,10 @@ def patch_reverse():
Monkey-patches the reverse function. Will not patch twice.
"""
global django_reverse
if urlresolvers.reverse is not reverse:
django_reverse = urlresolvers.reverse
urlresolvers.reverse = reverse
from django import urls
if urls.reverse is not reverse:
django_reverse = urls.reverse
urls.reverse = reverse

# Django 1.10 moves url helper functions like `reverse` into a new urls
# module, so we need to patch it as well. In addition, the
Expand All @@ -57,10 +56,10 @@ def patch_reverse():
# retroactively patch that `reverse` reference as well.
try:
from django import urls, shortcuts

urls.reverse = reverse
shortcuts.reverse = reverse
except ImportError:
pass


patch_reverse()
52 changes: 52 additions & 0 deletions django_auth_lti/request_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import logging
import time

from django.conf import settings
from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured
from oauthlib.common import to_unicode
from oauthlib.oauth1 import RequestValidator

logger = logging.getLogger(__name__)
NONCE_TTL = 3600 * 6


class LTIRequestValidator(RequestValidator):

enforce_ssl = True
allowed_signature_methods = ['HMAC-SHA1']
timestamp_lifetime = NONCE_TTL

#TODO: Find out if this is used...
dummy_secret = 'secret'
dummy_client = (u'dummy_'
'2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae')

def check_client_key(self, key):
# any non-empty string is OK as a client key
return len(key) > 0

def check_nonce(self, nonce):
# any non-empty string is OK as a nonce
return len(nonce) > 0

def validate_client_key(self, client_key, request):
return client_key in settings.LTI_OAUTH_CREDENTIALS

def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
request, request_token=None,
access_token=None):
nonce_key = 'lti_nonce:{}-{}-{}-{}'.format(client_key, timestamp, nonce, request_token or access_token)
exists = cache.get(nonce_key)
if exists:
logger.debug("nonce already exists: %s", nonce)
return False
else:
logger.debug("unused nonce, storing: %s", nonce)
cache.set(nonce_key, nonce, NONCE_TTL)
return True

def get_client_secret(self, client_key, request):
secret = settings.LTI_OAUTH_CREDENTIALS.get(client_key, self.dummy_secret)
# make sure secret val is unicode
return to_unicode(secret)
4 changes: 2 additions & 2 deletions django_auth_lti/tests/test_lti_auth_middleware.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import unittest
import mock
from mock import patch
from unittest import mock
from unittest.mock import patch
from django.test import RequestFactory
from django.contrib.auth import models
from django_auth_lti.middleware_patched import MultiLTILaunchAuthMiddleware
Expand Down
14 changes: 7 additions & 7 deletions django_auth_lti/tests/test_verification.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,40 @@
from unittest import TestCase
from mock import MagicMock
from unittest.mock import MagicMock
from django_auth_lti.verification import is_allowed
from django.core.exceptions import ImproperlyConfigured, PermissionDenied

class TestVerification(TestCase):

def test_is_allowed_config_failure(self):
request = MagicMock(LTI={})
allowed_roles = ["admin", "student"]
self.assertRaises(ImproperlyConfigured, is_allowed,
request, allowed_roles, False)

def test_is_allowed_success(self):
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(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(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(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(LTI={"roles":["TF"]})
allowed_roles = ["admin", "student"]
Expand Down
2 changes: 1 addition & 1 deletion django_auth_lti/timer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def __exit__(self, *args):
self.secs = self.end - self.start
self.msecs = self.secs * 1000 # millisecs
if self.verbose:
print 'elapsed time: %f ms' % self.msecs
print(('elapsed time: %f ms' % self.msecs))

'''
Example usage:
Expand Down
15 changes: 8 additions & 7 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

setup(
name='django-auth-lti',
version='1.3.1',
version='2.0.1',
packages=['django_auth_lti'],
include_package_data=True,
license='TBD License', # example license
Expand All @@ -24,16 +24,17 @@
'License :: OSI Approved :: BSD License', # example license
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
],
install_requires=[
"Django>=1.6",
"ims-lti-py==0.6",
"django-braces==1.3.1",
"oauth2==1.9.0.post1", # to catch errors uncaught by ims-lti-py
"Django>=2.0,<3.0",
"lti==0.9.4",
"django-braces==1.13.0",
"oauthlib==3.1.0",
"requests_oauthlib"
],
tests_require=[
'mock',
Expand Down
2 changes: 1 addition & 1 deletion tests/helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.test import RequestFactory
from django.contrib.auth import models
import mock
from unittest import mock

def build_lti_launch_request(post_data):
"""
Expand Down
4 changes: 2 additions & 2 deletions tests/test_lti_auth_middleware.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import unittest
from mock import patch
from unittest.mock import patch
from django_auth_lti.middleware import LTIAuthMiddleware
import helpers
from . import helpers


@patch('django_auth_lti.middleware.logger')
Expand Down
Loading

0 comments on commit 0580633

Please sign in to comment.