Files
edx-platform/cms/djangoapps/auth/authz.py

294 lines
10 KiB
Python

"""
Studio authorization functions primarily for course creators, instructors, and staff
"""
#=======================================================================================================================
#
# This code is somewhat duplicative of access.py in the LMS. We will unify the code as a separate story
# but this implementation should be data compatible with the LMS implementation
#
#=======================================================================================================================
from django.contrib.auth.models import User, Group
from django.core.exceptions import PermissionDenied
from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.locator import CourseLocator, Locator
from xmodule.modulestore.django import loc_mapper
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError
import itertools
# define a couple of simple roles, we just need ADMIN and EDITOR now for our purposes
INSTRUCTOR_ROLE_NAME = 'instructor'
STAFF_ROLE_NAME = 'staff'
# This is the group of people who have permission to create new courses on edge or edx.
COURSE_CREATOR_GROUP_NAME = "course_creator_group"
# we're just making a Django group for each location/role combo
# to do this we're just creating a Group name which is a formatted string
# of those two variables
def get_all_course_role_groupnames(location, role, use_filter=True):
'''
Get all of the possible groupnames for this role location pair. If use_filter==True,
only return the ones defined in the groups collection.
'''
location = Locator.to_locator_or_location(location)
# hack: check for existence of a group name in the legacy LMS format <role>_<course>
# if it exists, then use that one, otherwise use a <role>_<course_id> which contains
# more information
groupnames = []
try:
groupnames.append('{0}_{1}'.format(role, location.course_id))
except InvalidLocationError: # will occur on old locations where location is not of category course
pass
if isinstance(location, Location):
# least preferred role_course format
groupnames.append('{0}_{1}'.format(role, location.course))
try:
locator = loc_mapper().translate_location(location.course_id, location, False, False)
groupnames.append('{0}_{1}'.format(role, locator.course_id))
except (InvalidLocationError, ItemNotFoundError):
pass
elif isinstance(location, CourseLocator):
old_location = loc_mapper().translate_locator_to_location(location, get_course=True)
if old_location:
# the slashified version of the course_id (myu/mycourse/myrun)
groupnames.append('{0}_{1}'.format(role, old_location.course_id))
# add the least desirable but sometimes occurring format.
groupnames.append('{0}_{1}'.format(role, old_location.course))
# filter to the ones which exist
default = groupnames[0]
if use_filter:
groupnames = [group.name for group in Group.objects.filter(name__in=groupnames)]
return groupnames, default
def get_course_groupname_for_role(location, role):
'''
Get the preferred used groupname for this role, location combo.
Preference order:
* role_course_id (e.g., staff_myu.mycourse.myrun)
* role_old_course_id (e.g., staff_myu/mycourse/myrun)
* role_old_course (e.g., staff_mycourse)
'''
groupnames, default = get_all_course_role_groupnames(location, role)
return groupnames[0] if groupnames else default
def get_course_role_users(course_locator, role):
'''
Get all of the users with the given role in the given course.
'''
groupnames, _ = get_all_course_role_groupnames(course_locator, role)
groups = [Group.objects.get(name=groupname) for groupname in groupnames]
return list(itertools.chain.from_iterable(group.user_set.all() for group in groups))
def create_all_course_groups(creator, location):
"""
Create all permission groups for a new course and subscribe the caller into those roles
"""
create_new_course_group(creator, location, INSTRUCTOR_ROLE_NAME)
create_new_course_group(creator, location, STAFF_ROLE_NAME)
def create_new_course_group(creator, location, role):
'''
Create the new course group always using the preferred name even if another form already exists.
'''
groupnames, __ = get_all_course_role_groupnames(location, role, use_filter=False)
group, __ = Group.objects.get_or_create(name=groupnames[0])
creator.groups.add(group)
creator.save()
return
def _delete_course_group(location):
"""
This is to be called only by either a command line code path or through a app which has already
asserted permissions
"""
# remove all memberships
for role in [INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME]:
groupnames, _ = get_all_course_role_groupnames(location, role)
for groupname in groupnames:
group = Group.objects.get(name=groupname)
for user in group.user_set.all():
user.groups.remove(group)
user.save()
def _copy_course_group(source, dest):
"""
This is to be called only by either a command line code path or through an app which has already
asserted permissions to do this action
"""
for role in [INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME]:
groupnames, _ = get_all_course_role_groupnames(source, role)
for groupname in groupnames:
group = Group.objects.get(name=groupname)
new_group, _ = Group.objects.get_or_create(name=get_course_groupname_for_role(dest, INSTRUCTOR_ROLE_NAME))
for user in group.user_set.all():
user.groups.add(new_group)
user.save()
def add_user_to_course_group(caller, user, location, role):
"""
If caller is authorized, add the given user to the given course's role
"""
# only admins can add/remove other users
if not is_user_in_course_group_role(caller, location, INSTRUCTOR_ROLE_NAME):
raise PermissionDenied
group, _ = Group.objects.get_or_create(name=get_course_groupname_for_role(location, role))
return _add_user_to_group(user, group)
def add_user_to_creator_group(caller, user):
"""
Adds the user to the group of course creators.
The caller must have staff access to perform this operation.
Note that on the edX site, we currently limit course creators to edX staff, and this
method is a no-op in that environment.
"""
if not caller.is_active or not caller.is_authenticated or not caller.is_staff:
raise PermissionDenied
(group, _) = Group.objects.get_or_create(name=COURSE_CREATOR_GROUP_NAME)
return _add_user_to_group(user, group)
def _add_user_to_group(user, group):
"""
This is to be called only by either a command line code path or through an app which has already
asserted permissions to do this action
"""
if user.is_active and user.is_authenticated:
user.groups.add(group)
user.save()
return True
return False
def get_user_by_email(email):
"""
Get the user whose email is the arg. Return None if no such user exists.
"""
user = None
# try to look up user, return None if not found
try:
user = User.objects.get(email=email)
except:
pass
return user
def remove_user_from_course_group(caller, user, location, role):
"""
If caller is authorized, remove the given course x role authorization for user
"""
# only admins can add/remove other users
if not is_user_in_course_group_role(caller, location, INSTRUCTOR_ROLE_NAME):
raise PermissionDenied
# see if the user is actually in that role, if not then we don't have to do anything
groupnames, _ = get_all_course_role_groupnames(location, role)
user.groups.remove(*user.groups.filter(name__in=groupnames))
user.save()
def remove_user_from_creator_group(caller, user):
"""
Removes user from the course creator group.
The caller must have staff access to perform this operation.
"""
if not caller.is_active or not caller.is_authenticated or not caller.is_staff:
raise PermissionDenied
_remove_user_from_group(user, COURSE_CREATOR_GROUP_NAME)
def _remove_user_from_group(user, group_name):
"""
This is to be called only by either a command line code path or through an app which has already
asserted permissions to do this action
"""
group = Group.objects.get(name=group_name)
user.groups.remove(group)
user.save()
def is_user_in_course_group_role(user, location, role, check_staff=True):
"""
Check whether the given user has the given role in this course. If check_staff
then give permission if the user is staff without doing a course-role query.
"""
if user.is_active and user.is_authenticated:
# all "is_staff" flagged accounts belong to all groups
if check_staff and user.is_staff:
return True
groupnames, _ = get_all_course_role_groupnames(location, role)
return user.groups.filter(name__in=groupnames).exists()
return False
def is_user_in_creator_group(user):
"""
Returns true if the user has permissions to create a course.
Will always return True if user.is_staff is True.
Note that on the edX site, we currently limit course creators to edX staff. On
other sites, this method checks that the user is in the course creator group.
"""
if user.is_staff:
return True
# On edx, we only allow edX staff to create courses. This may be relaxed in the future.
if settings.FEATURES.get('DISABLE_COURSE_CREATION', False):
return False
# Feature flag for using the creator group setting. Will be removed once the feature is complete.
if settings.FEATURES.get('ENABLE_CREATOR_GROUP', False):
return user.groups.filter(name=COURSE_CREATOR_GROUP_NAME).exists()
return True
def get_users_with_instructor_role():
"""
Returns all users with the role 'instructor'
"""
return _get_users_with_role(INSTRUCTOR_ROLE_NAME)
def get_users_with_staff_role():
"""
Returns all users with the role 'staff'
"""
return _get_users_with_role(STAFF_ROLE_NAME)
def _get_users_with_role(role):
"""
Returns all users with the specified role.
"""
users = set()
for group in Group.objects.all():
if group.name.startswith(role + "_"):
for user in group.user_set.all():
users.add(user)
return users