api.py 6.79 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
"""
The Python API layer of the country access settings. Essentially the middle tier of the project, responsible for all
business logic that is not directly tied to the data itself.

This API is exposed via the middleware(emabargo/middileware.py) layer but may be used directly in-process.

"""
import logging

from django.conf import settings
11
from django.core.cache import cache
12
from ipware.ip import get_ip
13 14
from rest_framework import status
from rest_framework.response import Response
15

16
import pygeoip
17
from student.auth import has_course_author_access
18

19
from .models import CountryAccessRule, RestrictedCourse
20

21 22 23
log = logging.getLogger(__name__)


24
def redirect_if_blocked(course_key, access_point='enrollment', **kwargs):
25 26
    """Redirect if the user does not have access to the course. In case of blocked if access_point
    is not enrollment and course has enabled is_disabled_access_check then user can view that course.
27 28 29 30 31 32 33 34

    Arguments:
        course_key (CourseKey): Location of the course the user is trying to access.

    Keyword Arguments:
        Same as `check_course_access` and `message_url_path`

    """
35
    if settings.FEATURES.get('EMBARGO'):
36 37
        is_blocked = not check_course_access(course_key, **kwargs)
        if is_blocked:
38 39 40 41 42
            if access_point == "courseware":
                if not RestrictedCourse.is_disabled_access_check(course_key):
                    return message_url_path(course_key, access_point)
            else:
                return message_url_path(course_key, access_point)
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62


def check_course_access(course_key, user=None, ip_address=None, url=None):
    """
    Check is the user with this ip_address has access to the given course

    Arguments:
        course_key (CourseKey): Location of the course the user is trying to access.

    Keyword Arguments:
        user (User): The user making the request.  Can be None, in which case
            the user's profile country will not be checked.
        ip_address (str): The IP address of the request.
        url (str): The URL the user is trying to access.  Used in
            log messages.

    Returns:
        Boolean: True if the user has access to the course; False otherwise

    """
63
    # No-op if the country access feature is not enabled
64
    if not settings.FEATURES.get('EMBARGO'):
65 66
        return True

67 68 69 70 71 72 73
    # First, check whether there are any restrictions on the course.
    # If not, then we do not need to do any further checks
    course_is_restricted = RestrictedCourse.is_restricted_course(course_key)

    if not course_is_restricted:
        return True

74 75 76 77
    # Always give global and course staff access, regardless of embargo settings.
    if user is not None and has_course_author_access(user, course_key):
        return True

78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
    if ip_address is not None:
        # Retrieve the country code from the IP address
        # and check it against the allowed countries list for a course
        user_country_from_ip = _country_code_from_ip(ip_address)

        if not CountryAccessRule.check_country_access(course_key, user_country_from_ip):
            log.info(
                (
                    u"Blocking user %s from accessing course %s at %s "
                    u"because the user's IP address %s appears to be "
                    u"located in %s."
                ),
                getattr(user, 'id', '<Not Authenticated>'),
                course_key,
                url,
                ip_address,
                user_country_from_ip
            )
            return False

    if user is not None:
        # Retrieve the country code from the user's profile
        # and check it against the allowed countries list for a course.
        user_country_from_profile = _get_user_country_from_profile(user)

        if not CountryAccessRule.check_country_access(course_key, user_country_from_profile):
            log.info(
                (
                    u"Blocking user %s from accessing course %s at %s "
                    u"because the user's profile country is %s."
                ),
                user.id, course_key, url, user_country_from_profile
            )
            return False

    return True


def message_url_path(course_key, access_point):
    """Determine the URL path for the message explaining why the user was blocked.

    This is configured per-course.  See `RestrictedCourse` in the `embargo.models`
    module for more details.

    Arguments:
        course_key (CourseKey): The location of the course.
        access_point (str): How the user was trying to access the course.
            Can be either "enrollment" or "courseware".

    Returns:
        unicode: The URL path to a page explaining why the user was blocked.

    Raises:
        InvalidAccessPoint: Raised if access_point is not a supported value.

    """
    return RestrictedCourse.message_url_path(course_key, access_point)


def _get_user_country_from_profile(user):
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
    """
    Check whether the user is embargoed based on the country code in the user's profile.

    Args:
        user (User): The user attempting to access courseware.

    Returns:
        user country from profile.

    """
    cache_key = u'user.{user_id}.profile.country'.format(user_id=user.id)
    profile_country = cache.get(cache_key)
    if profile_country is None:
        profile = getattr(user, 'profile', None)
        if profile is not None and profile.country.code is not None:
            profile_country = profile.country.code.upper()
        else:
            profile_country = ""
        cache.set(cache_key, profile_country)

    return profile_country


def _country_code_from_ip(ip_addr):
    """
    Return the country code associated with an IP address.
    Handles both IPv4 and IPv6 addresses.

    Args:
        ip_addr (str): The IP address to look up.

    Returns:
        str: A 2-letter country code.

    """
    if ip_addr.find(':') >= 0:
        return pygeoip.GeoIP(settings.GEOIPV6_PATH).country_code_by_addr(ip_addr)
    else:
        return pygeoip.GeoIP(settings.GEOIP_PATH).country_code_by_addr(ip_addr)
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203


def get_embargo_response(request, course_id, user):
    """
    Check whether any country access rules block the user from enrollment.

    Args:
        request (HttpRequest): The request object
        course_id (str): The requested course ID
        user (str): The current user object

    Returns:
        HttpResponse: Response of the embargo page if embargoed, None if not

    """
    redirect_url = redirect_if_blocked(
        course_id, user=user, ip_address=get_ip(request), url=request.path)
    if redirect_url:
        return Response(
            status=status.HTTP_403_FORBIDDEN,
            data={
                "message": (
                    u"Users from this location cannot access the course '{course_id}'."
                ).format(course_id=course_id),
                "user_message_url": request.build_absolute_uri(redirect_url)
            }
        )