Skip to content

Commit

Permalink
Merge pull request #38 from uw-it-aca/feature/django-views
Browse files Browse the repository at this point in the history
adds authz for rest view
  • Loading branch information
jlaney authored Jan 9, 2018
2 parents 851d3a7 + 97b123f commit 3035b53
Show file tree
Hide file tree
Showing 11 changed files with 210 additions and 196 deletions.
5 changes: 2 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
sudo: false
language: python
dist: precise
python:
- '2.7'
- '3.4'
- '3.5'
- '3.6'
before_script:
- pip install -e .
- pip install pep8
- pip install pycodestyle
- pip install coverage
- pip install python-coveralls
- cp travis-ci/manage.py manage.py
script:
- pep8 blti/ --exclude=migrations
- pycodestyle blti/ --exclude=migrations
- coverage run --source=blti manage.py test blti
after_success:
- coveralls
Expand Down
37 changes: 37 additions & 0 deletions blti/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,40 @@ class BLTIKeyStore(models.Model):
consumer_key = models.CharField(max_length=80, unique=True)
shared_secret = models.CharField(max_length=80)
added_date = models.DateTimeField(auto_now_add=True)


class BLTIData(object):
def __init__(self, **kwargs):
# Canvas internal IDs
self.canvas_course_id = kwargs.get('custom_canvas_course_id')
self.canvas_user_id = kwargs.get('custom_canvas_user_id')
self.canvas_account_id = kwargs.get('custom_canvas_account_id')

# SIS IDs
self.course_sis_id = kwargs.get('lis_course_offering_sourcedid')
self.user_sis_id = kwargs.get('lis_person_sourcedid')
self.account_sis_id = kwargs.get('custom_canvas_account_sis_id')

# Course attributes
self.course_short_name = kwargs.get('context_label')
self.course_long_name = kwargs.get('context_title')

# User attributes
self.user_login_id = kwargs.get('custom_canvas_user_login_id')
self.user_full_name = kwargs.get('lis_person_name_full')
self.user_first_name = kwargs.get('lis_person_name_given')
self.user_last_name = kwargs.get('lis_person_name_family')
self.user_email = kwargs.get('lis_person_contact_email_primary')
self.user_avatar_url = kwargs.get('user_image')

# LTI app attributes
self.link_title = kwargs.get('resource_link_title')
self.return_url = kwargs.get('launch_presentation_return_url')

# Canvas hostname
self.canvas_api_domain = kwargs.get('custom_canvas_api_domain')

self.data = kwargs

def get(self, name):
return self.data.get(name)
2 changes: 1 addition & 1 deletion blti/performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def wrapper(*args, **kwargs):
start = time.time()
try:
val = func(*args, **kwargs)
except:
except Exception:
raise
finally:
module = self.__module__
Expand Down
8 changes: 4 additions & 4 deletions blti/templates/blti/401.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
<h2 class="ui-state-error">
<i class="fa fa-warning"></i> <span>Unauthorized</span>
</h2>
<p>
It appears that you don't have permission to access this page. Please make sure you're authorized to view this content.
If you think you should be able to view this page, please use the "Help" link to notify support of the problem.
</p>
<p><strong>{{ error }}</strong></p>
<p>
It appears that you don't have permission to access this page. Please make sure you're authorized to view this content. If you think you should be able to view this page, please use the "Help" link to notify support of the problem.
</p>
</div>
</div>
{% endblock content %}
13 changes: 0 additions & 13 deletions blti/templates/blti/error.html

This file was deleted.

43 changes: 43 additions & 0 deletions blti/tests.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from django.conf import settings
from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured
from blti.validators import BLTIOauth, BLTIRoles
from blti.crypto import aes128cbc
from blti.models import BLTIData
from blti import BLTI, BLTIException


Expand All @@ -21,6 +23,43 @@ def test_get_consumer(self):
self.assertEquals(BLTIOauth().get_consumer('ABC').secret, '12345')


class BLTIDataTest(TestCase):
def test_attributes(self):
params = getattr(settings, 'CANVAS_LTI_V1_LAUNCH_PARAMS', {})
blti = BLTIData(**params)

