Replace authz fns with roles.py ones

STUD-1006
This commit is contained in:
Don Mitchell
2013-12-27 16:11:23 -05:00
parent 858e354a6d
commit 3ebac80778
30 changed files with 439 additions and 642 deletions

View File

@@ -0,0 +1,86 @@
"""
The application interface to roles which checks whether any user trying to change
authorization has authorization to do so, which infers authorization via role hierarchy
(GlobalStaff is superset of auths of course instructor, ...), which consults the config
to decide whether to check course creator role, and other such functions.
"""
from django.core.exceptions import PermissionDenied
from django.conf import settings
from student.roles import GlobalStaff, CourseCreatorRole, CourseStaffRole, CourseInstructorRole, CourseRole, \
CourseBetaTesterRole
def has_access(user, role):
"""
Check whether this user has access to this role (either direct or implied)
:param user:
:param role: an AccessRole
"""
if not user.is_active:
return False
# do cheapest check first even tho it's not the direct one
if GlobalStaff().has_user(user):
return True
# CourseCreator is odd b/c it can be disabled via config
if isinstance(role, CourseCreatorRole):
# completely shut down course creation setting
if settings.FEATURES.get('DISABLE_COURSE_CREATION', False):
return False
# wide open course creation setting
if not settings.FEATURES.get('ENABLE_CREATOR_GROUP', False):
return True
if role.has_user(user):
return True
# if not, then check inferred permissions
if (isinstance(role, (CourseStaffRole, CourseBetaTesterRole)) and
CourseInstructorRole(role.location).has_user(user)):
return True
return False
def add_users(caller, role, *users):
"""
The caller requests adding the given users to the role. Checks that the caller
has sufficient authority.
:param caller: a user
:param role: an AccessRole
"""
_check_caller_authority(caller, role)
role.add_users(*users)
def remove_users(caller, role, *users):
"""
The caller requests removing the given users from the role. Checks that the caller
has sufficient authority.
:param caller: a user
:param role: an AccessRole
"""
# can always remove self (at this layer)
if not(len(users) == 1 and caller == users[0]):
_check_caller_authority(caller, role)
role.remove_users(*users)
def _check_caller_authority(caller, role):
"""
Internal function to check whether the caller has authority to manipulate this role
:param caller: a user
:param role: an AccessRole
"""
if not (caller.is_authenticated and caller.is_active):
raise PermissionDenied
# superuser
if GlobalStaff().has_user(caller):
return
if isinstance(role, (GlobalStaff, CourseCreatorRole)):
raise PermissionDenied
elif isinstance(role, CourseRole): # instructors can change the roles w/in their course
if not has_access(caller, CourseInstructorRole(role.location)):
raise PermissionDenied

View File

