Skip to content

Commit 3e94a09

Browse files
committed
feat: Add LTI utilities app (Bad user fix)
1 parent 61309b4 commit 3e94a09

File tree

14 files changed

+575
-0
lines changed

14 files changed

+575
-0
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[run]
2+
branch = True
3+
data_file = .coverage
4+
source=ol_openedx_lti_utilities
5+
omit =
6+
test_settings.py
7+
*/migrations/*
8+
*admin.py
9+
*/static/*
10+
*/templates/*
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Change Log
2+
##########
3+
4+
..
5+
All enhancements and patches to ol_openedx_lti_utilities will be documented
6+
in this file. It adheres to the structure of https://keepachangelog.com/ ,
7+
but in reStructuredText instead of Markdown (for ease of incorporation into
8+
Sphinx documentation and the PyPI description).
9+
10+
This project adheres to Semantic Versioning (https://semver.org/).
11+
12+
.. There should always be an "Unreleased" section for changes pending release.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Copyright (C) 2023 MIT Open Learning
2+
3+
All rights reserved.
4+
5+
Redistribution and use in source and binary forms, with or without
6+
modification, are permitted provided that the following conditions are met:
7+
8+
* Redistributions of source code must retain the above copyright notice, this
9+
list of conditions and the following disclaimer.
10+
11+
* Redistributions in binary form must reproduce the above copyright notice,
12+
this list of conditions and the following disclaimer in the documentation
13+
and/or other materials provided with the distribution.
14+
15+
* Neither the name of the copyright holder nor the names of its
16+
contributors may be used to endorse or promote products derived from
17+
this software without specific prior written permission.
18+
19+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
include CHANGELOG.rst
2+
include LICENSE.txt
3+
include README.rst
4+
recursive-include ol_openedx_lti_utilities *.html *.png *.gif *.js *.css *.jpg *.jpeg *.svg *.py
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
LTI Utilities Plugin
2+
=============================
3+
4+
A django app plugin to add LTI related utilities in Open edX platform.
5+
6+
7+
Installation
8+
------------
9+
10+
For detailed installation instructions, please refer to the `plugin installation guide <../../docs#installation-guide>`_.
11+
12+
Installation required in:
13+
14+
* LMS
15+
16+
How To Use
17+
----------
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
ol_openedx_lti_utilities
3+
"""
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""
2+
Ol Openedx LTI Utilities App Configuration
3+
"""
4+
5+
from django.apps import AppConfig
6+
from edx_django_utils.plugins import PluginURLs
7+
from openedx.core.djangoapps.plugins.constants import ProjectType
8+
9+
10+
class LTIUtilitiesConfig(AppConfig):
11+
"""
12+
Configuration class for Ol Openedx LTI Utilities
13+
"""
14+
15+
name = "ol_openedx_lti_utilities"
16+
17+
plugin_app = {
18+
PluginURLs.CONFIG: {
19+
ProjectType.LMS: {
20+
PluginURLs.NAMESPACE: "",
21+
PluginURLs.REGEX: "^api/lti-user-fix/",
22+
PluginURLs.RELATIVE_PATH: "urls",
23+
}
24+
},
25+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""
2+
OL Open edX LTI Utilities URLs
3+
"""
4+
5+
from django.urls import re_path
6+
7+
from ol_openedx_lti_utilities.views import LtiUserFixView
8+
9+
urlpatterns = [
10+
re_path(
11+
r"^",
12+
LtiUserFixView.as_view(),
13+
name="lti_user_fix",
14+
),
15+
]
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""
2+
Views for LTI Utilities operations.
3+
"""
4+
5+
import logging
6+
import re
7+
8+
from django.contrib.auth.models import User
9+
from django.http import Http404
10+
from edx_rest_framework_extensions import permissions
11+
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
12+
from edx_rest_framework_extensions.auth.session.authentication import (
13+
SessionAuthenticationAllowInactiveUser,
14+
)
15+
from lms.djangoapps.lti_provider.models import LtiUser
16+
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
17+
from rest_framework import status
18+
from rest_framework.response import Response
19+
from rest_framework.views import APIView
20+
21+
log = logging.getLogger(__name__)
22+
23+
# Matches 40-character random username used by LTI users
24+
LTI_USERNAME_REGEX = r"^[A-Za-z0-9]{40}$"
25+
26+
27+
class LtiUserFixView(APIView):
28+
"""
29+
Fix the auth record of an LTI-created user.
30+
31+
POST /api/lti-user-fix/
32+
33+
Request payload:
34+
{
35+
"email": "<user_email>",
36+
"username": "<desired_username>"
37+
}
38+
39+
Responses:
40+
- 200: Fixed successfully
41+
- 400: Bad request or user does not need fixing
42+
- 404: No matching LTI user found
43+
"""
44+
45+
# Same authentication model as CourseModesMixin
46+
authentication_classes = (
47+
JwtAuthentication,
48+
BearerAuthenticationAllowInactiveUser,
49+
SessionAuthenticationAllowInactiveUser,
50+
)
51+
52+
# Same permission enforcement as CourseModesMixin
53+
permission_classes = (permissions.JWT_RESTRICTED_APPLICATION_OR_USER_ACCESS,)
54+
55+
# Only POST allowed
56+
http_method_names = ["post"]
57+
58+
def post(self, request):
59+
"""
60+
Handle POST request to fix LTI user authentication record.
61+
62+
This endpoint fixes LTI-created users who have random 40-character usernames
63+
by updating their username to a more user-friendly value.
64+
65+
Parameters
66+
----------
67+
request : Request
68+
The HTTP request object containing email and username in request.data
69+
70+
Returns
71+
-------
72+
Response
73+
HTTP 200 on successful fix, HTTP 400 for bad requests or users that
74+
don't need fixing, HTTP 404 if no matching LTI user is found
75+
76+
Raises
77+
------
78+
Http404
79+
If no LTI user exists for the provided email address
80+
"""
81+
user_email = request.data.get("email")
82+
user_username = request.data.get("username")
83+
84+
if not user_email or not user_username:
85+
log.error("email and username are required")
86+
return Response(
87+
{"detail": "email and username are required"},
88+
status=status.HTTP_400_BAD_REQUEST,
89+
)
90+
91+
def get_bad_lti_user_or_raise(email):
92+
"""Get LTI user or raise if none exists."""
93+
qs = LtiUser.objects.filter(edx_user__email=email)
94+
if not qs.exists():
95+
raise LtiUser.DoesNotExist
96+
return qs.first()
97+
98+
try:
99+
lti_user = get_bad_lti_user_or_raise(user_email)
100+
101+
# LTI-created users have a 40-char random username → "bad" username
102+
bad_lti_user = (
103+
lti_user.edx_user
104+
if re.match(LTI_USERNAME_REGEX, lti_user.edx_user.username)
105+
else None
106+
)
107+
108+
if not bad_lti_user:
109+
return Response(
110+
{"detail": "User does not need fixing"},
111+
status=status.HTTP_400_BAD_REQUEST,
112+
)
113+
114+
except LtiUser.DoesNotExist as exc:
115+
log.error("No user was found against the given email (%s)", user_email) # noqa: TRY400
116+
raise Http404 from exc
117+
118+
# Fix username
119+
user = User.objects.get(email=user_email)
120+
user.username = user_username
121+
user.save()
122+
123+
return Response(status=status.HTTP_200_OK)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[project]
2+
name = "ol-openedx-lti-utilities"
3+
version = "0.1.0"
4+
description = "An Open edX plugin to add utilities for LTI operations"
5+
authors = [
6+
{name = "MIT Office of Digital Learning"}
7+
]
8+
license = "BSD-3-Clause"
9+
readme = "README.rst"
10+
requires-python = ">=3.11"
11+
dependencies = [
12+
"Django>=4.0",
13+
"djangorestframework>=3.14.0",
14+
"edx-django-utils>4.0.0",
15+
"edx-drf-extensions>=10.0.0",
16+
"edx-opaque-keys",
17+
]
18+
19+
[project.entry-points."lms.djangoapp"]
20+
ol_openedx_lti_utilities = "ol_openedx_lti_utilities.app:LTIUtilitiesConfig"
21+
22+
[build-system]
23+
requires = ["hatchling"]
24+
build-backend = "hatchling.build"
25+
26+
[tool.hatch.build.targets.wheel]
27+
packages = ["ol_openedx_lti_utilities"]
28+
include = [
29+
"ol_openedx_lti_utilities/**/*.py",
30+
]
31+
32+
[tool.hatch.build.targets.sdist]
33+
include = [
34+
"ol_openedx_lti_utilities/**/*",
35+
"README.rst",
36+
"pyproject.toml",
37+
]

0 commit comments

Comments
 (0)