access.py 29.3 KB
Newer Older
1 2
"""
This file contains (or should), all access control logic for the courseware.
3
Ideally, it will be the only place that needs to know about any special settings
4 5 6 7 8 9 10 11
like DISABLE_START_DATES.

Note: The access control logic in this file does NOT check for enrollment in
  a course.  It is expected that higher layers check for enrollment so we
  don't have to hit the enrollments table on every module load.

  If enrollment is to be checked, use get_course_with_access in courseware.courses.
  It is a wrapper around has_access that additionally checks for enrollment.
12
"""
13
from datetime import datetime
14
import logging
15
import pytz
16 17

from django.conf import settings
Ned Batchelder committed
18
from django.contrib.auth.models import AnonymousUser
19 20 21
from django.utils.timezone import UTC

from opaque_keys.edx.keys import CourseKey, UsageKey
22

23
from xblock.core import XBlock
24

25
from xmodule.course_module import (
26 27 28 29
    CourseDescriptor,
    CATALOG_VISIBILITY_CATALOG_AND_ABOUT,
    CATALOG_VISIBILITY_ABOUT,
)
30
from xmodule.error_module import ErrorDescriptor
31
from xmodule.x_module import XModule, DEPRECATION_VSCOMPAT_EVENT
32
from xmodule.split_test_module import get_split_user_partitions
33
from xmodule.partitions.partitions import NoSuchUserPartitionError, NoSuchUserPartitionGroupError
34

35
from external_auth.models import ExternalAuthMap
36
from courseware.masquerade import get_masquerade_role, is_masquerading_as_student
37
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
38
from student import auth
39
from student.models import CourseEnrollmentAllowed
40
from student.roles import (
41
    CourseBetaTesterRole,
42
    CourseCcxCoachRole,
43 44 45
    CourseInstructorRole,
    CourseStaffRole,
    GlobalStaff,
46
    SupportStaffRole,
47 48
    OrgInstructorRole,
    OrgStaffRole,
49
)
50 51 52
from util.milestones_helpers import (
    get_pre_requisite_courses_not_completed,
    any_unfulfilled_milestones,
53
    is_prerequisite_courses_enabled,
54
)
cewing committed
55
from ccx_keys.locator import CCXLocator
56

57 58
import dogstats_wrapper as dog_stats_api

59 60 61 62 63
from courseware.access_response import (
    MilestoneError,
    MobileAvailabilityError,
    VisibilityError,
)
64
from courseware.access_utils import adjust_start_date, check_start_date, debug, ACCESS_GRANTED, ACCESS_DENIED
65

66 67 68
from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException
from lms.djangoapps.ccx.models import CustomCourseForEdX

69 70
log = logging.getLogger(__name__)

71

72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
def has_ccx_coach_role(user, course_key):
    """
    Check if user is a coach on this ccx.

    Arguments:
        user (User): the user whose descriptor access we are checking.
        course_key (CCXLocator): Key to CCX.

    Returns:
        bool: whether user is a coach on this ccx or not.
    """
    if hasattr(course_key, 'ccx'):
        ccx_id = course_key.ccx
        role = CourseCcxCoachRole(course_key)

        if role.has_user(user):
            list_ccx = CustomCourseForEdX.objects.filter(
                course_id=course_key.to_course_locator(),
                coach=user
            )
            if list_ccx.exists():
                coach_ccx = list_ccx[0]
                return str(coach_ccx.id) == ccx_id
    else:
        raise CCXLocatorValidationException("Invalid CCX key. To verify that "
                                            "user is a coach on CCX, you must provide key to CCX")
    return False


101
def has_access(user, action, obj, course_key=None):
102 103 104 105 106 107
    """
    Check whether a user has the access to do action on obj.  Handles any magic
    switching based on various settings.

    Things this module understands:
    - start dates for modules
108
    - visible_to_staff_only for modules
109
    - DISABLE_START_DATES
110
    - different access for instructor, staff, course staff, and students.
111
    - mobile_available flag for course modules
112

113 114
    user: a Django user object. May be anonymous. If none is passed,
                    anonymous is assumed
115

116 117
    obj: The object to check access for.  A module, descriptor, location, or
                    certain special strings (e.g. 'global')
118 119 120 121 122 123

    action: A string specifying the action that the client is trying to perform.

    actions depend on the obj type, but include e.g. 'enroll' for courses.  See the
    type-specific functions below for the known actions for that type.

124
    course_key: A course_key specifying which course run this access is for.
125 126 127
        Required when accessing anything other than a CourseDescriptor, 'global',
        or a location with category 'course'

128 129
    Returns an AccessResponse object.  It is up to the caller to actually
    deny access in a way that makes sense in context.
130
    """