@@ -64,11 +64,13 @@ class GlobalStaff(AccessRole):
def add_users(self, *users):
for user in users:
user.is_staff = True
user.save()
if (user.is_authenticated and user.is_active):
user.is_staff = True
user.save()
def remove_users(self, *users):
for user in users:
# don't check is_authenticated nor is_active on purpose
user.is_staff = False
user.save()
@@ -96,10 +98,10 @@ class GroupBasedRole(AccessRole):
"""
Return whether the supplied django user has access to this role.
"""
# pylint: disable=protected-access
if not user.is_authenticated():
if not (user.is_authenticated and user.is_active):
return False
# pylint: disable=protected-access
if not hasattr(user, '_groups'):
user._groups = set(name.lower() for name in user.groups.values_list('name', flat=True))
@@ -109,8 +111,12 @@ class GroupBasedRole(AccessRole):
"""
Add the supplied django users to this role.
"""
# silently ignores anonymous and inactive users so that any that are
# legit get updated.
users = [user for user in users if user.is_authenticated and user.is_active]
group, _ = Group.objects.get_or_create(name=self._group_names[0])
group.user_set.add(*users)
# remove cache
for user in users:
if hasattr(user, '_groups'):
del user._groups
@@ -119,8 +125,10 @@ class GroupBasedRole(AccessRole):
"""
Remove the supplied django users from this role.
"""
group, _ = Group.objects.get_or_create(name=self._group_names[0])
group.user_set.remove(*users)
groups = Group.objects.filter(name__in=self._group_names)
for group in groups:
group.user_set.remove(*users)
# remove cache
for user in users:
if hasattr(user, '_groups'):
del user._groups
@@ -144,31 +152,33 @@ class CourseRole(GroupBasedRole):
"""
# TODO: figure out how to make the group name generation lazy so it doesn't force the
# loc mapping?
location = Locator.to_locator_or_location(location)
self.location = Locator.to_locator_or_location(location)
self.role = role
# direct copy from auth.authz.get_all_course_role_groupnames will refactor to one impl asap
groupnames = []
# pylint: disable=no-member
if isinstance(location, Location):
if isinstance(self.location, Location):
try:
groupnames.append('{0}_{1}'.format(role, location.course_id))
groupnames.append('{0}_{1}'.format(role, self.location.course_id))
course_context = self.location.course_id # course_id is valid for translation
except InvalidLocationError: # will occur on old locations where location is not of category course
if course_context is None:
raise CourseContextRequired()
else:
groupnames.append('{0}_{1}'.format(role, course_context))
try:
locator = loc_mapper().translate_location(location.course_id, location, False, False)
locator = loc_mapper().translate_location(course_context, self.location, False, False)
groupnames.append('{0}_{1}'.format(role, locator.package_id))
except (InvalidLocationError, ItemNotFoundError):
# if it's never been mapped, the auth won't be via the Locator syntax
pass
# least preferred legacy role_course format
groupnames.append('{0}_{1}'.format(role, location.course))
elif isinstance(location, CourseLocator):
groupnames.append('{0}_{1}'.format(role, location.package_id))
groupnames.append('{0}_{1}'.format(role, self.location.course))
elif isinstance(self.location, CourseLocator):
groupnames.append('{0}_{1}'.format(role, self.location.package_id))
# handle old Location syntax
old_location = loc_mapper().translate_locator_to_location(location, get_course=True)
old_location = loc_mapper().translate_locator_to_location(self.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))
@@ -191,20 +201,23 @@ class OrgRole(GroupBasedRole):
class CourseStaffRole(CourseRole):
"""A Staff member of a course"""
ROLE = 'staff'
def __init__(self, *args, **kwargs):
super(CourseStaffRole, self).__init__('staff', *args, **kwargs)
super(CourseStaffRole, self).__init__(self.ROLE, *args, **kwargs)
class CourseInstructorRole(CourseRole):
"""A course Instructor"""
ROLE = 'instructor'
def __init__(self, *args, **kwargs):
super(CourseInstructorRole, self).__init__('instructor', *args, **kwargs)
super(CourseInstructorRole, self).__init__(self.ROLE, *args, **kwargs)
class CourseBetaTesterRole(CourseRole):
"""A course Beta Tester"""
ROLE = 'beta_testers'
def __init__(self, *args, **kwargs):
super(CourseBetaTesterRole, self).__init__('beta_testers', *args, **kwargs)
super(CourseBetaTesterRole, self).__init__(self.ROLE, *args, **kwargs)
class OrgStaffRole(OrgRole):
@@ -217,3 +230,13 @@ class OrgInstructorRole(OrgRole):
"""An organization instructor"""
def __init__(self, *args, **kwargs):
super(OrgInstructorRole, self).__init__('instructor', *args, **kwargs)
class CourseCreatorRole(GroupBasedRole):
"""
This is the group of people who have permission to create new courses (we may want to eventually
make this an org based role).
"""
ROLE = "course_creator_group"
def __init__(self, *args, **kwargs):
super(CourseCreatorRole, self).__init__(self.ROLE, *args, **kwargs)