Skip to content

Commit

Permalink
Wip
Browse files Browse the repository at this point in the history
  • Loading branch information
giovannicimolin committed Jul 21, 2020
1 parent a6cf1b1 commit 8074666
Show file tree
Hide file tree
Showing 25 changed files with 265 additions and 27 deletions.
2 changes: 1 addition & 1 deletion lti_consumer/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@


admin.site.register(LtiConfiguration)
admin.site.register(LtiAgsLineItem)
admin.site.register(LtiAgsLineItem)
3 changes: 2 additions & 1 deletion lti_consumer/lti_1p3/ags.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
LTI Advantage Assignments and Grades service implementation
"""


class LtiAgs:
"""
LTI Advantage Consumer
Expand Down Expand Up @@ -72,4 +73,4 @@ def get_lti_ags_launch_claim(self):
if self.lineitem_url:
ags_claim.update({"lineitem": self.lineitem_url})

return ags_claim
return ags_claim
7 changes: 6 additions & 1 deletion lti_consumer/lti_1p3/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@
"scope",
])

LTI_1P3_ACCESS_TOKEN_SCOPES = []

LTI_1P3_ACCESS_TOKEN_SCOPES = [
# LTI-AGS Scopes
'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly',
'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem',
]


class LTI_1P3_CONTEXT_TYPE(Enum): # pylint: disable=invalid-name
Expand Down
14 changes: 10 additions & 4 deletions lti_consumer/lti_1p3/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ def _validate_preflight_response(self, response):
except AssertionError:
raise exceptions.PreflightRequestValidationFailure()

def check_token(self, token, allowed_scope=''):
def check_token(self, token, allowed_scopes=[]):
"""
Check if token has access to allowed scopes.
"""
Expand All @@ -420,8 +420,14 @@ def check_token(self, token, allowed_scope=''):

# Check if token has permission for the requested scope,
# and throws exception if not.
if allowed_scope not in token_scopes:
raise exceptions.UnauthorizedToken()
# In `allowed_scopes` is empty, return true (just check
# token validity).
if allowed_scopes:
return any(
[scope in allowed_scopes for scope in token_scopes]
)

return True