131 132 133 134
    # Just in case user is passed in as None, make them anonymous
    if not user:
        user = AnonymousUser()

cewing committed
135 136 137
    if isinstance(course_key, CCXLocator):
        course_key = course_key.to_course_locator()

138 139 140
    # delegate the work to type-specific functions.
    # (start with more specific types, then get more general)
    if isinstance(obj, CourseDescriptor):
141
        return _has_access_course(user, action, obj)
142

143
    if isinstance(obj, CourseOverview):
144
        return _has_access_course(user, action, obj)
145

146
    if isinstance(obj, ErrorDescriptor):
147
        return _has_access_error_desc(user, action, obj, course_key)
148

149
    if isinstance(obj, XModule):
150
        return _has_access_xmodule(user, action, obj, course_key)
151

152 153
    # NOTE: any descriptor access checkers need to go above this
    if isinstance(obj, XBlock):
154
        return _has_access_descriptor(user, action, obj, course_key)
155

156 157 158
    if isinstance(obj, CCXLocator):
        return _has_access_ccx_key(user, action, obj)

159 160 161
    if isinstance(obj, CourseKey):
        return _has_access_course_key(user, action, obj)

162
    if isinstance(obj, UsageKey):
163
        return _has_access_location(user, action, obj, course_key)
164

165
    if isinstance(obj, basestring):
stv committed
166
        return _has_access_string(user, action, obj)
167

168 169
    # Passing an unknown object here is a coding error, so rather than
    # returning a default, complain.
170
    raise TypeError("Unknown object type in has_access(): '{0}'"
171 172
                    .format(type(obj)))

173 174

# ================ Implementation helpers ================================
175 176 177 178 179 180 181 182 183 184 185 186 187
def _can_access_descriptor_with_start_date(user, descriptor, course_key):  # pylint: disable=invalid-name
    """
    Checks if a user has access to a descriptor based on its start date.

    If there is no start date specified, grant access.
    Else, check if we're past the start date.

    Note:
        We do NOT check whether the user is staff or if the descriptor
        is detached... it is assumed both of these are checked by the caller.

    Arguments:
        user (User): the user whose descriptor access we are checking.
188 189 190 191 192 193 194 195
        descriptor (AType): the descriptor for which we are checking access,
            where AType is CourseDescriptor, CourseOverview, or any other class
            that represents a descriptor and has the attributes .location, .id,
            .start, and .days_early_for_beta.

    Returns:
        AccessResponse: The result of this access check. Possible results are
            ACCESS_GRANTED or a StartDateError.
196
    """
197
    return check_start_date(user, descriptor.days_early_for_beta, descriptor.start, course_key)
198 199 200 201 202 203 204 205 206 207 208 209


def _can_view_courseware_with_prerequisites(user, course):  # pylint: disable=invalid-name
    """
    Checks if a user has access to a course based on its prerequisites.

    If the user is staff or anonymous, immediately grant access.
    Else, return whether or not the prerequisite courses have been passed.

    Arguments:
        user (User): the user whose course access we are checking.
        course (AType): the course for which we are checking access.
210 211 212
            where AType is CourseDescriptor, CourseOverview, or any other
            class that represents a course and has the attributes .location
            and .id.
213
    """
214 215 216 217 218

    def _is_prerequisites_disabled():
        """
        Checks if prerequisites are disabled in the settings.
        """
219
        return ACCESS_DENIED if is_prerequisite_courses_enabled() else ACCESS_GRANTED
220

221
    return (
222
        _is_prerequisites_disabled()
223 224
        or _has_staff_access_to_descriptor(user, course, course.id)
        or user.is_anonymous()
225
        or _has_fulfilled_prerequisites(user, [course.id])
226 227 228 229 230 231 232 233 234 235 236 237
    )


