cms-jwt 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cms_jwt-0.2.0.dist-info/METADATA +5 -0
- cms_jwt-0.2.0.dist-info/RECORD +9 -0
- cms_jwt-0.2.0.dist-info/WHEEL +5 -0
- cms_jwt-0.2.0.dist-info/entry_points.txt +2 -0
- cms_jwt-0.2.0.dist-info/top_level.txt +1 -0
- cms_jwt_import_auth/__init__.py +0 -0
- cms_jwt_import_auth/apps.py +15 -0
- cms_jwt_import_auth/bkp_tutor.py +28 -0
- cms_jwt_import_auth/middleware.py +56 -0
@@ -0,0 +1,9 @@
|
|
1
|
+
cms_jwt_import_auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
cms_jwt_import_auth/apps.py,sha256=MN-6wJb6hogjkdNitK73eJAFJTxeWNeMF1RvOtt2zwU,421
|
3
|
+
cms_jwt_import_auth/bkp_tutor.py,sha256=WG-jmUJri2HqQEB0VNHGr7SW-omj2e47qdrf90V2SVg,1138
|
4
|
+
cms_jwt_import_auth/middleware.py,sha256=sGr0yoTcvzj7fJmZPAxZP7EmeiMIPTLLXGlKb5r4o38,1968
|
5
|
+
cms_jwt-0.2.0.dist-info/METADATA,sha256=o3LiNAipgA--CCtrX58s8L7uD7cDxLCMr8jNMt-iVVY,100
|
6
|
+
cms_jwt-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
7
|
+
cms_jwt-0.2.0.dist-info/entry_points.txt,sha256=dLKqSjEXCuwF40XdxvhDeDDWqp8hKiJoSs2cmJFZ5oU,74
|
8
|
+
cms_jwt-0.2.0.dist-info/top_level.txt,sha256=0EQPRrTo85_1lQvSWaaqeOZC-JSn-Igm_R1Gs5dQ6oI,20
|
9
|
+
cms_jwt-0.2.0.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
cms_jwt_import_auth
|
File without changes
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from django.apps import AppConfig
|
2
|
+
|
3
|
+
class CMSJWTImportAuthConfig(AppConfig):
|
4
|
+
name = "cms_jwt_import_auth"
|
5
|
+
verbose_name = "CMS JWT-or-Session auth for course import"
|
6
|
+
|
7
|
+
plugin_app = {
|
8
|
+
'settings_config': {
|
9
|
+
'cms.djangoapp': {
|
10
|
+
'common': {'relative_path': 'settings.common'},
|
11
|
+
'production': {'relative_path': 'settings.production'},
|
12
|
+
},
|
13
|
+
},
|
14
|
+
}
|
15
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
from tutor import hooks
|
2
|
+
|
3
|
+
def register():
|
4
|
+
# Ensure the Django app is present in the image
|
5
|
+
hooks.Filters.CMS_EXTRA_REQUIREMENTS.add_item("edx-drf-extensions")
|
6
|
+
# If your Django app is published to pip, add it here too:
|
7
|
+
# hooks.Filters.CMS_EXTRA_REQUIREMENTS.add_item("cms-jwt-import-auth>=0.1.0")
|
8
|
+
|
9
|
+
# Load the Django app + middleware
|
10
|
+
hooks.Filters.CMS_EXTRA_APPS.add_item("cms_jwt_import_auth")
|
11
|
+
hooks.Filters.CMS_EXTRA_MIDDLEWARE.add_item(
|
12
|
+
"cms_jwt_import_auth.middleware.ImportJWTOrSessionMiddleware"
|
13
|
+
)
|
14
|
+
|
15
|
+
# Optional settings patch
|
16
|
+
def _patch(settings):
|
17
|
+
settings.setdefault("REST_FRAMEWORK", {})
|
18
|
+
settings["REST_FRAMEWORK"]["DEFAULT_AUTHENTICATION_CLASSES"] = (
|
19
|
+
"edx_rest_framework_extensions.auth.jwt.authentication.JwtAuthentication",
|
20
|
+
"rest_framework.authentication.SessionAuthentication",
|
21
|
+
)
|
22
|
+
settings.setdefault("JWT_AUTH", {})
|
23
|
+
settings["JWT_AUTH"].update({
|
24
|
+
"JWT_ISSUER": "https://campus-dev.nextere.com/oauth2",
|
25
|
+
"JWT_AUDIENCE": "openedx",
|
26
|
+
})
|
27
|
+
hooks.Filters.CMS_EXTRA_SETTINGS.add_item(_patch)
|
28
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
import re
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
from django.http import JsonResponse
|
6
|
+
from django.utils.deprecation import MiddlewareMixin
|
7
|
+
from django.contrib.auth.models import AnonymousUser
|
8
|
+
|
9
|
+
from rest_framework.request import Request as DRFRequest
|
10
|
+
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
11
|
+
|
12
|
+
class ImportJWTOrSessionMiddleware(MiddlewareMixin):
|
13
|
+
"""
|
14
|
+
Enforce authentication for the course import endpoint in CMS:
|
15
|
+
/api/courses/v1/<course_id>/import/
|
16
|
+
Accept either:
|
17
|
+
- existing logged-in Studio session, OR
|
18
|
+
- JWT in Authorization header (JWT or Bearer)
|
19
|
+
If neither present/valid -> 401 JSON.
|
20
|
+
|
21
|
+
NOTE:
|
22
|
+
- Keep this middleware scoped to just the import route to avoid overhead.
|
23
|
+
"""
|
24
|
+
|
25
|
+
# Path matcher (strict, trailing slash optional)
|
26
|
+
# Example: /api/courses/v1/course-v1:ACME+ONB101+2025_T1/import/
|
27
|
+
PATTERN = re.compile(
|
28
|
+
r"^/api/courses/v1/[^/]+/import/?$"
|
29
|
+
)
|
30
|
+
|
31
|
+
def process_request(self, request):
|
32
|
+
path = request.path or ""
|
33
|
+
if not self.PATTERN.match(path):
|
34
|
+
return None # ignore all other routes
|
35
|
+
|
36
|
+
# If session-authenticated, allow through
|
37
|
+
user = getattr(request, "user", None)
|
38
|
+
if user is not None and user.is_authenticated:
|
39
|
+
return None
|
40
|
+
|
41
|
+
# Try JWT auth (accept JWT or Bearer header schemes)
|
42
|
+
auth = JwtAuthentication()
|
43
|
+
drf_request = DRFRequest(request)
|
44
|
+
try:
|
45
|
+
user_auth_tuple: Optional[tuple] = auth.authenticate(drf_request)
|
46
|
+
except Exception as e:
|
47
|
+
# Invalid token format / signature, return 401
|
48
|
+
return JsonResponse({"detail": f"Invalid JWT: {str(e)}"}, status=401)
|
49
|
+
|
50
|
+
if user_auth_tuple:
|
51
|
+
request.user, _ = user_auth_tuple
|
52
|
+
return None # authenticated via JWT
|
53
|
+
|
54
|
+
# Neither session nor JWT
|
55
|
+
return JsonResponse({"detail": "Authentication required"}, status=401)
|
56
|
+
|