diff --git a/lms/djangoapps/bulk_enroll/views.py b/lms/djangoapps/bulk_enroll/views.py index f5a1bef36b..a62d7302f9 100644 --- a/lms/djangoapps/bulk_enroll/views.py +++ b/lms/djangoapps/bulk_enroll/views.py @@ -14,7 +14,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from bulk_enroll.serializers import BulkEnrollmentSerializer -from instructor.views.api import students_update_enrollment +from lms.djangoapps.instructor.views.api import students_update_enrollment from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort, get_cohort_by_name from openedx.core.djangoapps.course_groups.models import CourseUserGroup from openedx.core.djangoapps.enrollments.views import EnrollmentUserThrottle diff --git a/lms/djangoapps/ccx/migrations/0005_change_ccx_coach_to_staff.py b/lms/djangoapps/ccx/migrations/0005_change_ccx_coach_to_staff.py index 66191f9145..a42d0f0119 100644 --- a/lms/djangoapps/ccx/migrations/0005_change_ccx_coach_to_staff.py +++ b/lms/djangoapps/ccx/migrations/0005_change_ccx_coach_to_staff.py @@ -10,7 +10,7 @@ from django.db import migrations from django.http import Http404 from courseware.courses import get_course_by_id -from instructor.access import allow_access, revoke_access +from lms.djangoapps.instructor.access import allow_access, revoke_access log = logging.getLogger("edx.ccx") diff --git a/lms/djangoapps/class_dashboard/dashboard_data.py b/lms/djangoapps/class_dashboard/dashboard_data.py index f39642aed7..c4a04d54c4 100644 --- a/lms/djangoapps/class_dashboard/dashboard_data.py +++ b/lms/djangoapps/class_dashboard/dashboard_data.py @@ -11,7 +11,7 @@ from opaque_keys.edx.locator import BlockUsageLocator from six import text_type from courseware import models -from instructor_analytics.csvs import create_csv_response +from lms.djangoapps.instructor_analytics.csvs import create_csv_response from util.json_request import JsonResponse from xmodule.modulestore.django import modulestore from xmodule.modulestore.inheritance import own_metadata diff --git a/lms/djangoapps/discussion/rest_api/views.py b/lms/djangoapps/discussion/rest_api/views.py index af2179e9a3..e534872c0b 100644 --- a/lms/djangoapps/discussion/rest_api/views.py +++ b/lms/djangoapps/discussion/rest_api/views.py @@ -19,7 +19,7 @@ from rest_framework.viewsets import ViewSet from six import text_type from discussion.views import get_divided_discussions -from instructor.access import update_forum_role +from lms.djangoapps.instructor.access import update_forum_role from lms.djangoapps.discussion.django_comment_client.utils import available_division_schemes from lms.djangoapps.discussion.rest_api.api import ( create_comment, diff --git a/lms/djangoapps/instructor/permissions.py b/lms/djangoapps/instructor/permissions.py new file mode 100644 index 0000000000..d47a4b28f1 --- /dev/null +++ b/lms/djangoapps/instructor/permissions.py @@ -0,0 +1,30 @@ +""" +Permissions for the instructor dashboard and associated actions +""" + +from bridgekeeper import perms +from bridgekeeper.rules import is_staff +from courseware.rules import HasAccessRule + +ALLOW_STUDENT_TO_BYPASS_ENTRANCE_EXAM = 'instructor.allow_student_to_bypass_entrance_exam' +ASSIGN_TO_COHORTS = 'instructor.assign_to_cohorts' +EDIT_COURSE_ACCESS = 'instructor.edit_course_access' +EDIT_FORUM_ROLES = 'instructor.edit_forum_roles' +EDIT_INVOICE_VALIDATION = 'instructor.edit_invoice_validation' +ENABLE_CERTIFICATE_GENERATION = 'instructor.enable_certificate_generation' +GENERATE_CERTIFICATE_EXCEPTIONS = 'instructor.generate_certificate_exceptions' +GENERATE_BULK_CERTIFICATE_EXCEPTIONS = 'instructor.generate_bulk_certificate_exceptions' +GIVE_STUDENT_EXTENSION = 'instructor.give_student_extension' +VIEW_ISSUED_CERTIFICATES = 'instructor.view_issued_certificates' + + +perms[ALLOW_STUDENT_TO_BYPASS_ENTRANCE_EXAM] = HasAccessRule('staff') +perms[ASSIGN_TO_COHORTS] = HasAccessRule('staff') +perms[EDIT_COURSE_ACCESS] = HasAccessRule('instructor') +perms[EDIT_FORUM_ROLES] = HasAccessRule('staff') +perms[EDIT_INVOICE_VALIDATION] = HasAccessRule('staff') +perms[ENABLE_CERTIFICATE_GENERATION] = is_staff +perms[GENERATE_CERTIFICATE_EXCEPTIONS] = is_staff +perms[GENERATE_BULK_CERTIFICATE_EXCEPTIONS] = is_staff +perms[GIVE_STUDENT_EXTENSION] = HasAccessRule('staff') +perms[VIEW_ISSUED_CERTIFICATES] = HasAccessRule('staff') diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 8e7163f1ed..60afef59e7 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -148,6 +148,20 @@ from .tools import ( strip_if_string ) +from ..permissions import ( + ALLOW_STUDENT_TO_BYPASS_ENTRANCE_EXAM, + ASSIGN_TO_COHORTS, + EDIT_COURSE_ACCESS, + EDIT_FORUM_ROLES, + EDIT_INVOICE_VALIDATION, + ENABLE_CERTIFICATE_GENERATION, + GENERATE_CERTIFICATE_EXCEPTIONS, + GENERATE_BULK_CERTIFICATE_EXCEPTIONS, + GIVE_STUDENT_EXTENSION, + VIEW_ISSUED_CERTIFICATES, +) + + log = logging.getLogger(__name__) TASK_SUBMISSION_OK = 'created' @@ -249,6 +263,28 @@ def require_level(level): return decorator +def require_course_permission(permission): + """ + Decorator with argument that requires a specific permission of the requesting + user. If the requirement is not satisfied, returns an + HttpResponseForbidden (403). + + Assumes that request is in args[0]. + Assumes that course_id is in kwargs['course_id']. + """ + def decorator(func): # pylint: disable=missing-docstring + def wrapped(*args, **kwargs): + request = args[0] + course = get_course_by_id(CourseKey.from_string(kwargs['course_id'])) + + if request.user.has_perm(permission, course): + return func(*args, **kwargs) + else: + return HttpResponseForbidden() + return wrapped + return decorator + + def require_sales_admin(func): """ Decorator for checking sales administrator access before executing an HTTP endpoint. This decorator @@ -877,7 +913,7 @@ def bulk_beta_modify_access(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('instructor') +@require_course_permission(EDIT_COURSE_ACCESS) @require_post_params( unique_student_identifier="email or username of user to change access", rolename="'instructor', 'staff', 'beta', or 'ccx_coach'", @@ -1143,7 +1179,7 @@ def get_sale_order_records(request, course_id): # pylint: disable=unused-argume return instructor_analytics.csvs.create_csv_response("e-commerce_sale_order_records.csv", csv_columns, datarows) -@require_level('staff') +@require_course_permission(EDIT_INVOICE_VALIDATION) @require_POST def sale_validation(request, course_id): """ @@ -1210,7 +1246,7 @@ def re_validate_invoice(obj_invoice): @transaction.non_atomic_requests @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(VIEW_ISSUED_CERTIFICATES) def get_issued_certificates(request, course_id): """ Responds with JSON if CSV is not required. contains a list of issued certificates. @@ -1388,7 +1424,7 @@ def _cohorts_csv_validator(file_storage, file_to_validate): @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) @require_POST -@require_level('staff') +@require_course_permission(ASSIGN_TO_COHORTS) @common_exceptions_400 def add_users_to_cohorts(request, course_id): """ @@ -2758,7 +2794,7 @@ def send_email(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(EDIT_FORUM_ROLES) @require_post_params( unique_student_identifier="email or username of user to change access", rolename="the forum role", @@ -2851,7 +2887,7 @@ def _display_unit(unit): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(GIVE_STUDENT_EXTENSION) @require_post_params('student', 'url', 'due_datetime') def change_due_date(request, course_id): """ @@ -2875,7 +2911,7 @@ def change_due_date(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(GIVE_STUDENT_EXTENSION) @require_post_params('student', 'url') def reset_due_date(request, course_id): """ @@ -2986,7 +3022,7 @@ def generate_example_certificates(request, course_id=None): # pylint: disable=u return redirect(_instructor_dash_url(course_key, section='certificates')) -@require_global_staff +@require_course_permission(ENABLE_CERTIFICATE_GENERATION) @require_POST def enable_certificate_generation(request, course_id=None): """Enable/disable self-generated certificates for a course. @@ -3006,7 +3042,7 @@ def enable_certificate_generation(request, course_id=None): @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(ALLOW_STUDENT_TO_BYPASS_ENTRANCE_EXAM) @require_POST def mark_student_can_skip_entrance_exam(request, course_id): """ @@ -3270,7 +3306,7 @@ def get_student(username_or_email, course_key): @transaction.non_atomic_requests @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_global_staff +@require_course_permission(GENERATE_CERTIFICATE_EXCEPTIONS) @require_POST @common_exceptions_400 def generate_certificate_exceptions(request, course_id, generate_for=None): @@ -3312,7 +3348,7 @@ def generate_certificate_exceptions(request, course_id, generate_for=None): @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_global_staff +@require_course_permission(GENERATE_BULK_CERTIFICATE_EXCEPTIONS) @require_POST def generate_bulk_certificate_exceptions(request, course_id): """ diff --git a/lms/djangoapps/instructor_analytics/tests/test_basic.py b/lms/djangoapps/instructor_analytics/tests/test_basic.py index c2118deae7..e9e3bc3268 100644 --- a/lms/djangoapps/instructor_analytics/tests/test_basic.py +++ b/lms/djangoapps/instructor_analytics/tests/test_basic.py @@ -20,7 +20,7 @@ from six.moves import range, zip from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory from courseware.tests.factories import InstructorFactory -from instructor_analytics.basic import ( +from lms.djangoapps.instructor_analytics.basic import ( AVAILABLE_FEATURES, PROFILE_FEATURES, STUDENT_FEATURES, diff --git a/lms/djangoapps/instructor_analytics/tests/test_csvs.py b/lms/djangoapps/instructor_analytics/tests/test_csvs.py index c943619fec..e496f6964c 100644 --- a/lms/djangoapps/instructor_analytics/tests/test_csvs.py +++ b/lms/djangoapps/instructor_analytics/tests/test_csvs.py @@ -6,7 +6,7 @@ import pytest from django.test import TestCase from six.moves import range -from instructor_analytics.csvs import create_csv_response, format_dictlist, format_instances +from lms.djangoapps.instructor_analytics.csvs import create_csv_response, format_dictlist, format_instances class TestAnalyticsCSVS(TestCase): diff --git a/lms/djangoapps/instructor_analytics/tests/test_distributions.py b/lms/djangoapps/instructor_analytics/tests/test_distributions.py index ad3a829642..1ca18523cc 100644 --- a/lms/djangoapps/instructor_analytics/tests/test_distributions.py +++ b/lms/djangoapps/instructor_analytics/tests/test_distributions.py @@ -6,7 +6,7 @@ from django.test import TestCase from opaque_keys.edx.locator import CourseLocator from six.moves import range -from instructor_analytics.distributions import AVAILABLE_PROFILE_FEATURES, profile_distribution +from lms.djangoapps.instructor_analytics.distributions import AVAILABLE_PROFILE_FEATURES, profile_distribution from student.models import CourseEnrollment from student.tests.factories import UserFactory diff --git a/lms/djangoapps/instructor_task/tasks_helper/enrollments.py b/lms/djangoapps/instructor_task/tasks_helper/enrollments.py index 4843cbc3ff..ba745c4caa 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/enrollments.py +++ b/lms/djangoapps/instructor_task/tasks_helper/enrollments.py @@ -14,8 +14,8 @@ from pytz import UTC from courseware.courses import get_course_by_id from edxmako.shortcuts import render_to_string -from instructor_analytics.basic import enrolled_students_features, list_may_enroll -from instructor_analytics.csvs import format_dictlist +from lms.djangoapps.instructor_analytics.basic import enrolled_students_features, list_may_enroll +from lms.djangoapps.instructor_analytics.csvs import format_dictlist from lms.djangoapps.instructor.paidcourse_enrollment_report import PaidCourseEnrollmentReportProvider from lms.djangoapps.instructor_task.models import ReportStore from shoppingcart.models import ( diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py index c97887b065..5ca6f833f2 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/grades.py +++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py @@ -22,8 +22,8 @@ from six.moves import zip, zip_longest from course_blocks.api import get_course_blocks from courseware.courses import get_course_by_id from courseware.user_state_client import DjangoXBlockUserStateClient -from instructor_analytics.basic import list_problem_responses -from instructor_analytics.csvs import format_dictlist +from lms.djangoapps.instructor_analytics.basic import list_problem_responses +from lms.djangoapps.instructor_analytics.csvs import format_dictlist from lms.djangoapps.certificates.models import CertificateWhitelist, GeneratedCertificate, certificate_info_for_user from lms.djangoapps.grades.api import CourseGradeFactory from lms.djangoapps.grades.api import context as grades_context diff --git a/lms/djangoapps/instructor_task/tasks_helper/misc.py b/lms/djangoapps/instructor_task/tasks_helper/misc.py index f505db4944..f836b34c92 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/misc.py +++ b/lms/djangoapps/instructor_task/tasks_helper/misc.py @@ -18,8 +18,8 @@ from django.core.files.storage import DefaultStorage from openassessment.data import OraAggregateData from pytz import UTC -from instructor_analytics.basic import get_proctored_exam_results -from instructor_analytics.csvs import format_dictlist +from lms.djangoapps.instructor_analytics.basic import get_proctored_exam_results +from lms.djangoapps.instructor_analytics.csvs import format_dictlist from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort from openedx.core.djangoapps.course_groups.models import CourseUserGroup from survey.models import SurveyAnswer diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py index bf4c760f51..1bf308fda9 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py @@ -33,7 +33,7 @@ from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory from courseware.tests.factories import InstructorFactory -from instructor_analytics.basic import UNAVAILABLE, list_problem_responses +from lms.djangoapps.instructor_analytics.basic import UNAVAILABLE, list_problem_responses from lms.djangoapps.certificates.models import CertificateStatuses, GeneratedCertificate from lms.djangoapps.certificates.tests.factories import CertificateWhitelistFactory, GeneratedCertificateFactory from lms.djangoapps.grades.models import PersistentCourseGrade