def _can_load_course_on_mobile(user, course):
    """
    Checks if a user can view the given course on a mobile device.

    This function only checks mobile-specific access restrictions. Other access
    restrictions such as start date and the .visible_to_staff_only flag must
    be checked by callers in *addition* to the return value of this function.

    Arguments:
238
        user (User): the user whose course access we are checking.
239 240 241 242 243 244 245 246 247 248
        course (CourseDescriptor|CourseOverview): the course for which we are
            checking access.

    Returns:
        bool: whether the course can be accessed on mobile.
    """
    return (
        is_mobile_available_for_user(user, course) and
        (
            _has_staff_access_to_descriptor(user, course, course.id) or
249
            _has_fulfilled_all_milestones(user, course.id)
250 251 252 253
        )
    )


254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
def _can_enroll_courselike(user, courselike):
    """
    Ascertain if the user can enroll in the given courselike object.

    Arguments:
        user (User): The user attempting to enroll.
        courselike (CourseDescriptor or CourseOverview): The object representing the
            course in which the user is trying to enroll.

    Returns:
        AccessResponse, indicating whether the user can enroll.
    """
    enrollment_domain = courselike.enrollment_domain
    # Courselike objects (e.g., course descriptors and CourseOverviews) have an attribute named `id`
    # which actually points to a CourseKey. Sigh.
    course_key = courselike.id

    # If using a registration method to restrict enrollment (e.g., Shibboleth)
    if settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and enrollment_domain:
        if user is not None and user.is_authenticated() and \
                ExternalAuthMap.objects.filter(user=user, external_domain=enrollment_domain):
            debug("Allow: external_auth of " + enrollment_domain)
            reg_method_ok = True
        else:
            reg_method_ok = False
    else:
        reg_method_ok = True

    # If the user appears in CourseEnrollmentAllowed paired with the given course key,
    # they may enroll. Note that as dictated by the legacy database schema, the filter
    # call includes a `course_id` kwarg which requires a CourseKey.
    if user is not None and user.is_authenticated():
        if CourseEnrollmentAllowed.objects.filter(email=user.email, course_id=course_key):
            return ACCESS_GRANTED

    if _has_staff_access_to_descriptor(user, courselike, course_key):
        return ACCESS_GRANTED

    if courselike.invitation_only:
        debug("Deny: invitation only")
        return ACCESS_DENIED

    now = datetime.now(UTC())
    enrollment_start = courselike.enrollment_start or datetime.min.replace(tzinfo=pytz.UTC)
    enrollment_end = courselike.enrollment_end or datetime.max.replace(tzinfo=pytz.UTC)
    if reg_method_ok and enrollment_start < now < enrollment_end:
        debug("Allow: in enrollment period")
        return ACCESS_GRANTED

    return ACCESS_DENIED


306
def _has_access_course(user, action, courselike):
307
    """
308 309 310 311 312 313 314
    Check if user has access to a course.

    Arguments:
        user (User): the user whose course access we are checking.
        action (string): The action that is being checked.
        courselike (CourseDescriptor or CourseOverview): The object
            representing the course that the user wants to access.
315 316 317 318

    Valid actions:

    'load' -- load the courseware, see inside the course
319
    'load_forum' -- can load and contribute to the forums (one access level for now)
320
    'load_mobile' -- can load from a mobile context
321
    'enroll' -- enroll.  Checks for enrollment window.
322 323
    'see_exists' -- can see that the course exists.
    'staff' -- staff access to course.
324 325
    'see_in_catalog' -- user is able to see the course listed in the course catalog.
    'see_about_page' -- user is able to see the course about page.
326 327
    """
    def can_load():
Calen Pennington committed
328 329
        """
        Can this user load this course?
330 331

        NOTE: this is not checking whether user is actually enrolled in the course.
Calen Pennington committed
332
        """
333 334 335 336 337 338 339 340 341
        response = (
            _visible_to_nonstaff_users(courselike) and
            _can_access_descriptor_with_start_date(user, courselike, courselike.id)
        )

        return (
            ACCESS_GRANTED if (response or _has_staff_access_to_descriptor(user, courselike, courselike.id))
            else response
        )
342 343

    def can_enroll():
344 345 346 347
        """
        Returns whether the user can enroll in the course.
        """
        return _can_enroll_courselike(user, courselike)
