From 4a5ad34586453274f61379644eb24140ea36d860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Thu, 13 Jun 2024 11:17:26 +0300 Subject: [PATCH] feat: [AXM-644] Add authorization via cms worker for content generation view --- .../contentstore/signals/handlers.py | 5 +++-- cms/djangoapps/contentstore/utils.py | 17 +++++++++++++++++ cms/envs/common.py | 2 ++ cms/envs/production.py | 3 +++ lms/urls.py | 2 +- openedx/features/offline_mode/urls.py | 1 + openedx/features/offline_mode/views.py | 18 ++++++++++++++++-- 7 files changed, 43 insertions(+), 5 deletions(-) diff --git a/cms/djangoapps/contentstore/signals/handlers.py b/cms/djangoapps/contentstore/signals/handlers.py index 08347dd9cd53..d04a28a1d71d 100644 --- a/cms/djangoapps/contentstore/signals/handlers.py +++ b/cms/djangoapps/contentstore/signals/handlers.py @@ -2,7 +2,6 @@ import logging -import requests from datetime import datetime, timezone from functools import wraps from typing import Optional @@ -23,6 +22,7 @@ CoursewareSearchIndexer, LibrarySearchIndexer, ) +from cms.djangoapps.contentstore.utils import get_cms_api_client from common.djangoapps.track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type from common.djangoapps.util.block_utils import yield_dynamic_block_descendants from lms.djangoapps.grades.api import task_compute_all_grades_for_course @@ -160,7 +160,8 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable= transaction.on_commit(lambda: emit_catalog_info_changed_signal(course_key)) if is_offline_mode_enabled(course_key): - requests.post( + client = get_cms_api_client() + client.post( url=urljoin(settings.LMS_ROOT_URL, LMS_OFFLINE_HANDLER_URL), data={'course_id': str(course_key)}, ) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 9ed3f1ce4b36..82269214af69 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -5,6 +5,7 @@ import configparser import logging import re +import requests from collections import defaultdict from contextlib import contextmanager from datetime import datetime, timezone @@ -12,10 +13,12 @@ from uuid import uuid4 from django.conf import settings +from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.urls import reverse from django.utils import translation from django.utils.translation import gettext as _ +from edx_rest_api_client.auth import SuppliedJwtAuth from eventtracking import tracker from help_tokens.core import HelpUrlExpert from lti_consumer.models import CourseAllowPIISharingInLTIFlag @@ -67,6 +70,7 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.site_configuration.models import SiteConfiguration from openedx.core.djangoapps.models.course_details import CourseDetails +from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user from openedx.core.lib.courses import course_image_url from openedx.core.lib.html_to_text import html_to_text from openedx.features.content_type_gating.models import ContentTypeGatingConfig @@ -107,6 +111,7 @@ IMPORTABLE_FILE_TYPES = ('.tar.gz', '.zip') log = logging.getLogger(__name__) +User = get_user_model() def add_instructor(course_key, requesting_user, new_instructor): @@ -2317,3 +2322,15 @@ def get_xblock_render_context(request, block): return str(exc) return "" + + +def get_cms_api_client(): + """ + Returns an API client which can be used to make Exams API requests. + """ + user = User.objects.get(username=settings.EDXAPP_CMS_SERVICE_USER_NAME) + jwt = create_jwt_for_user(user) + client = requests.Session() + client.auth = SuppliedJwtAuth(jwt) + + return client diff --git a/cms/envs/common.py b/cms/envs/common.py index 24a63fb7d99e..8f9187c15de2 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -2525,6 +2525,8 @@ EXAMS_SERVICE_URL = 'http://localhost:18740/api/v1' EXAMS_SERVICE_USERNAME = 'edx_exams_worker' +CMS_SERVICE_USER_NAME = 'edxapp_cms_worker' + FINANCIAL_REPORTS = { 'STORAGE_TYPE': 'localfs', 'BUCKET': None, diff --git a/cms/envs/production.py b/cms/envs/production.py index cf2a7d2f3fad..351804fdd379 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -169,6 +169,9 @@ def get_env_setting(setting): AUTHORING_API_URL = ENV_TOKENS.get('AUTHORING_API_URL', '') # Note that FEATURES['PREVIEW_LMS_BASE'] gets read in from the environment file. +CMS_SERVICE_USER_NAME = ENV_TOKENS.get('CMS_SERVICE_USER_NAME', CMS_SERVICE_USER_NAME) + + OPENAI_API_KEY = ENV_TOKENS.get('OPENAI_API_KEY', '') LEARNER_ENGAGEMENT_PROMPT_FOR_ACTIVE_CONTRACT = ENV_TOKENS.get('LEARNER_ENGAGEMENT_PROMPT_FOR_ACTIVE_CONTRACT', '') LEARNER_ENGAGEMENT_PROMPT_FOR_NON_ACTIVE_CONTRACT = ENV_TOKENS.get( diff --git a/lms/urls.py b/lms/urls.py index 98e02398de38..f382df0184b2 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -1055,5 +1055,5 @@ ] urlpatterns += [ - path('offline_mode/', include('openedx.features.offline_mode.urls')), + path('offline_mode/', include('openedx.features.offline_mode.urls', namespace='offline_mode')), ] diff --git a/openedx/features/offline_mode/urls.py b/openedx/features/offline_mode/urls.py index f5178a424316..d44d0d738f8a 100644 --- a/openedx/features/offline_mode/urls.py +++ b/openedx/features/offline_mode/urls.py @@ -5,6 +5,7 @@ from .views import SudioCoursePublishedEventHandler +app_name = 'offline_mode' urlpatterns = [ path('handle_course_published', SudioCoursePublishedEventHandler.as_view(), name='handle_course_published'), ] diff --git a/openedx/features/offline_mode/views.py b/openedx/features/offline_mode/views.py index 111b69175770..8e6309b202cb 100644 --- a/openedx/features/offline_mode/views.py +++ b/openedx/features/offline_mode/views.py @@ -2,10 +2,15 @@ Views for the offline_mode app. """ from opaque_keys.edx.keys import CourseKey +from opaque_keys import InvalidKeyError +from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication from rest_framework import status +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import IsAdminUser from rest_framework.response import Response from rest_framework.views import APIView +from openedx.core.lib.api.authentication import BearerAuthentication from .tasks import generate_offline_content_for_course from .toggles import is_offline_mode_enabled @@ -18,6 +23,9 @@ class SudioCoursePublishedEventHandler(APIView): and it triggers the generation of offline content. """ + authentication_classes = (JwtAuthentication, BearerAuthentication, SessionAuthentication) + permission_classes = (IsAdminUser,) + def post(self, request, *args, **kwargs): """ Trigger the generation of offline content task. @@ -30,14 +38,20 @@ def post(self, request, *args, **kwargs): Returns: Response: The response object. """ - course_id = request.data.get('course_id') if not course_id: return Response( data={'error': 'course_id is required'}, status=status.HTTP_400_BAD_REQUEST ) - course_key = CourseKey.from_string(course_id) + try: + course_key = CourseKey.from_string(course_id) + except InvalidKeyError: + return Response( + data={'error': 'Invalid course_id'}, + status=status.HTTP_400_BAD_REQUEST + ) + if is_offline_mode_enabled(course_key): generate_offline_content_for_course.apply_async(args=[course_id]) return Response(status=status.HTTP_200_OK)