Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LTI configurations pipeline EDLY-6642 #4

Merged
merged 1 commit into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions lti_consumer/lti_xblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,15 @@ def _get_lti_launch_url(self, consumer):
"""
launch_url = self.launch_url

if (
hasattr(consumer, "lti_launch_url")
and consumer.lti_launch_url
and self.lti_version == "lti_1p1"
):
launch_url = consumer.lti_launch_url
elif hasattr(consumer, "launch_url") and consumer.launch_url:
launch_url = consumer.launch_url

# The lti_launch_url property only exists on the LtiConsumer1p1. The LtiConsumer1p3 does not have an
# attribute with this name, so ensure that we're accessing it on the appropriate consumer class.
if consumer and self.config_type in ("database", "external") and self.lti_version == "lti_1p1":
Expand Down Expand Up @@ -1567,7 +1576,7 @@ def _get_lti_1p3_launch_url(self, consumer):
# The "external" config_type is not supported for LTI 1.3, only LTI 1.1. Therefore, ensure that we define
# the lti_1p3_launch_url using the LTI 1.3 consumer only for config_types that support LTI 1.3.
# TODO: This needs to change when the LTI 1.3 support is added to the external config_type in the future.
if consumer and self.lti_version == "lti_1p3" and self.config_type == "database":
if consumer and self.lti_version == "lti_1p3" and consumer.launch_url:
lti_1p3_launch_url = consumer.launch_url

return lti_1p3_launch_url
Expand All @@ -1589,11 +1598,7 @@ def _get_context_for_template(self):
allowed_attributes = dict(bleach.sanitizer.ALLOWED_ATTRIBUTES, **{'img': ['src', 'alt']})
sanitized_comment = bleach.clean(self.score_comment, tags=allowed_tags, attributes=allowed_attributes)

lti_consumer = None
# Don't pull from the Django database unless the config_type is one that stores the LTI configuration in the
# database.
if self.config_type in ("database", "external"):
lti_consumer = self._get_lti_consumer()
lti_consumer = self._get_lti_consumer()

launch_url = self._get_lti_launch_url(lti_consumer)
lti_block_launch_handler = self._get_lti_block_launch_handler()
Expand Down
24 changes: 24 additions & 0 deletions lti_consumer/migrations/0017_auto_20240415_1329.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 2.2.24 on 2024-04-15 13:29

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


class Migration(migrations.Migration):

dependencies = [
('lti_consumer', '0016_lticonfiguration_lti_1p3_proctoring_enabled'),
]

operations = [
migrations.AddField(
model_name='lticonfiguration',
name='course_key',
field=opaque_keys.edx.django.models.CourseKeyField(blank=True, db_index=True, default=None, help_text='Enable these settings course wide', max_length=255, null=True),
),
migrations.AddField(
model_name='lticonfiguration',
name='organization_slug',
field=models.CharField(blank=True, db_index=True, default=None, help_text='Enable these settings organization wide', max_length=50, null=True),
),
]
106 changes: 104 additions & 2 deletions lti_consumer/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,70 @@ def generate_client_id():
return str(uuid.uuid4())


class LtiConfigurationManager(models.Manager):
"""
Custom manager for LtiConfiguration to prioritize course-wide or
organization-wide configuration."
"""

def get(self, *args, **kwargs):
fields_to_copy = [
"lti_1p1_launch_url",
"lti_1p1_client_key",
"lti_1p1_client_secret",
"lti_1p3_internal_private_key",
"lti_1p3_internal_private_key_id",
"lti_1p3_internal_public_jwk",
"lti_1p3_client_id",
"lti_1p3_oidc_url",
"lti_1p3_launch_url",
"lti_1p3_tool_public_key",
"lti_1p3_tool_keyset_url",
"lti_advantage_deep_linking_enabled",
"lti_advantage_deep_linking_launch_url",
]

block_lti_configuration = super().get(*args, **kwargs)

if block_lti_configuration.location is None or "organization_slug" in kwargs:
return block_lti_configuration

new_kwargs = {
"course_key": block_lti_configuration.location.course_key,
"version": block_lti_configuration.version,
"organization_slug": block_lti_configuration.location.course_key.org,
}

try:
filter_configuration = super().get(*args, **new_kwargs)

for field_name in fields_to_copy:
setattr(
block_lti_configuration,
field_name,
getattr(filter_configuration, field_name),
)
block_lti_configuration.external_id = LtiConfiguration.CONFIG_EXTERNAL
return block_lti_configuration

except LtiConfiguration.DoesNotExist:
new_kwargs["course_key"] = None
try:
filter_configuration = super().get(*args, **new_kwargs)

for field_name in fields_to_copy:
setattr(
block_lti_configuration,
field_name,
getattr(filter_configuration, field_name),
)
block_lti_configuration.external_id = LtiConfiguration.CONFIG_EXTERNAL
return block_lti_configuration

except LtiConfiguration.DoesNotExist:
return block_lti_configuration


class LtiConfiguration(models.Model):
"""
Model to store LTI Configuration for LTI 1.1 and 1.3.
Expand Down Expand Up @@ -232,6 +296,17 @@ class LtiConfiguration(models.Model):
help_text='Enable LTI Proctoring Services',
)