348 349 350 351 352 353

    def see_exists():
        """
        Can see if can enroll, but also if can load it: if user enrolled in a course and now
        it's past the enrollment period, they should still see it.
        """
354
        return ACCESS_GRANTED if (can_load() or can_enroll()) else ACCESS_DENIED
355

356 357 358 359 360 361 362
    def can_see_in_catalog():
        """
        Implements the "can see course in catalog" logic if a course should be visible in the main course catalog
        In this case we use the catalog_visibility property on the course descriptor
        but also allow course staff to see this.
        """
        return (
363 364
            _has_catalog_visibility(courselike, CATALOG_VISIBILITY_CATALOG_AND_ABOUT)
            or _has_staff_access_to_descriptor(user, courselike, courselike.id)
365 366 367 368 369 370 371 372 373
        )

    def can_see_about_page():
        """
        Implements the "can see course about page" logic if a course about page should be visible
        In this case we use the catalog_visibility property on the course descriptor
        but also allow course staff to see this.
        """
        return (
374 375 376
            _has_catalog_visibility(courselike, CATALOG_VISIBILITY_CATALOG_AND_ABOUT)
            or _has_catalog_visibility(courselike, CATALOG_VISIBILITY_ABOUT)
            or _has_staff_access_to_descriptor(user, courselike, courselike.id)
377 378
        )

379 380
    checkers = {
        'load': can_load,
381
        'view_courseware_with_prerequisites':
382 383
            lambda: _can_view_courseware_with_prerequisites(user, courselike),
        'load_mobile': lambda: can_load() and _can_load_course_on_mobile(user, courselike),
384 385
        'enroll': can_enroll,
        'see_exists': see_exists,
386 387
        'staff': lambda: _has_staff_access_to_descriptor(user, courselike, courselike.id),
        'instructor': lambda: _has_instructor_access_to_descriptor(user, courselike, courselike.id),
388 389
        'see_in_catalog': can_see_in_catalog,
        'see_about_page': can_see_about_page,
390
    }
391

392
    return _dispatch(checkers, action, user, courselike)
393 394


395
def _has_access_error_desc(user, action, descriptor, course_key):
396 397 398 399 400 401 402 403
    """
    Only staff should see error descriptors.

    Valid actions:
    'load' -- load this descriptor, showing it to the user.
    'staff' -- staff access to descriptor.
    """
    def check_for_staff():
404
        return _has_staff_access_to_descriptor(user, descriptor, course_key)
405 406 407

    checkers = {
        'load': check_for_staff,
408 409
        'staff': check_for_staff,
        'instructor': lambda: _has_instructor_access_to_descriptor(user, descriptor, course_key)
410
    }
411 412 413 414

    return _dispatch(checkers, action, user, descriptor)


415 416 417 418 419
def _has_group_access(descriptor, user, course_key):
    """
    This function returns a boolean indicating whether or not `user` has
    sufficient group memberships to "load" a block (the `descriptor`)
    """
420 421 422 423
    if len(descriptor.user_partitions) == len(get_split_user_partitions(descriptor.user_partitions)):
        # Short-circuit the process, since there are no defined user partitions that are not
        # user_partitions used by the split_test module. The split_test module handles its own access
        # via updating the children of the split_test module.
424
        return ACCESS_GRANTED
425 426 427 428 429 430 431 432

    # use merged_group_access which takes group access on the block's
    # parents / ancestors into account
    merged_access = descriptor.merged_group_access
    # check for False in merged_access, which indicates that at least one
    # partition's group list excludes all students.
    if False in merged_access.values():
        log.warning("Group access check excludes all students, access will be denied.", exc_info=True)
433
        return ACCESS_DENIED
434 435 436

    # resolve the partition IDs in group_access to actual
    # partition objects, skipping those which contain empty group directives.
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454
    # If a referenced partition could not be found, it will be denied
    # If the partition is found but is no longer active (meaning it's been disabled)
    # then skip the access check for that partition.
    partitions = []
    for partition_id, group_ids in merged_access.items():
        try:
            partition = descriptor._get_user_partition(partition_id)  # pylint: disable=protected-access
            if partition.active:
                if group_ids is not None:
                    partitions.append(partition)
            else:
                log.debug(
                    "Skipping partition with ID %s in course %s because it is no longer active",
                    partition.id, course_key
                )
        except NoSuchUserPartitionError:
            log.warning("Error looking up user partition, access will be denied.", exc_info=True)
            return ACCESS_DENIED
