-
Notifications
You must be signed in to change notification settings - Fork 5
feat: Add LTI utilities app (Bad user fix) #652
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| [run] | ||
| branch = True | ||
| data_file = .coverage | ||
| source=ol_openedx_lti_utilities | ||
| omit = | ||
| test_settings.py | ||
| */migrations/* | ||
| *admin.py | ||
| */static/* | ||
| */templates/* |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| Change Log | ||
| ########## | ||
|
|
||
| .. | ||
| All enhancements and patches to ol_openedx_lti_utilities will be documented | ||
| in this file. It adheres to the structure of https://keepachangelog.com/ , | ||
| but in reStructuredText instead of Markdown (for ease of incorporation into | ||
| Sphinx documentation and the PyPI description). | ||
|
|
||
| This project adheres to Semantic Versioning (https://semver.org/). | ||
|
|
||
| .. There should always be an "Unreleased" section for changes pending release. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| Copyright (C) 2023 MIT Open Learning | ||
|
|
||
| All rights reserved. | ||
|
|
||
| Redistribution and use in source and binary forms, with or without | ||
| modification, are permitted provided that the following conditions are met: | ||
|
|
||
| * Redistributions of source code must retain the above copyright notice, this | ||
| list of conditions and the following disclaimer. | ||
|
|
||
| * Redistributions in binary form must reproduce the above copyright notice, | ||
| this list of conditions and the following disclaimer in the documentation | ||
| and/or other materials provided with the distribution. | ||
|
|
||
| * Neither the name of the copyright holder nor the names of its | ||
| contributors may be used to endorse or promote products derived from | ||
| this software without specific prior written permission. | ||
|
|
||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||
| AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
| IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
| DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||
| FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||
| DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||
| SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||
| CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||
| OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
| OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| include CHANGELOG.rst | ||
| include LICENSE.txt | ||
| include README.rst | ||
| recursive-include ol_openedx_lti_utilities *.html *.png *.gif *.js *.css *.jpg *.jpeg *.svg *.py |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| LTI Utilities Plugin | ||
| ============================= | ||
|
|
||
| A django app plugin to add LTI related utilities in Open edX platform. | ||
|
|
||
|
|
||
| Installation | ||
| ------------ | ||
|
|
||
| For detailed installation instructions, please refer to the `plugin installation guide <../../docs#installation-guide>`_. | ||
|
|
||
| Installation required in: | ||
|
|
||
| * LMS | ||
|
|
||
| How To Use | ||
| ---------- | ||
|
|
||
| **API Request** | ||
|
|
||
| To manually call the API, Send a POST request to ``<LMS_BASE>/lti-user-fix/`` with a JSON body containing the following field: | ||
| - ``email``: The email address of the user whose LTI account needs to be fixed. | ||
|
|
||
| A sample request looks like below: | ||
|
|
||
| :: | ||
|
|
||
| POST: http://local.openedx.io:8000/api/lti-user-fix/ | ||
|
|
||
| Payload: | ||
| { | ||
| "email": "[email protected]" | ||
| } | ||
|
|
||
|
|
||
| API Response | ||
| ------------ | ||
|
|
||
| The successful response would be an indication that an LTI user in bad state was found and fixed. The response status code would be 200. |
3 changes: 3 additions & 0 deletions
3
src/ol_openedx_lti_utilities/ol_openedx_lti_utilities/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| """ | ||
| ol_openedx_lti_utilities | ||
| """ |
25 changes: 25 additions & 0 deletions
25
src/ol_openedx_lti_utilities/ol_openedx_lti_utilities/app.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| """ | ||
| Ol Openedx LTI Utilities App Configuration | ||
| """ | ||
|
|
||
| from django.apps import AppConfig | ||
| from edx_django_utils.plugins import PluginURLs | ||
| from openedx.core.djangoapps.plugins.constants import ProjectType | ||
|
|
||
|
|
||
| class LTIUtilitiesConfig(AppConfig): | ||
| """ | ||
| Configuration class for Ol Openedx LTI Utilities | ||
| """ | ||
|
|
||
| name = "ol_openedx_lti_utilities" | ||
|
|
||
| plugin_app = { | ||
| PluginURLs.CONFIG: { | ||
| ProjectType.LMS: { | ||
| PluginURLs.NAMESPACE: "", | ||
| PluginURLs.REGEX: "^api/lti-user-fix/", | ||
| PluginURLs.RELATIVE_PATH: "urls", | ||
| } | ||
| }, | ||
| } |
15 changes: 15 additions & 0 deletions
15
src/ol_openedx_lti_utilities/ol_openedx_lti_utilities/urls.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| """ | ||
| OL Open edX LTI Utilities URLs | ||
| """ | ||
|
|
||
| from django.urls import re_path | ||
|
|
||
| from ol_openedx_lti_utilities.views import LtiUserFixView | ||
|
|
||
| urlpatterns = [ | ||
| re_path( | ||
| r"^", | ||
| LtiUserFixView.as_view(), | ||
| name="lti_user_fix", | ||
| ), | ||
| ] |
120 changes: 120 additions & 0 deletions
120
src/ol_openedx_lti_utilities/ol_openedx_lti_utilities/views.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| """ | ||
| Views for LTI Utilities operations. | ||
| """ | ||
|
|
||
| import logging | ||
|
|
||
| from django.db import transaction | ||
| from django.http import Http404, HttpResponseBadRequest | ||
| from edx_rest_framework_extensions import permissions | ||
| from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication | ||
| from edx_rest_framework_extensions.auth.session.authentication import ( | ||
| SessionAuthenticationAllowInactiveUser, | ||
| ) | ||
| from lms.djangoapps.lti_provider.models import LtiUser | ||
| from openedx.core.djangoapps.user_api.accounts.utils import ( | ||
| create_retirement_request_and_deactivate_account, | ||
| ) | ||
| from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser | ||
| from rest_framework import status | ||
| from rest_framework.response import Response | ||
| from rest_framework.views import APIView | ||
| from social_django.models import UserSocialAuth | ||
|
|
||
| log = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| PLACEHOLDER_EMAIL_DOMAIN = "lti_example.com" | ||
|
|
||
|
|
||
| class LtiUserFixView(APIView): | ||
| """ | ||
| Fix the auth record of an LTI-created user. | ||
|
|
||
| POST /api/lti-user-fix/ | ||
|
|
||
| Request payload: | ||
| { | ||
| "email": "<user_email>", | ||
| } | ||
|
|
||
| Responses: | ||
| - 200: Fixed successfully | ||
| - 400: Bad request or user does not need fixing | ||
| - 404: No matching LTI user found | ||
| """ | ||
|
|
||
| # Same authentication model as CourseModesMixin | ||
| authentication_classes = ( | ||
| JwtAuthentication, | ||
| BearerAuthenticationAllowInactiveUser, | ||
| SessionAuthenticationAllowInactiveUser, | ||
| ) | ||
|
|
||
| # Same permission enforcement as CourseModesMixin | ||
| permission_classes = (permissions.JWT_RESTRICTED_APPLICATION_OR_USER_ACCESS,) | ||
|
|
||
| # Only POST allowed | ||
| http_method_names = ["post"] | ||
|
|
||
| def post(self, request): | ||
| """ | ||
| Handle POST request to fix LTI user authentication record. | ||
|
|
||
| This endpoint fixes LTI-created users who have lti_user_id as usernames | ||
|
|
||
| Parameters | ||
| ---------- | ||
| request : Request | ||
| The HTTP request object containing email in the payload. | ||
|
|
||
| Returns | ||
| ------- | ||
| Response | ||
| HTTP 200 on successful fix, HTTP 400 for bad requests or users that | ||
| don't need fixing, HTTP 404 if no matching LTI user is found | ||
|
|
||
| Raises | ||
| ------ | ||
| Http404 | ||
| If no LTI user exists for the provided email address | ||
| """ | ||
| user_email = request.data.get("email") | ||
|
|
||
| if not user_email: | ||
| log.error("email is required") | ||
| return HttpResponseBadRequest("email is required") | ||
|
|
||
| # A user that is created by LTI will always have the same username as | ||
| # lti_user_id in LtiUser table. | ||
| with transaction.atomic(): | ||
| lti_user = LtiUser.objects.filter(edx_user__email=user_email).first() | ||
| if not lti_user: | ||
| log.error("No user was found against the given email (%s)", user_email) | ||
| raise Http404 | ||
| if lti_user.lti_user_id != lti_user.edx_user.username: | ||
| log.error( | ||
| "User with email (%s) does not appear to be an LTI-created user", | ||
| user_email, | ||
| ) | ||
| return HttpResponseBadRequest( | ||
| "User with the given email does not appear to be an " | ||
| "LTI-created user." | ||
| ) | ||
|
|
||
| user = lti_user.edx_user | ||
| user.email = user.email.split("@")[0] + "@" + PLACEHOLDER_EMAIL_DOMAIN | ||
| user.save() | ||
| # Remove social auth records for this user | ||
| UserSocialAuth.objects.filter(user=user).delete() | ||
| # Remove the old LTI mapping so that a new one gets created the next time | ||
| # users access edX via LTI | ||
| lti_user.delete() | ||
|
|
||
| # Send the user for retirement and deactivate the account | ||
| try: | ||
| create_retirement_request_and_deactivate_account(user) | ||
| except Exception as e: # noqa: BLE001 | ||
| log.error("Error retiring and deactivating user: %s", e) # noqa: TRY400 | ||
|
|
||
| return Response(status=status.HTTP_200_OK) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| [project] | ||
| name = "ol-openedx-lti-utilities" | ||
| version = "0.1.0" | ||
| description = "An Open edX plugin to add utilities for LTI operations" | ||
| authors = [ | ||
| {name = "MIT Office of Digital Learning"} | ||
| ] | ||
| license = "BSD-3-Clause" | ||
| readme = "README.rst" | ||
| requires-python = ">=3.11" | ||
| dependencies = [ | ||
| "Django>=4.0", | ||
| "djangorestframework>=3.14.0", | ||
| "edx-django-utils>4.0.0", | ||
| "edx-drf-extensions>=10.0.0", | ||
| "edx-opaque-keys", | ||
| ] | ||
|
|
||
| [project.entry-points."lms.djangoapp"] | ||
| ol_openedx_lti_utilities = "ol_openedx_lti_utilities.app:LTIUtilitiesConfig" | ||
|
|
||
| [build-system] | ||
| requires = ["hatchling"] | ||
| build-backend = "hatchling.build" | ||
|
|
||
| [tool.hatch.build.targets.wheel] | ||
| packages = ["ol_openedx_lti_utilities"] | ||
| include = [ | ||
| "ol_openedx_lti_utilities/**/*.py", | ||
| ] | ||
|
|
||
| [tool.hatch.build.targets.sdist] | ||
| include = [ | ||
| "ol_openedx_lti_utilities/**/*", | ||
| "README.rst", | ||
| "pyproject.toml", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| [isort] | ||
| include_trailing_comma = True | ||
| indent = ' ' | ||
| line_length = 120 | ||
| multi_line_output = 3 | ||
| skip= | ||
| migrations | ||
|
|
||
| [wheel] | ||
| universal = 1 | ||
|
|
||
| [tool:pytest] | ||
| pep8maxlinelength = 119 | ||
| DJANGO_SETTINGS_MODULE = lms.envs.test | ||
| addopts = --nomigrations --reuse-db --durations=20 | ||
| # Enable default handling for all warnings, including those that are ignored by default; | ||
| # but hide rate-limit warnings (because we deliberately don't throttle test user logins) | ||
| # and field_data deprecation warnings (because fixing them requires a major low-priority refactoring) | ||
| filterwarnings = | ||
| default | ||
| ignore::xblock.exceptions.FieldDataDeprecationWarning | ||
| ignore::pytest.PytestConfigWarning | ||
| ignore:No request passed to the backend, unable to rate-limit:UserWarning | ||
| ignore:Flags not at the start of the expression:DeprecationWarning | ||
| ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc':DeprecationWarning | ||
| ignore:invalid escape sequence:DeprecationWarning | ||
| ignore:`formatargspec` is deprecated since Python 3.5:DeprecationWarning | ||
| ignore:the imp module is deprecated in favour of importlib:DeprecationWarning | ||
| ignore:"is" with a literal:SyntaxWarning | ||
| ignore:defusedxml.lxml is no longer supported:DeprecationWarning | ||
| ignore: `np.int` is a deprecated alias for the builtin `int`.:DeprecationWarning | ||
| ignore: `np.float` is a deprecated alias for the builtin `float`.:DeprecationWarning | ||
| ignore: `np.complex` is a deprecated alias for the builtin `complex`.:DeprecationWarning | ||
| ignore: 'etree' is deprecated. Use 'xml.etree.ElementTree' instead.:DeprecationWarning | ||
| ignore: defusedxml.cElementTree is deprecated, import from defusedxml.ElementTree instead.:DeprecationWarning | ||
|
|
||
|
|
||
| junit_family = xunit2 | ||
| norecursedirs = .* *.egg build conf dist node_modules test_root cms/envs lms/envs | ||
| python_classes = | ||
| python_files = tests.py test_*.py tests_*.py *_tests.py __init__.py |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| """Pytest config""" | ||
|
|
||
| import json | ||
| import logging | ||
| from pathlib import Path | ||
|
|
||
| import pytest | ||
|
|
||
| BASE_DIR = Path(__file__).parent.absolute() | ||
|
|
||
|
|
||
| def pytest_addoption(parser): | ||
| """Pytest hook that adds command line options""" | ||
| parser.addoption( | ||
| "--disable-logging", | ||
| action="store_true", | ||
| default=False, | ||
| help="Disable all logging during test run", | ||
| ) | ||
| parser.addoption( | ||
| "--error-log-only", | ||
| action="store_true", | ||
| default=False, | ||
| help="Disable all logging output below 'error' level during test run", | ||
| ) | ||
|
|
||
|
|
||
| def pytest_configure(config): | ||
| """Pytest hook that runs after command line options have been parsed""" | ||
| if config.getoption("--disable-logging"): | ||
| logging.disable(logging.CRITICAL) | ||
| elif config.getoption("--error-log-only"): | ||
| logging.disable(logging.WARNING) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def example_event(request): | ||
| """An example real event captured previously""" # noqa: D401 | ||
| with Path.open(BASE_DIR / ".." / "test_data" / "example_event.json") as f: | ||
| request.cls.example_event = json.load(f) | ||
| yield |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we get an error here, shouldn't we respond with an error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I added it as an additional step and I explicitly didn't return error to MITx Online if retirement fails. That's because we would have already updated the email of the old user at this point so MITx Online registration can be retried. Let me know if we should link it to the error response we send to MITx Online.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, we can leave it as is and if we need more information passed back to mitxonline later we can circle back on it.