course_key = CourseKeyField(
max_length=255, db_index=True, default=None, blank=True, null=True,
help_text='Enable these settings course wide'
)
organization_slug = models.CharField(
max_length=50, db_index=True, default=None, blank=True, null=True,
help_text='Enable these settings organization wide'
)

objects = LtiConfigurationManager()

def clean(self):
if self.config_store == self.CONFIG_ON_XBLOCK and self.location is None:
raise ValidationError({
Expand Down Expand Up @@ -319,7 +394,12 @@ def _get_lti_1p1_consumer(self):
Return a class of LTI 1.1 consumer.
"""
# If LTI configuration is stored in the XBlock.
if self.config_store == self.CONFIG_ON_XBLOCK:
if self.external_id == LtiConfiguration.CONFIG_EXTERNAL:
key = self.lti_1p1_client_key
secret = self.lti_1p1_client_secret
launch_url = self.lti_1p1_launch_url

elif self.config_store == self.CONFIG_ON_XBLOCK:
block = compat.load_enough_xblock(self.location)
key, secret = block.lti_provider_key_secret
launch_url = block.launch_url
Expand Down Expand Up @@ -352,6 +432,9 @@ def get_lti_advantage_deep_linking_enabled(self):
"""
Return whether LTI 1.3 Advantage Deep Linking is enabled.
"""
if self.external_id == LtiConfiguration.CONFIG_EXTERNAL:
return self.lti_advantage_deep_linking_enabled

if self.config_store == self.CONFIG_EXTERNAL:
# TODO: Add support for CONFIG_EXTERNAL for LTI 1.3.
raise NotImplementedError("CONFIG_EXTERNAL is not supported for LTI 1.3 Advantage services: %s")
Expand All @@ -365,6 +448,9 @@ def get_lti_advantage_deep_linking_launch_url(self):
"""
Return the LTI 1.3 Advantage Deep Linking launch URL.
"""
if self.external_id == LtiConfiguration.CONFIG_EXTERNAL:
return self.lti_advantage_deep_linking_launch_url

if self.config_store == self.CONFIG_EXTERNAL:
# TODO: Add support for CONFIG_EXTERNAL for LTI 1.3.
raise NotImplementedError("CONFIG_EXTERNAL is not supported for LTI 1.3 Advantage services: %s")
Expand Down Expand Up @@ -484,7 +570,23 @@ def _get_lti_1p3_consumer(self):
if self.lti_1p3_proctoring_enabled and self.config_store == self.CONFIG_ON_DB:
consumer_class = LtiProctoringConsumer

if self.config_store == self.CONFIG_ON_XBLOCK:
if self.external_id == LtiConfiguration.CONFIG_EXTERNAL:
consumer = consumer_class(
iss=get_lti_api_base(),
lti_oidc_url=self.lti_1p3_oidc_url,
lti_launch_url=self.lti_1p3_launch_url,
client_id=self.lti_1p3_client_id,
# Deployment ID hardcoded to 1 since
# we're not using multi-tenancy.
deployment_id="1",
# XBlock Private RSA Key
rsa_key=self.lti_1p3_private_key,
rsa_key_id=self.lti_1p3_private_key_id,
# LTI 1.3 Tool key/keyset url
tool_key=self.lti_1p3_tool_public_key,
tool_keyset_url=self.lti_1p3_tool_keyset_url,
)
elif self.config_store == self.CONFIG_ON_XBLOCK:
block = compat.load_enough_xblock(self.location)

consumer = consumer_class(
Expand Down
Loading