455 456 457 458 459 460 461 462 463 464 465 466 467

    # next resolve the group IDs specified within each partition
    partition_groups = []
    try:
        for partition in partitions:
            groups = [
                partition.get_group(group_id)
                for group_id in merged_access[partition.id]
            ]
            if groups:
                partition_groups.append((partition, groups))
    except NoSuchUserPartitionGroupError:
        log.warning("Error looking up referenced user partition group, access will be denied.", exc_info=True)
468
        return ACCESS_DENIED
469 470 471 472 473 474 475 476 477 478 479 480

    # look up the user's group for each partition
    user_groups = {}
    for partition, groups in partition_groups:
        user_groups[partition.id] = partition.scheme.get_group_for_user(
            course_key,
            user,
            partition,
        )

    # finally: check that the user has a satisfactory group assignment
    # for each partition.
Andy Armstrong committed
481
    if not all(user_groups.get(partition.id) in groups for partition, groups in partition_groups):
482
        return ACCESS_DENIED
483 484

    # all checks passed.
485
    return ACCESS_GRANTED
486 487


488
def _has_access_descriptor(user, action, descriptor, course_key=None):
489 490 491 492 493 494 495 496 497 498 499 500
    """
    Check if user has access to this descriptor.

    Valid actions:
    'load' -- load this descriptor, showing it to the user.
    'staff' -- staff access to descriptor.

    NOTE: This is the fallback logic for descriptors that don't have custom policy
    (e.g. courses).  If you call this method directly instead of going through
    has_access(), it will not do the right thing.
    """
    def can_load():
501 502 503 504 505 506
        """
        NOTE: This does not check that the student is enrolled in the course
        that contains this module.  We may or may not want to allow non-enrolled
        students to see modules.  If not, views should check the course, so we
        don't have to hit the enrollments table on every module load.
        """
507 508
        response = (
            _visible_to_nonstaff_users(descriptor)
509
            and _has_group_access(descriptor, user, course_key)
510 511 512
            and
            (
                _has_detached_class_tag(descriptor)
513
                or _can_access_descriptor_with_start_date(user, descriptor, course_key)
514
            )
515 516 517 518 519 520
        )

        return (
            ACCESS_GRANTED if (response or _has_staff_access_to_descriptor(user, descriptor, course_key))
            else response
        )
521 522 523

    checkers = {
        'load': can_load,
524 525 526
        'staff': lambda: _has_staff_access_to_descriptor(user, descriptor, course_key),
        'instructor': lambda: _has_instructor_access_to_descriptor(user, descriptor, course_key)
    }
527 528 529 530

    return _dispatch(checkers, action, user, descriptor)


531
def _has_access_xmodule(user, action, xmodule, course_key):
532 533 534 535 536 537 538
    """
    Check if user has access to this xmodule.

    Valid actions:
      - same as the valid actions for xmodule.descriptor
    """
    # Delegate to the descriptor
539
    return has_access(user, action, xmodule.descriptor, course_key)
540 541


542
def _has_access_location(user, action, location, course_key):
543 544 545 546 547 548 549 550 551 552 553
    """
    Check if user has access to this location.

    Valid actions:
    'staff' : True if the user has staff access to this location

    NOTE: if you add other actions, make sure that

     has_access(user, location, action) == has_access(user, get_item(location), action)
    """
    checkers = {
554 555
        'staff': lambda: _has_staff_access_to_location(user, location, course_key)
    }
556 557 558 559

    return _dispatch(checkers, action, user, location)


560 561 562 563 564 565 566 567 568 569 570 571 572 573 574
def _has_access_course_key(user, action, course_key):
    """
    Check if user has access to the course with this course_key

    Valid actions:
    'staff' : True if the user has staff access to this location
    'instructor' : True if the user has staff access to this location
    """
    checkers = {
        'staff': lambda: _has_staff_access_to_location(user, None, course_key),
        'instructor': lambda: _has_instructor_access_to_location(user, None, course_key),
    }

    return _dispatch(checkers, action, user, course_key)

575