self.assertEquals(blti.link_title, 'Example App')
self.assertEquals(blti.return_url,
'https://example.instructure.com/courses/123456')
self.assertEquals(blti.canvas_course_id, '123456')
self.assertEquals(blti.course_sis_id, '2018-spring-ABC-101-A')
self.assertEquals(blti.course_short_name, 'ABC 101 A')
self.assertEquals(blti.course_long_name, 'ABC 101 A: Example Course')
self.assertEquals(blti.canvas_user_id, '123456')
self.assertEquals(blti.user_login_id, 'javerage')
self.assertEquals(blti.user_sis_id, 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX')
self.assertEquals(blti.user_full_name, 'James Average')
self.assertEquals(blti.user_first_name, 'James')
self.assertEquals(blti.user_last_name, 'Average')
self.assertEquals(blti.user_email, '[email protected]')
self.assertEquals(
blti.user_avatar_url, (
'https://example.instructure.com/images/thumbnails/123456/'
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'))
self.assertEquals(blti.canvas_account_id, '12345')
self.assertEquals(blti.account_sis_id, 'example:account')
self.assertEquals(blti.canvas_api_domain, 'example.instructure.com')

def test_get(self):
params = getattr(settings, 'CANVAS_LTI_V1_LAUNCH_PARAMS', {})
blti = BLTIData(**params)

self.assertEquals(blti.get('custom_canvas_course_id'), '123456')
self.assertEquals(blti.get('lis_person_contact_email_primary'),
'[email protected]')
self.assertEquals(blti.get('invalid_param_name'), None)


class BLTIRolesTest(TestCase):
def test_has_admin_role(self):
self.assertEquals(
Expand All @@ -40,6 +79,10 @@ def test_has_learner_role(self):
False, BLTIRoles().has_learner_role(['Faculty', 'Staff']))

def test_validate(self):
blti = None
self.assertRaises(
BLTIException, BLTIRoles().validate, blti, 'member')

blti = {'roles': 'Member'}
self.assertEquals(None, BLTIRoles().validate(blti, 'member'))
self.assertRaises(
Expand Down
26 changes: 14 additions & 12 deletions blti/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,17 @@ def has_instructor_role(self, roles):
def has_learner_role(self, roles):
return self._has_role(roles, self.LIS_LEARNER)

def validate(self, blti, visibility):
if visibility:
roles = ','.join([blti.get('roles', ''),
blti.get('ext_roles', '')]).split(',')

# ADMIN includes instructors, MEMBER includes
if not (self.has_admin_role(roles) or
self.has_instructor_role(roles) or
(visibility == self.MEMBER and
self.has_learner_role(roles))):
raise BLTIException(
'You do not have privilege to view this content.')
def validate(self, blti, visibility='member'):
if blti is None:
raise BLTIException('Missing LTI parameters')

roles = ','.join([blti.get('roles', ''),
blti.get('ext_roles', '')]).split(',')

# ADMIN includes instructors, MEMBER includes
if not (self.has_admin_role(roles) or
self.has_instructor_role(roles) or
(visibility == self.MEMBER and
self.has_learner_role(roles))):
raise BLTIException(
'You do not have privilege to view this content.')
107 changes: 107 additions & 0 deletions blti/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from django.http import HttpResponse
from django.views.generic.base import TemplateView
from django.views.decorators.csrf import csrf_exempt
from blti import BLTI, BLTIException
from blti.models import BLTIData
from blti.validators import BLTIOauth, BLTIRoles
from blti.performance import log_response_time
import json
try:
from urllib import unquote_plus
except ImportError:
from urllib.parse import unquote_plus # Python3


class BLTIView(TemplateView):
authorized_role = 'member'

def dispatch(self, request, *args, **kwargs):
try:
kwargs['blti_params'] = self.validate(request)
except BLTIException as err:
self.template_name = 'blti/401.html'
return self.render_to_response({'error': err}, status=401)

return super(BLTIView, self).dispatch(request, *args, **kwargs)

def render_to_response(self, context, **kwargs):
response = super(BLTIView, self).render_to_response(context, **kwargs)
self.add_headers(response=response, **kwargs)
return response

def add_headers(self, **kwargs):
pass

def get_session(self, request):
return BLTI().get_session(request)

def set_session(self, request, **kwargs):
BLTI().set_session(request, **kwargs)

def validate(self, request):
blti_params = self.get_session(request)
self.authorize(blti_params)
return blti_params

def authorize(self, blti_params):
BLTIRoles().validate(blti_params, visibility=self.authorized_role)
self.blti = BLTIData(blti_params)


class BLTILaunchView(BLTIView):
http_method_names = ['post']

@csrf_exempt
def dispatch(self, request, *args, **kwargs):
return super(BLTILaunchView, self).dispatch(request, *args, **kwargs)

def validate(self, request):
params = {}
body = request.read()
try:
params = dict((k, v) for k, v in [tuple(
map(unquote_plus, kv.split('='))
) for kv in body.split('&')])
except Exception:
raise BLTIException('Missing or malformed parameter or value')

blti_params = BLTIOauth().validate(request, params=params)
self.authorize(blti_params)
self.set_session(request, **blti_params)

return blti_params


class RawBLTIView(BLTILaunchView):
template_name = 'blti/raw.html'
authorized_role = 'admin'


class RESTDispatch(BLTIView):
"""
A superclass for API views
"""
authorized_role = 'member'

@log_response_time
def dispatch(self, request, *args, **kwargs):
try:
kwargs['blti_params'] = self.validate(request)
except BLTIException as ex:
return self.error_response(401, ex)

return super(BLTIView, self).dispatch(request, *args, **kwargs)

def render_to_response(self, context, **kwargs):
return self.json_response(content=context)

def error_response(self, status, message='', content={}):
content['error'] = str(message)
return self.json_response(content=content, status=status)

def json_response(self, content={}, status=200):
response = HttpResponse(json.dumps(content),
status=status,
content_type='application/json')
self.add_headers(response=response, **kwargs)
return response
86 changes: 0 additions & 86 deletions blti/views/__init__.py

This file was deleted.

Loading

0 comments on commit 3035b53

Please sign in to comment.