Add a staff-access specific bridgekeeper rule with full query support
This commit is contained in:
@@ -3,7 +3,10 @@ Permission definitions for the courseware djangoapp
|
||||
"""
|
||||
|
||||
from bridgekeeper import perms
|
||||
from .rules import HasAccessRule
|
||||
from .rules import HasAccessRule, HasStaffAccessToContent
|
||||
|
||||
VIEW_COURSE_HOME = 'courseware.view_course_home'
|
||||
MASQUERADE_AS_STUDENT = 'courseware.masquerade_as_student'
|
||||
|
||||
perms[VIEW_COURSE_HOME] = HasAccessRule('load')
|
||||
perms[MASQUERADE_AS_STUDENT] = HasStaffAccessToContent()
|
||||
|
||||
@@ -3,15 +3,26 @@ django-rules and Bridgekeeper rules for courseware related features
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from bridgekeeper.rules import Rule
|
||||
import logging
|
||||
|
||||
import laboratory
|
||||
import rules
|
||||
from bridgekeeper.rules import Rule, EMPTY
|
||||
from course_modes.models import CourseMode
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from student.models import CourseEnrollment
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from student.models import CourseEnrollment, CourseAccessRole
|
||||
from xblock.core import XBlock
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
|
||||
from .access import has_access
|
||||
|
||||
import rules
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@rules.predicate
|
||||
@@ -46,3 +57,92 @@ class HasAccessRule(Rule):
|
||||
# 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) == candidate
|
||||
|
||||
def publish(self, result):
|
||||
if not result.match:
|
||||
LOG.warning(
|
||||
u"StaffAccessExperiment: control=%r, candidate=%r",
|
||||
result.control,
|
||||
result.candidates[0],
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
|
||||
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, basestring):
|
||||
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'),
|
||||
course_id__isnull=False
|
||||
).values('course_id')
|
||||
org_staff_or_instructor_courses = CourseAccessRole.objects.filter(
|
||||
user=user,
|
||||
role__in=('staff', 'instructor'),
|
||||
course_key__isnull=True,
|
||||
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
|
||||
|
||||
@@ -66,8 +66,11 @@ from ..entrance_exams import (
|
||||
from ..masquerade import check_content_start_date_for_masquerade_user, setup_masquerade
|
||||
from ..model_data import FieldDataCache
|
||||
from ..module_render import get_module_for_descriptor, toc_for_course
|
||||
from ..permissions import MASQUERADE_AS_STUDENT
|
||||
|
||||
from .views import CourseTabView
|
||||
|
||||
|
||||
log = logging.getLogger("edx.courseware.views.index")
|
||||
|
||||
TEMPLATE_IMPORTS = {'urllib': urllib}
|
||||
@@ -152,6 +155,7 @@ class CoursewareIndex(View):
|
||||
# If the user is considered enrolled show the default XBlock student_view.
|
||||
pass
|
||||
|
||||
self.can_masquerade = request.user.has_perm(MASQUERADE_AS_STUDENT, self.course)
|
||||
self.is_staff = has_access(request.user, 'staff', self.course)
|
||||
self._setup_masquerade_for_effective_user()
|
||||
|
||||
@@ -168,7 +172,7 @@ class CoursewareIndex(View):
|
||||
self.masquerade, self.effective_user = setup_masquerade(
|
||||
self.request,
|
||||
self.course_key,
|
||||
self.is_staff,
|
||||
self.can_masquerade,
|
||||
reset_masquerade_data=True
|
||||
)
|
||||
# Set the user in the request to the effective user.
|
||||
@@ -415,6 +419,7 @@ class CoursewareIndex(View):
|
||||
'init': '',
|
||||
'fragment': Fragment(),
|
||||
'staff_access': self.is_staff,
|
||||
'can_masquerade': self.can_masquerade,
|
||||
'masquerade': self.masquerade,
|
||||
'supports_preview_menu': True,
|
||||
'studio_url': get_studio_url(self.course, 'course'),
|
||||
|
||||
@@ -63,7 +63,7 @@ from courseware.courses import (
|
||||
from courseware.masquerade import setup_masquerade
|
||||
from courseware.model_data import FieldDataCache
|
||||
from courseware.models import BaseStudentModuleHistory, StudentModule
|
||||
from courseware.permissions import VIEW_COURSE_HOME
|
||||
from courseware.permissions import VIEW_COURSE_HOME, MASQUERADE_AS_STUDENT
|
||||
from courseware.url_helpers import get_redirect_url
|
||||
from courseware.user_state_client import DjangoXBlockUserStateClient
|
||||
from edxmako.shortcuts import marketing_link, render_to_response, render_to_string
|
||||
@@ -355,8 +355,8 @@ def course_info(request, course_id):
|
||||
with modulestore().bulk_operations(course_key):
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
|
||||
staff_access = has_access(request.user, 'staff', course)
|
||||
masquerade, user = setup_masquerade(request, course_key, staff_access, reset_masquerade_data=True)
|
||||
can_masquerade = request.user.has_perm(MASQUERADE_AS_STUDENT, course)
|
||||
masquerade, user = setup_masquerade(request, course_key, can_masquerade, reset_masquerade_data=True)
|
||||
|
||||
# LEARNER-612: CCX redirect handled by new Course Home (DONE)
|
||||
# LEARNER-1697: Transition banner messages to new Course Home (DONE)
|
||||
@@ -432,7 +432,7 @@ def course_info(request, course_id):
|
||||
'course_subtitle': course_subtitle,
|
||||
'show_subtitle': course_homepage_show_subtitle,
|
||||
'show_org': course_homepage_show_org,
|
||||
'staff_access': staff_access,
|
||||
'can_masquerade': can_masquerade,
|
||||
'masquerade': masquerade,
|
||||
'supports_preview_menu': True,
|
||||
'studio_url': get_studio_url(course, 'course_info'),
|
||||
@@ -645,11 +645,16 @@ class CourseTabView(EdxFragmentView):
|
||||
"""
|
||||
Creates the context for the fragment's template.
|
||||
"""
|
||||
staff_access = has_access(request.user, 'staff', course)
|
||||
can_masquerade = request.user.has_perm(MASQUERADE_AS_STUDENT, course)
|
||||
supports_preview_menu = tab.get('supports_preview_menu', False)
|
||||
uses_bootstrap = self.uses_bootstrap(request, course, tab=tab)
|
||||
if supports_preview_menu:
|
||||
masquerade, masquerade_user = setup_masquerade(request, course.id, staff_access, reset_masquerade_data=True)
|
||||
masquerade, masquerade_user = setup_masquerade(
|
||||
request,
|
||||
course.id,
|
||||
can_masquerade,
|
||||
reset_masquerade_data=True,
|
||||
)
|
||||
request.user = masquerade_user
|
||||
else:
|
||||
masquerade = None
|
||||
@@ -658,7 +663,7 @@ class CourseTabView(EdxFragmentView):
|
||||
'course': course,
|
||||
'tab': tab,
|
||||
'active_page': tab.get('type', None),
|
||||
'staff_access': staff_access,
|
||||
'can_masquerade': can_masquerade,
|
||||
'masquerade': masquerade,
|
||||
'supports_preview_menu': supports_preview_menu,
|
||||
'uses_bootstrap': uses_bootstrap,
|
||||
@@ -988,11 +993,12 @@ def _progress(request, course_key, student_id):
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
|
||||
staff_access = bool(has_access(request.user, 'staff', course))
|
||||
can_masquerade = request.user.has_perm(MASQUERADE_AS_STUDENT, course)
|
||||
|
||||
masquerade = None
|
||||
if student_id is None or student_id == request.user.id:
|
||||
# This will be a no-op for non-staff users, returning request.user
|
||||
masquerade, student = setup_masquerade(request, course_key, staff_access, reset_masquerade_data=True)
|
||||
masquerade, student = setup_masquerade(request, course_key, can_masquerade, reset_masquerade_data=True)
|
||||
else:
|
||||
try:
|
||||
coach_access = has_ccx_coach_role(request.user, course_key)
|
||||
@@ -1035,6 +1041,7 @@ def _progress(request, course_key, student_id):
|
||||
'courseware_summary': courseware_summary,
|
||||
'studio_url': studio_url,
|
||||
'grade_summary': course_grade.summary,
|
||||
'can_masquerade': can_masquerade,
|
||||
'staff_access': staff_access,
|
||||
'masquerade': masquerade,
|
||||
'supports_preview_menu': True,
|
||||
|
||||
@@ -11,7 +11,7 @@ from xmodule.partitions.partitions_service import get_all_partitions_for_course
|
||||
%>
|
||||
|
||||
<%
|
||||
show_preview_menu = course and staff_access and supports_preview_menu
|
||||
show_preview_menu = course and can_masquerade and supports_preview_menu
|
||||
%>
|
||||
|
||||
% if show_preview_menu:
|
||||
|
||||
@@ -100,6 +100,7 @@ help-tokens
|
||||
html5lib # HTML parser, used for capa problems
|
||||
ipaddress # Ip network support for Embargo feature
|
||||
jsonfield # Django model field for validated JSON; used in several apps
|
||||
laboratory # Library for testing that code refactors/infrastructure changes produce identical results
|
||||
mailsnake # Needed for mailchimp (mailing djangoapp)
|
||||
mako==1.0.2 # Primary template language used for server-side page rendering
|
||||
Markdown # Convert text markup to HTML; used in capa problems, forums, and course wikis
|
||||
|
||||
@@ -147,6 +147,7 @@ jmespath==0.9.4 # via boto3, botocore
|
||||
jsondiff==1.2.0 # via edx-enterprise
|
||||
jsonfield==2.0.2
|
||||
kombu==3.0.37 # via celery
|
||||
laboratory==1.0.2
|
||||
lazy==1.1
|
||||
lepl==5.1.3 # via rfc6266-parser
|
||||
libsass==0.10.0
|
||||
|
||||
@@ -188,6 +188,7 @@ jmespath==0.9.4
|
||||
jsondiff==1.2.0
|
||||
jsonfield==2.0.2
|
||||
kombu==3.0.37
|
||||
laboratory==1.0.2
|
||||
lazy-object-proxy==1.4.1
|
||||
lazy==1.1
|
||||
lepl==5.1.3
|
||||
|
||||
@@ -182,6 +182,7 @@ jmespath==0.9.4
|
||||
jsondiff==1.2.0
|
||||
jsonfield==2.0.2
|
||||
kombu==3.0.37
|
||||
laboratory==1.0.2
|
||||
lazy-object-proxy==1.4.1 # via astroid
|
||||
lazy==1.1
|
||||
lepl==5.1.3
|
||||
|
||||
Reference in New Issue
Block a user