576
def _has_access_ccx_key(user, action, ccx_key):
577 578 579 580 581
    """Check if user has access to the course for this ccx_key

    Delegates checking to _has_access_course_key
    Valid actions: same as for that function
    """
582 583 584
    course_key = ccx_key.to_course_locator()
    return _has_access_course_key(user, action, course_key)

585

stv committed
586
def _has_access_string(user, action, perm):
587 588 589 590 591 592 593 594
    """
    Check if user has certain special access, specified as string.  Valid strings:

    'global'

    Valid actions:

    'staff' -- global staff access.
595 596
    'support' -- access to student support functionality
    'certificates' --- access to view and regenerate certificates for other users.
597 598 599
    """

    def check_staff():
600 601 602
        """
        Checks for staff access
        """
603 604
        if perm != 'global':
            debug("Deny: invalid permission '%s'", perm)
605 606
            return ACCESS_DENIED
        return ACCESS_GRANTED if GlobalStaff().has_user(user) else ACCESS_DENIED
607

608 609 610 611 612 613 614 615 616
    def check_support():
        """Check that the user has access to the support UI. """
        if perm != 'global':
            return ACCESS_DENIED
        return (
            ACCESS_GRANTED if GlobalStaff().has_user(user) or SupportStaffRole().has_user(user)
            else ACCESS_DENIED
        )

617
    checkers = {
618 619 620
        'staff': check_staff,
        'support': check_support,
        'certificates': check_support,
621
    }
622 623 624 625

    return _dispatch(checkers, action, user, perm)


626 627 628 629 630 631 632 633 634 635 636 637
#####  Internal helper methods below

def _dispatch(table, action, user, obj):
    """
    Helper: call table[action], raising a nice pretty error if there is no such key.

    user and object passed in only for error messages and debugging
    """
    if action in table:
        result = table[action]()
        debug("%s user %s, object %s, action %s",
              'ALLOWED' if result else 'DENIED',
638
              user,
639
              obj.location.to_deprecated_string() if isinstance(obj, XBlock) else str(obj),
640
              action)
641 642
        return result

643
    raise ValueError(u"Unknown action for object type '{0}': '{1}'".format(
644 645
        type(obj), action))

646

647
def _adjust_start_date_for_beta_testers(user, descriptor, course_key):  # pylint: disable=invalid-name
648 649 650 651 652 653 654 655 656 657
    """
    If user is in a beta test group, adjust the start date by the appropriate number of
    days.

    Arguments:
       user: A django user.  May be anonymous.
       descriptor: the XModuleDescriptor the user is trying to get access to, with a
       non-None start date.

    Returns:
658
        A datetime.  Either the same as start, or earlier for beta testers.
659 660 661 662 663 664 665 666

    NOTE: number of days to adjust should be cached to avoid looking it up thousands of
    times per query.

    NOTE: For now, this function assumes that the descriptor's location is in the course
    the user is looking at.  Once we have proper usages and definitions per the XBlock
    design, this should use the course the usage is in.
    """
667
    return adjust_start_date(user, descriptor.days_early_for_beta, descriptor.start, course_key)
668

Calen Pennington committed
669

670 671 672 673
def _has_instructor_access_to_location(user, location, course_key=None):
    if course_key is None:
        course_key = location.course_key
    return _has_access_to_course(user, 'instructor', course_key)
674 675


676
def _has_staff_access_to_location(user, location, course_key=None):
677 678 679
    if course_key is None:
        course_key = location.course_key
    return _has_access_to_course(user, 'staff', course_key)
680

681

682
def _has_access_to_course(user, access_level, course_key):
683
    """
684
    Returns True if the given user has access_level (= staff or
685 686 687
    instructor) access to the course with the given course_key.
    This ensures the user is authenticated and checks if global staff or has
    staff / instructor access.
688 689

    access_level = string, either "staff" or "instructor"
690
    """
691
    if user is None or (not user.is_authenticated()):
692
        debug("Deny: no user or anon user")
693
        return ACCESS_DENIED
694

695
    if is_masquerading_as_student(user, course_key):
696
        return ACCESS_DENIED
697

698
    if GlobalStaff().has_user(user):
699
        debug("Allow: user.is_staff")
700
        return ACCESS_GRANTED
701

702
    if access_level not in ('staff', 'instructor'):
