This reverts commit 3d2617983b.
The change was not quite right because the method six provides needs to
have `self` be passed in as well. See the docs here:
https://six.readthedocs.io/#unittest-assertions
Reverting for now and we can fix it in smaller chunks later.
154 lines
5.3 KiB
Python
154 lines
5.3 KiB
Python
"""
|
|
django-rules and Bridgekeeper rules for courseware related features
|
|
"""
|
|
from __future__ import absolute_import
|
|
|
|
import logging
|
|
import traceback
|
|
|
|
import laboratory
|
|
import rules
|
|
import six
|
|
from bridgekeeper.rules import EMPTY, Rule
|
|
from django.conf import settings
|
|
from django.db.models import Q
|
|
from opaque_keys.edx.django.models import CourseKeyField
|
|
from opaque_keys.edx.keys import CourseKey, UsageKey
|
|
from xblock.core import XBlock
|
|
|
|
from course_modes.models import CourseMode
|
|
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
|
from student.models import CourseAccessRole, CourseEnrollment
|
|
from xmodule.course_module import CourseDescriptor
|
|
from xmodule.error_module import ErrorDescriptor
|
|
from xmodule.x_module import XModule
|
|
|
|
from .access import has_access
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
@rules.predicate
|
|
def is_track_ok_for_exam(user, exam):
|
|
"""
|
|
Returns whether the user is in an appropriate enrollment mode
|
|
"""
|
|
course_id = CourseKey.from_string(exam['course_id'])
|
|
mode, is_active = CourseEnrollment.enrollment_mode_for_user(user, course_id)
|
|
return is_active and mode in (CourseMode.VERIFIED, CourseMode.MASTERS, CourseMode.PROFESSIONAL)
|
|
|
|
|
|
# The edx_proctoring.api uses this permission to gate access to the
|
|
# proctored experience
|
|
can_take_proctored_exam = is_track_ok_for_exam
|
|
rules.set_perm('edx_proctoring.can_take_proctored_exam', is_track_ok_for_exam)
|
|
|
|
|
|
class HasAccessRule(Rule):
|
|
"""
|
|
A rule that calls `has_access` to determine whether it passes
|
|
"""
|
|
def __init__(self, action):
|
|
self.action = action
|
|
|
|
def check(self, user, instance=None):
|
|
return has_access(user, self.action, instance)
|
|
|
|
def query(self, user):
|
|
# Return an always-empty queryset filter so that this always
|
|
# fails permissions, but still passes the is_possible_for check
|
|
# that is used to determine if the rule should allow a user
|
|
# into django admin
|
|
return Q(pk__in=[])
|
|
|
|
|
|
class StaffAccessExperiment(laboratory.Experiment):
|
|
def compare(self, control, candidate):
|
|
return bool(control.value) == candidate.value
|
|
|
|
def publish(self, result):
|
|
if not result.match:
|
|
|
|
LOG.warning(
|
|
u"StaffAccessExperiment: control=%r, candidate=%r\n%s",
|
|
result.control,
|
|
result.candidates[0],
|
|
"".join(traceback.format_stack(limit=10))
|
|
)
|
|
|
|
|
|
class HasStaffAccessToContent(Rule):
|
|
"""
|
|
Check whether a user has `staff` access in a course.
|
|
|
|
Expects to be used to filter a CourseOverview queryset
|
|
"""
|
|
def check(self, user, instance=None):
|
|
"""
|
|
Return True if the supplied user has staff-level access to the supplied content.
|
|
"""
|
|
staff_sql_experiment = StaffAccessExperiment(
|
|
raise_on_mismatch=settings.DEBUG,
|
|
context={'userid': user.id, 'instance': repr(instance)}
|
|
)
|
|
staff_sql_experiment.control(self._check_with_has_access, args=(user, instance))
|
|
staff_sql_experiment.candidate(self._check_with_query, args=(user, instance))
|
|
return staff_sql_experiment.conduct()
|
|
|
|
def _check_with_has_access(self, user, instance=None):
|
|
return has_access(user, 'staff', instance)
|
|
|
|
def _check_with_query(self, user, instance=None):
|
|
"""
|
|
Use the query method to check whether a single user has access to the supplied object.
|
|
"""
|
|
# delegate the work to type-specific functions.
|
|
# (start with more specific types, then get more general)
|
|
if isinstance(instance, (CourseDescriptor, CourseOverview)):
|
|
course_key = instance.id
|
|
elif isinstance(instance, (ErrorDescriptor, XModule, XBlock)):
|
|
course_key = instance.scope_ids.usage_id.course_key
|
|
elif isinstance(instance, CourseKey):
|
|
course_key = instance
|
|
elif isinstance(instance, UsageKey):
|
|
course_key = instance.course_key
|
|
elif isinstance(instance, six.string_types):
|
|
course_key = CourseKey.from_string(instance)
|
|
|
|
return self.filter(user, CourseOverview.objects.filter(id=course_key)).exists()
|
|
|
|
def query(self, user):
|
|
"""
|
|
Returns a Q object that expects to be used to filter CourseOverview queries.
|
|
"""
|
|
if not user.is_authenticated:
|
|
return EMPTY
|
|
|
|
masq_settings = getattr(user, 'masquerade_settings', {})
|
|
masq_as_student = [
|
|
course_key for
|
|
(course_key, masq_setting) in masq_settings.items()
|
|
if masq_setting.role == 'student'
|
|
]
|
|
|
|
not_masquerading_as_student = ~Q(id__in=masq_as_student)
|
|
|
|
is_global_staff = user.is_staff
|
|
course_staff_or_instructor_courses = CourseAccessRole.objects.filter(
|
|
user=user,
|
|
role__in=('staff', 'instructor')
|
|
).exclude(
|
|
course_id=CourseKeyField.Empty,
|
|
).values('course_id')
|
|
org_staff_or_instructor_courses = CourseAccessRole.objects.filter(
|
|
user=user,
|
|
role__in=('staff', 'instructor'),
|
|
course_id=CourseKeyField.Empty,
|
|
org__isnull=False
|
|
).values('org')
|
|
|
|
query = not_masquerading_as_student
|
|
if not is_global_staff:
|
|
query &= Q(id__in=course_staff_or_instructor_courses) | Q(org__in=org_staff_or_instructor_courses)
|
|
return query
|