Skip to content

Commit

Permalink
Add redis service to improve LTI launch performance (tl-its-umich-e…
Browse files Browse the repository at this point in the history
…du#277)

* Add some experimental refactoring, cleanup, and settings

* Tweak error throwing so message is shown to user; log LtiException

* Modify error message extraction, construction

* Update import syntax, order

* Remove unused import, one-line other imports

* Add newline

* Add maybe functional redis implementation?

* Simplify redis setup; add password auth

* Upgrade to latest django-redis

* Remove django-mysql cache configs

* Apply suggestions from code review

Update REDIS to support full URL and document values

---------

Co-authored-by: Code Hugger (Matthew Jones) <[email protected]>
  • Loading branch information
ssciolla and jonespm authored Nov 19, 2024
1 parent 4543361 commit 13d56fb
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 34 deletions.
7 changes: 7 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ ALLOWED_HOSTS=.ngrok.io,.localhost,127.0.0.1
# Database Password
# DB_PASSWORD=cae_pw

##### Redis

# Redis Password, default is blank
REDIS_PASS=
# Redis URL, full url of redis server and database number, has a default in the code
# REDIS_URL= redis://redis:6379/1

##### Gunicorn server options
# Number of workers to start
# GUNICORN_WORKERS=4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ webpack-stats.json
# MySQL data files
.data
ngrok.yml
redis.conf

# Temp files
*.swp
Expand Down
71 changes: 44 additions & 27 deletions backend/canvas_app_explorer/lti1p3.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,29 @@
from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from pylti1p3.contrib.django import DjangoOIDCLogin, DjangoMessageLaunch, \
DjangoCacheDataStorage, DjangoDbToolConf
from pylti1p3.contrib.django import (
DjangoCacheDataStorage, DjangoDbToolConf, DjangoMessageLaunch, DjangoOIDCLogin
)
from pylti1p3.exception import LtiException

from .canvas_roles import STAFF_COURSE_ROLES


logger = logging.getLogger(__name__)

COURSE_MEMBERSHIP = 'http://purl.imsglobal.org/vocab/lis/v2/membership'
TOOL_CONF = DjangoDbToolConf()

CacheConfig = namedtuple('CacheConfig', ['launch_data_storage', 'cache_lifetime'])

COURSE_MEMBERSHIP = 'http://purl.imsglobal.org/vocab/lis/v2/membership'
DUMMY_CACHE = 'DummyCache'

TOOL_CONF = DjangoDbToolConf()
def extract_error_message(error: Exception) -> Union[str, None]:
"""
Check Exception for a string message as the first position argument.
"""
if len(error.args) >= 1 and isinstance(error.args[0], str):
return error.args[0]
return None

# Custom LTI variable keys
USERNAME_KEY = 'user_username'
Expand Down Expand Up @@ -111,17 +121,14 @@ def generate_config_json(request: HttpRequest) -> \
return HttpResponse(config_json, content_type='application/json')


def get_cache_config():
CacheConfig = namedtuple('CacheConfig', ['is_dummy_cache', 'launch_data_storage', 'cache_lifetime'])
is_dummy_cache = DUMMY_CACHE in settings.DB_CACHE_CONFIGS['BACKEND']
launch_data_storage = DjangoCacheDataStorage(cache_name='default') if not is_dummy_cache else None
def get_cache_config() -> CacheConfig:
launch_data_storage = DjangoCacheDataStorage()
cache_ttl = settings.DB_CACHE_CONFIGS['CACHE_TTL']
cache_lifetime = cache_ttl if cache_ttl else 7200
return CacheConfig(is_dummy_cache, launch_data_storage, cache_lifetime)
return CacheConfig(launch_data_storage, cache_lifetime)


def create_user_in_django(request: HttpRequest, message_launch: ExtendedDjangoMessageLaunch):
launch_data = message_launch.get_launch_data()
def create_user_in_django(request: HttpRequest, launch_data: Dict[str, Any]):
logger.debug(f'lti launch data {launch_data}')
custom_params = launch_data['https://purl.imsglobal.org/spec/lti/claim/custom']
logger.debug(f'lti_custom_param {custom_params}')
Expand Down Expand Up @@ -191,34 +198,44 @@ def create_user_in_django(request: HttpRequest, message_launch: ExtendedDjangoMe


@csrf_exempt
def login(request):
def login(request: HttpRequest):
cache_config = get_cache_config()
target_link_uri = request.POST.get('target_link_uri', request.GET.get('target_link_uri'))
if not target_link_uri:
error_message = 'LTI Login failed due to missing "target_link_uri" param'
return lti_error(error_message)
CacheConfig = get_cache_config()
oidc_login = DjangoOIDCLogin(request, TOOL_CONF, launch_data_storage=CacheConfig.launch_data_storage)
oidc_login = DjangoOIDCLogin(request, TOOL_CONF, launch_data_storage=cache_config.launch_data_storage)
return oidc_login.enable_check_cookies().redirect(target_link_uri)


@require_POST
@csrf_exempt
def launch(request):
CacheConfig = get_cache_config()
message_launch = ExtendedDjangoMessageLaunch(request, TOOL_CONF, launch_data_storage=CacheConfig.launch_data_storage)
if not CacheConfig.is_dummy_cache:
# fetch platform's public key from cache instead of calling the API will speed up the launch process
message_launch.set_public_key_caching(CacheConfig.launch_data_storage,
cache_lifetime=CacheConfig.cache_lifetime)
else:
logger.info('DummyCache is set up, recommended atleast to us Mysql DB cache for LTI advantage services')
def launch(request: HttpRequest):
cache_config = get_cache_config()
message_launch = ExtendedDjangoMessageLaunch(request, TOOL_CONF, launch_data_storage=cache_config.launch_data_storage)
message_launch.set_public_key_caching(cache_config.launch_data_storage, cache_config.cache_lifetime)

try:
launch_data: Dict[str, Any] = message_launch.get_launch_data()
except LtiException as lti_exception:
logger.error(lti_exception)
message = f'LTI launch error occurred. Please try launching the tool again.'
error_message = extract_error_message(lti_exception)
if error_message:
message += f' Error: {error_message}.'
response = HttpResponse(message)
response.status_code = 401
return response

# TODO: Implement custom AUTHENTICATION_BACKEND rather than using this one
try:
create_user_in_django(request, message_launch)
create_user_in_django(request, launch_data)
except PermissionDenied as e:
message = f': {e.args[0]}' if len(e.args) >= 1 else ''
return HttpResponseForbidden('Permission denied' + message)
message = 'Permission denied.'
error_message = extract_error_message(e)
if error_message:
message += ' ' + error_message
return HttpResponseForbidden(message)

url = reverse('home')
return redirect(url)
22 changes: 15 additions & 7 deletions backend/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django_mysql',
'webpack_loader',
'rest_framework',
'pylti1p3.contrib.django.lti1p3_tool_config',
Expand Down Expand Up @@ -154,12 +153,19 @@
# So request works over the proxy
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

DB_CACHE_CONFIGS = os.getenv('DB_CACHE_CONFIGS',
{'CACHE_TTL': 600, 'BACKEND': 'django_mysql.cache.MySQLCache',
'LOCATION': 'canvas_app_explorer_cache',
'CACHE_KEY_PREFIX': 'app_explorer',
'CACHE_OPTIONS': {'COMPRESS_MIN_LENGTH': 5000, 'COMPRESS_LEVEL': 6}
})
DB_CACHE_CONFIGS = os.getenv(
'DB_CACHE_CONFIGS',
{
'CACHE_TTL': 600,
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': os.getenv('REDIS_URL', 'redis://redis:6379/1'),
'CACHE_KEY_PREFIX': 'app_explorer',
'CACHE_OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'PASSWORD': os.getenv('REDIS_PASS')
}
}
)

CACHES = {
'default': {
Expand All @@ -171,6 +177,8 @@
}
}

SESSION_ENGINE = 'django.contrib.sessions.backends.cache'

LOGGING = {
'version': 1,
'disable_existing_loggers': False,
Expand Down
5 changes: 5 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,8 @@ services:
- .env
environment:
- DEBUG=True
redis:
image: redis:7
volumes:
- ./redis.conf:/usr/local/etc/redis/redis.conf
command: [ "redis-server", "--include /usr/local/etc/redis/redis.conf" ]
2 changes: 2 additions & 0 deletions redis.conf.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
loglevel debug
requirepass 'some password here'
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ Django==4.2.16
django-csp==3.8 # For Content Security Policy
django-db-file-storage==0.5.6.1 # Support for storage in the database
django-mysql==4.15.0
django-redis==5.4.0
django-tinymce==4.1.0 # Rich text editor
django-watchman==1.3
django-webpack-loader==3.1.1
Pillow==11.0.0
whitenoise==6.8.2 # For serving static files


# DRF
djangorestframework==3.15.2
django-filter==24.3 # Filtering support
Expand Down

0 comments on commit 13d56fb

Please sign in to comment.