703
        log.debug("Error in access._has_access_to_course access_level=%s unknown", access_level)
704
        debug("Deny: unknown access level")
705
        return ACCESS_DENIED
706

707
    staff_access = (
708 709
        CourseStaffRole(course_key).has_user(user) or
        OrgStaffRole(course_key.org).has_user(user)
710 711 712
    )
    if staff_access and access_level == 'staff':
        debug("Allow: user has course staff access")
713
        return ACCESS_GRANTED
714 715

    instructor_access = (
716 717
        CourseInstructorRole(course_key).has_user(user) or
        OrgInstructorRole(course_key.org).has_user(user)
718 719 720 721
    )

    if instructor_access and access_level in ('staff', 'instructor'):
        debug("Allow: user has course instructor access")
722
        return ACCESS_GRANTED
723 724

    debug("Deny: user did not have correct access")
725
    return ACCESS_DENIED
726

727

728
def _has_instructor_access_to_descriptor(user, descriptor, course_key):  # pylint: disable=invalid-name
729 730 731 732 733
    """Helper method that checks whether the user has staff access to
    the course of the location.

    descriptor: something that has a location attribute
    """
734
    return _has_instructor_access_to_location(user, descriptor.location, course_key)
735

736

737
def _has_staff_access_to_descriptor(user, descriptor, course_key):
738 739 740
    """Helper method that checks whether the user has staff access to
    the course of the location.

741
    descriptor: something that has a location attribute
742
    """
743
    return _has_staff_access_to_location(user, descriptor.location, course_key)
744 745


746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803
def _visible_to_nonstaff_users(descriptor):
    """
    Returns if the object is visible to nonstaff users.

    Arguments:
        descriptor: object to check
    """
    return VisibilityError() if descriptor.visible_to_staff_only else ACCESS_GRANTED


def _has_detached_class_tag(descriptor):
    """
    Returns if the given descriptor's type is marked as detached.

    Arguments:
        descriptor: object to check
    """
    return ACCESS_GRANTED if 'detached' in descriptor._class_tags else ACCESS_DENIED  # pylint: disable=protected-access


def _has_fulfilled_all_milestones(user, course_id):
    """
    Returns whether the given user has fulfilled all milestones for the
    given course.

    Arguments:
        course_id: ID of the course to check
        user_id: ID of the user to check
    """
    return MilestoneError() if any_unfulfilled_milestones(course_id, user.id) else ACCESS_GRANTED


def _has_fulfilled_prerequisites(user, course_id):
    """
    Returns whether the given user has fulfilled all prerequisites for the
    given course.

    Arguments:
        user: user to check
        course_id: ID of the course to check
    """
    return MilestoneError() if get_pre_requisite_courses_not_completed(user, course_id) else ACCESS_GRANTED


def _has_catalog_visibility(course, visibility_type):
    """
    Returns whether the given course has the given visibility type
    """
    return ACCESS_GRANTED if course.catalog_visibility == visibility_type else ACCESS_DENIED


def _is_descriptor_mobile_available(descriptor):
    """
    Returns if descriptor is available on mobile.
    """
    return ACCESS_GRANTED if descriptor.mobile_available else MobileAvailabilityError()


804
def is_mobile_available_for_user(user, descriptor):
805 806 807 808 809
    """
    Returns whether the given course is mobile_available for the given user.
    Checks:
        mobile_available flag on the course
        Beta User and staff access overrides the mobile_available flag
810 811
    Arguments:
        descriptor (CourseDescriptor|CourseOverview): course or overview of course in question
812 813
    """
    return (
814
        auth.user_has_role(user, CourseBetaTesterRole(descriptor.id))
815 816
        or _has_staff_access_to_descriptor(user, descriptor, descriptor.id)
        or _is_descriptor_mobile_available(descriptor)
817 818 819
    )


820
def get_user_role(user, course_key):
821 822 823 824
    """
    Return corresponding string if user has staff, instructor or student
    course role in LMS.
    """
825 826 827
    role = get_masquerade_role(user, course_key)
    if role:
        return role
828
    elif has_access(user, 'instructor', course_key):
829
        return 'instructor'
830
    elif has_access(user, 'staff', course_key):
831 832 833
        return 'staff'
    else:
        return 'student'