def set_extra_claim(self, claim):
"""
Expand Down Expand Up @@ -482,4 +488,4 @@ def enable_ags(
)

# Include LTI AGS claim inside the LTI Launch message
self.set_extra_claim(self.ags.get_lti_ags_launch_claim())
self.set_extra_claim(self.ags.get_lti_ags_launch_claim())
4 changes: 4 additions & 0 deletions lti_consumer/lti_1p3/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class TokenSignatureExpired(Lti1p3Exception):
pass


class UnauthorizedToken(Lti1p3Exception):
pass


class NoSuitableKeys(Lti1p3Exception):
pass

Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,15 @@ def authenticate(self, request):
raise exceptions.AuthenticationFailed(msg)

# Verify token validity
import pdb; pdb.set_trace()

# This doesn't validate specific permissions, just checks if the token
# is valid or not.
try:
lti_consumer.check_token(auth[1])
# Attach token to request to be used to check permissions later
request.lti_auth_token = auth[1]
except:
msg = _('LTI configuration not found.')
raise exceptions.AuthenticationFailed(msg)

# Passing parameters back to the view through the request.
# Not exactly optimal.
Expand All @@ -50,4 +57,4 @@ def authenticate(self, request):

# Return (None, None) since this isn't tied to any authentication
# backend on Django, and it's just used for LTI endpoints.
return (None, None)
return (None, None)
File renamed without changes.
29 changes: 29 additions & 0 deletions lti_consumer/lti_1p3/extensions/rest_framework/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from rest_framework import permissions

from lti_consumer.lti_1p3 import exceptions


class LtiAgsPermissions(permissions.BasePermission):
def has_permission(self, request, view):
if view.action in ['list', 'retrieve']:
try:
request.lti_consumer.check_token(
request.lti_auth_token,
[
'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly',
'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem',
],
)
return True
except exceptions.UnauthorizedToken:
return False
elif view.action in ['create', 'update', 'partial_update', 'delete']:
try:
request.lti_consumer.check_token(
request.lti_auth_token,
'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem'
)
return True
except exceptions.UnauthorizedToken:
return False
return False
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ class LineItemsRenderer(renderers.JSONRenderer):

class LineItemRenderer(renderers.JSONRenderer):
media_type = 'application/vnd.ims.lis.v2.lineitem+json'
format = 'json'
format = 'json'
36 changes: 36 additions & 0 deletions lti_consumer/lti_1p3/key_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,39 @@ def get_public_jwk(self):
public_keys.append(self.key)

return json.loads(public_keys.dump_jwks())

def validate_and_decode(self, token, iss=None, aud=None):
"""
Check if a platform token is valid, and return allowed scopes.
Validates a token sent by the tool using the platform's RSA Key.
Optionally validate iss and aud claims if provided.
"""
try:
# Get KID from JWT header
jwt = JWT().unpack(token)

# Verify message signature
message = JWS().verify_compact(token, keys=[self.key])

# If message is valid, check expiration from JWT
if 'exp' in message and message['exp'] < time.time():
raise exceptions.TokenSignatureExpired()

# Validate issuer claim
if iss and 'iss' in message and message['iss'] != iss:
raise exceptions.InvalidClaimValue()

# Validate audience claim
if aud and 'aud' in message and aud in message['aud']:
raise exceptions.InvalidClaimValue()

# Else return token contents
return message

except NoSuitableSigningKeys:
raise exceptions.NoSuitableKeys()
except BadSyntax:
raise exceptions.MalformedJwtToken()
except WrongNumberOfParts:
raise exceptions.MalformedJwtToken()
2 changes: 1 addition & 1 deletion lti_consumer/lti_xblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,7 @@ def _get_lti1p3_consumer(self):

# Check if enabled and setup LTI-AGS
consumer.enable_ags(
lineitems_url="http://f9c95f3db2ca.ngrok.io/api/lti_consumer/v1/lti/1/lti-ags"
lineitems_url="http://f9c95f3db2ca.ngrok.io/api/lti_consumer/v1/lti/1/lti-ags"
)

return consumer
Expand Down
24 changes: 24 additions & 0 deletions lti_consumer/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 2.2.14 on 2020-07-17 17:12

from django.db import migrations, models
import opaque_keys.edx.django.models


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='LtiConfiguration',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('version', models.CharField(choices=[('LTI_1P1', 'LTI 1.3'), ('LTI_1P3', 'LTI 1.3 (with LTI Advantage Support)')], default='LTI_1P1', max_length=10)),
('config_store', models.CharField(choices=[('CONFIG_ON_XBLOCK', 'Configuration Stored on XBlock fields')], default='CONFIG_ON_XBLOCK', max_length=10)),
('location', opaque_keys.edx.django.models.UsageKeyField(blank=True, db_index=True, max_length=255, null=True)),
],
),
]
18 changes: 18 additions & 0 deletions lti_consumer/migrations/0002_auto_20200717_1720.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.2.14 on 2020-07-17 17:20

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('lti_consumer', '0001_initial'),
]

operations = [
migrations.AlterField(
model_name='lticonfiguration',
name='version',
field=models.CharField(choices=[('LTI_1P1', 'LTI 1.1'), ('LTI_1P3', 'LTI 1.3 (with LTI Advantage Support)')], default='LTI_1P1', max_length=10),
),
]
18 changes: 18 additions & 0 deletions lti_consumer/migrations/0003_auto_20200717_1722.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.2.14 on 2020-07-17 17:22

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('lti_consumer', '0002_auto_20200717_1720'),
]

operations = [
migrations.AlterField(
model_name='lticonfiguration',
name='config_store',
field=models.CharField(choices=[('CONFIG_ON_XBLOCK', 'Configuration Stored on XBlock fields')], default='CONFIG_ON_XBLOCK', max_length=25),
),
]
28 changes: 28 additions & 0 deletions lti_consumer/migrations/0004_ltiagslineitem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 2.2.14 on 2020-07-17 17:44

from django.db import migrations, models
import django.db.models.deletion
import opaque_keys.edx.django.models


class Migration(migrations.Migration):

dependencies = [
('lti_consumer', '0003_auto_20200717_1722'),
]

operations = [
migrations.CreateModel(
name='LtiAgsLineItem',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('resource_id', models.CharField(blank=True, max_length=50)),
('resource_link_id', opaque_keys.edx.django.models.UsageKeyField(blank=True, db_index=True, max_length=255, null=True)),
('score_maximum', models.IntegerField()),
('tag', models.CharField(blank=True, max_length=50)),
('start_date_time', models.DateTimeField(blank=True)),
('end_date_time', models.DateTimeField(blank=True)),
('lti_configuration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lti_consumer.LtiConfiguration')),
],
),
]
24 changes: 24 additions & 0 deletions lti_consumer/migrations/0005_auto_20200717_1827.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 2.2.14 on 2020-07-17 18:27

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('lti_consumer', '0004_ltiagslineitem'),
]

operations = [
migrations.AddField(
model_name='ltiagslineitem',
name='label',
field=models.CharField(default='', max_length=100),
preserve_default=False,
),
migrations.AlterField(
model_name='ltiagslineitem',
name='resource_id',
field=models.CharField(blank=True, max_length=100),
),
]
23 changes: 23 additions & 0 deletions lti_consumer/migrations/0006_auto_20200717_1828.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 2.2.14 on 2020-07-17 18:28

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('lti_consumer', '0005_auto_20200717_1827'),
]

operations = [
migrations.AlterField(
model_name='ltiagslineitem',
name='end_date_time',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='ltiagslineitem',
name='start_date_time',
field=models.DateTimeField(blank=True, null=True),
),
]
19 changes: 19 additions & 0 deletions lti_consumer/migrations/0007_auto_20200717_2011.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 2.2.14 on 2020-07-17 20:11

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('lti_consumer', '0006_auto_20200717_1828'),
]

operations = [
migrations.AlterField(
model_name='ltiagslineitem',
name='lti_configuration',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='lti_consumer.LtiConfiguration'),
),
]
2 changes: 1 addition & 1 deletion lti_consumer/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,4 @@ def __str__(self):
return "{} - {}".format(
self.resource_link_id,
self.label,
)
)
4 changes: 2 additions & 2 deletions lti_consumer/plugin/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
)


## TEST CODE
# TEST CODE
from rest_framework import routers

from lti_consumer.views.ags import LtiAgsLineItemViewset
from lti_consumer.views import LtiAgsLineItemViewset

router = routers.SimpleRouter(trailing_slash=False)
router.register(r'lti-ags', LtiAgsLineItemViewset, base_name='lti-ags-view')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,4 @@ class Meta:
'resourceLinkId',
'startDateTime',
'endDateTime',
)
)
Loading

0 comments on commit 8074666

Please sign in to comment.