Allow anonymous access for course blocks API

Opens the course blocks API to public access, and allows anonymous users to
use the API to fetch data about public courses. Anonymous users need to
explicitly pass an empty username parameter to get the block data that is
visible to the public.
This commit is contained in:
Kshitij Sobti
2020-06-17 14:49:01 +05:30
parent fe550fbd5f
commit 570d02a858
4 changed files with 229 additions and 53 deletions

View File

@@ -3,8 +3,7 @@ Course API Forms
"""
import six
from django.contrib.auth.models import User
from django.contrib.auth.models import AnonymousUser, User
from django.core.exceptions import ValidationError
from django.forms import CharField, ChoiceField, Form, IntegerField
from django.http import Http404
@@ -49,7 +48,7 @@ class BlockListGetForm(Form):
try:
return int(value)
except ValueError:
raise ValidationError(u"'{}' is not a valid depth value.".format(value))
raise ValidationError("'{}' is not a valid depth value.".format(value))
def clean_requested_fields(self):
"""
@@ -76,7 +75,7 @@ class BlockListGetForm(Form):
try:
usage_key = UsageKey.from_string(usage_key)
except InvalidKeyError:
raise ValidationError(u"'{}' is not a valid usage key.".format(six.text_type(usage_key)))
raise ValidationError("'{}' is not a valid usage key.".format(str(usage_key)))
return usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))
@@ -106,38 +105,86 @@ class BlockListGetForm(Form):
cleaned_data['user'] = self._clean_requested_user(cleaned_data, usage_key.course_key)
return cleaned_data
def clean_username(self):
"""
Return cleaned username.
Overrides the default behaviour that maps an empty string to None. This
allows us to differentiate between no username being provided (None) vs
an empty username being provided ('').
"""
# In case all_blocks is specified, ignore the username.
if self.cleaned_data.get('all_blocks', False):
return None
# See if 'username' was provided as a parameter in the raw data.
# If so, we return the already-cleaned version of that, otherwise we
# return None
if 'username' in self.data:
return self.cleaned_data['username']
return None
def _clean_requested_user(self, cleaned_data, course_key):
"""
Validates and returns the requested_user, while checking permissions.
"""
requesting_user = self.initial['requesting_user']
requested_username = cleaned_data.get('username', None)
all_blocks = cleaned_data.get('all_blocks', False)
if not requested_username:
return self._verify_no_user(requesting_user, cleaned_data, course_key)
if requested_username is None and not all_blocks:
raise ValidationError({'username': ["This field is required unless all_blocks is requested."]})
if requesting_user.is_anonymous:
return self._verify_anonymous_user(requested_username, course_key, all_blocks)
if all_blocks:
return self._verify_all_blocks(requesting_user, course_key)
elif requesting_user.username.lower() == requested_username.lower():
return self._verify_requesting_user(requesting_user, course_key)
else:
return self._verify_other_user(requesting_user, requested_username, course_key)
@staticmethod
def _verify_no_user(requesting_user, cleaned_data, course_key):
def _verify_anonymous_user(username, course_key, all_blocks):
"""
Verifies form for when the requesting user is anonymous.
"""
if all_blocks:
raise PermissionDenied(
"Anonymous users do not have permission to access all blocks in '{course_key}'.".format(
course_key=str(course_key),
)
)
# Check for '' and explicitly '' since the only valid option for anonymous users is
# an empty string that corresponds to an anonymous user.
if username != '':
raise PermissionDenied("Anonymous users cannot access another user's blocks.")
if not permissions.is_course_public(course_key):
raise PermissionDenied(
"Course blocks for '{course_key}' cannot be accessed anonymously.".format(
course_key=course_key,
)
)
return AnonymousUser()
@staticmethod
def _verify_all_blocks(requesting_user, course_key): # pylint: disable=useless-return
"""
Verifies form for when no username is specified, including permissions.
"""
# Verify that access to all blocks is requested
# (and not unintentionally requested).
if not cleaned_data.get('all_blocks', None):
raise ValidationError({'username': ['This field is required unless all_blocks is requested.']})
# Verify all blocks can be accessed for the course.
if not permissions.can_access_all_blocks(requesting_user, course_key):
raise PermissionDenied(
u"'{requesting_username}' does not have permission to access all blocks in '{course_key}'."
.format(requesting_username=requesting_user.username, course_key=six.text_type(course_key))
"'{requesting_username}' does not have permission to access all blocks in '{course_key}'.".format(
requesting_username=requesting_user.username,
course_key=str(course_key),
)
)
# return None for user
return None
@staticmethod
@@ -147,8 +194,9 @@ class BlockListGetForm(Form):
"""
if not permissions.can_access_self_blocks(requesting_user, course_key):
raise PermissionDenied(
u"Course blocks for '{requesting_username}' cannot be accessed."
.format(requesting_username=requesting_user.username)
"Course blocks for '{requesting_username}' cannot be accessed.".format(
requesting_username=requesting_user.username,
)
)
return requesting_user
@@ -158,11 +206,18 @@ class BlockListGetForm(Form):
Verifies whether the requesting user can access another user's view of
the blocks in the course.
"""
# If accessing a public course, and requesting only content available publicly,
# we can allow the request.
if requested_username == '' and permissions.is_course_public(course_key):
return AnonymousUser()
# Verify requesting user can access the user's blocks.
if not permissions.can_access_others_blocks(requesting_user, course_key):
raise PermissionDenied(
u"'{requesting_username}' does not have permission to access view for '{requested_username}'."
.format(requesting_username=requesting_user.username, requested_username=requested_username)
"'{requesting_username}' does not have permission to access view for '{requested_username}'.".format(
requesting_username=requesting_user.username,
requested_username=requested_username,
)
)
# Verify user exists.
@@ -170,5 +225,7 @@ class BlockListGetForm(Form):
return User.objects.get(username=requested_username)
except User.DoesNotExist:
raise Http404(
u"Requested user '{requested_username}' does not exist.".format(requested_username=requested_username)
"Requested user '{requested_username}' does not exist.".format(
requested_username=requested_username,
)
)