Commit 008b8a68 by Calen Pennington

Merge pull request #844 from MITx/feature/cdodge/cms-unified-user-database

adhere to the naming conventions of user groups (instructor_* and staff_...
parents f43c340c a80e8ce3
...@@ -6,21 +6,33 @@ from django.core.exceptions import PermissionDenied ...@@ -6,21 +6,33 @@ from django.core.exceptions import PermissionDenied
from xmodule.modulestore import Location from xmodule.modulestore import Location
'''
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
'''
# define a couple of simple roles, we just need ADMIN and EDITOR now for our purposes # define a couple of simple roles, we just need ADMIN and EDITOR now for our purposes
ADMIN_ROLE_NAME = 'admin' INSTRUCTOR_ROLE_NAME = 'instructor'
EDITOR_ROLE_NAME = 'editor' STAFF_ROLE_NAME = 'staff'
# we're just making a Django group for each location/role combo # 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 # to do this we're just creating a Group name which is a formatted string
# of those two variables # of those two variables
def get_course_groupname_for_role(location, role): def get_course_groupname_for_role(location, role):
loc = Location(location) loc = Location(location)
groupname = loc.course_id + ':' + role # 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
groupname = '{0}_{1}'.format(role, loc.course)
if len(Group.objects.filter(name = groupname)) == 0:
groupname = '{0}_{1}'.format(role,loc.course_id)
return groupname return groupname
def get_users_in_course_group_by_role(location, role): def get_users_in_course_group_by_role(location, role):
groupname = get_course_groupname_for_role(location, role) groupname = get_course_groupname_for_role(location, role)
group = Group.objects.get(name=groupname) (group, created) = Group.objects.get_or_create(name=groupname)
return group.user_set.all() return group.user_set.all()
...@@ -28,8 +40,8 @@ def get_users_in_course_group_by_role(location, role): ...@@ -28,8 +40,8 @@ def get_users_in_course_group_by_role(location, role):
Create all permission groups for a new course and subscribe the caller into those roles Create all permission groups for a new course and subscribe the caller into those roles
''' '''
def create_all_course_groups(creator, location): def create_all_course_groups(creator, location):
create_new_course_group(creator, location, ADMIN_GROUP_NAME) create_new_course_group(creator, location, INSTRUCTOR_ROLE_NAME)
create_new_course_group(creator, location, EDITOR_GROUP_NAME) create_new_course_group(creator, location, STAFF_ROLE_NAME)
def create_new_course_group(creator, location, role): def create_new_course_group(creator, location, role):
...@@ -46,7 +58,7 @@ def create_new_course_group(creator, location, role): ...@@ -46,7 +58,7 @@ def create_new_course_group(creator, location, role):
def add_user_to_course_group(caller, user, location, role): def add_user_to_course_group(caller, user, location, role):
# only admins can add/remove other users # only admins can add/remove other users
if not is_user_in_course_group_role(caller, location, ADMIN_ROLE_NAME): if not is_user_in_course_group_role(caller, location, INSTRUCTOR_ROLE_NAME):
raise PermissionDenied raise PermissionDenied
if user.is_active and user.is_authenticated: if user.is_active and user.is_authenticated:
...@@ -73,7 +85,7 @@ def get_user_by_email(email): ...@@ -73,7 +85,7 @@ def get_user_by_email(email):
def remove_user_from_course_group(caller, user, location, role): def remove_user_from_course_group(caller, user, location, role):
# only admins can add/remove other users # only admins can add/remove other users
if not is_user_in_course_group_role(caller, location, ADMIN_ROLE_NAME): if not is_user_in_course_group_role(caller, location, INSTRUCTOR_ROLE_NAME):
raise PermissionDenied raise PermissionDenied
# see if the user is actually in that role, if not then we don't have to do anything # see if the user is actually in that role, if not then we don't have to do anything
......
...@@ -42,7 +42,7 @@ from xmodule.contentstore.content import StaticContent ...@@ -42,7 +42,7 @@ from xmodule.contentstore.content import StaticContent
from cache_toolbox.core import set_cached_content, get_cached_content, del_cached_content from cache_toolbox.core import set_cached_content, get_cached_content, del_cached_content
from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
from auth.authz import ADMIN_ROLE_NAME, EDITOR_ROLE_NAME from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME
from .utils import get_course_location_for_item, get_lms_link_for_item from .utils import get_course_location_for_item, get_lms_link_for_item
from xmodule.templates import all_templates from xmodule.templates import all_templates
...@@ -98,11 +98,22 @@ def index(request): ...@@ -98,11 +98,22 @@ def index(request):
# ==== Views with per-item permissions================================ # ==== Views with per-item permissions================================
def has_access(user, location, role=EDITOR_ROLE_NAME): def has_access(user, location, role=STAFF_ROLE_NAME):
'''Return True if user allowed to access this piece of data''' '''
'''Note that the CMS permissions model is with respect to courses''' Return True if user allowed to access this piece of data
'''There is a super-admin permissions if user.is_staff is set''' Note that the CMS permissions model is with respect to courses
return is_user_in_course_group_role(user, get_course_location_for_item(location), role) There is a super-admin permissions if user.is_staff is set
Also, since we're unifying the user database between LMS and CAS,
I'm presuming that the course instructor (formally known as admin)
will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR
has all the rights that STAFF do
'''
course_location = get_course_location_for_item(location)
_has_access = is_user_in_course_group_role(user, course_location, role)
# if we're not in STAFF, perhaps we're in INSTRUCTOR groups
if not _has_access and role == STAFF_ROLE_NAME:
_has_access = is_user_in_course_group_role(user, course_location, INSTRUCTOR_ROLE_NAME)
return _has_access
@login_required @login_required
...@@ -591,15 +602,16 @@ This view will return all CMS users who are editors for the specified course ...@@ -591,15 +602,16 @@ This view will return all CMS users who are editors for the specified course
''' '''
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def manage_users(request, org, course, name): def manage_users(request, location):
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item # check that logged in user has permissions to this item
if not has_access(request.user, location, role=ADMIN_ROLE_NAME): if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied() raise PermissionDenied()
return render_to_response('manage_users.html', { return render_to_response('manage_users.html', {
'editors': get_users_in_course_group_by_role(location, EDITOR_ROLE_NAME) 'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
'add_user_postback_url' : reverse('add_user', args=[location]).rstrip('/'),
'remove_user_postback_url' : reverse('remove_user', args=[location]).rstrip('/')
}) })
...@@ -615,18 +627,17 @@ def create_json_response(errmsg = None): ...@@ -615,18 +627,17 @@ def create_json_response(errmsg = None):
This POST-back view will add a user - specified by email - to the list of editors for This POST-back view will add a user - specified by email - to the list of editors for
the specified course the specified course
''' '''
@expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def add_user(request, org, course, name): def add_user(request, location):
email = request.POST["email"] email = request.POST["email"]
if email=='': if email=='':
return create_json_response('Please specify an email address.') return create_json_response('Please specify an email address.')
location = ['i4x', org, course, 'course', name]
# check that logged in user has admin permissions to this course # check that logged in user has admin permissions to this course
if not has_access(request.user, location, role=ADMIN_ROLE_NAME): if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied() raise PermissionDenied()
user = get_user_by_email(email) user = get_user_by_email(email)
...@@ -640,7 +651,7 @@ def add_user(request, org, course, name): ...@@ -640,7 +651,7 @@ def add_user(request, org, course, name):
return create_json_response('User {0} has registered but has not yet activated his/her account.'.format(email)) return create_json_response('User {0} has registered but has not yet activated his/her account.'.format(email))
# ok, we're cool to add to the course group # ok, we're cool to add to the course group
add_user_to_course_group(request.user, user, location, EDITOR_ROLE_NAME) add_user_to_course_group(request.user, user, location, STAFF_ROLE_NAME)
return create_json_response() return create_json_response()
...@@ -648,22 +659,21 @@ def add_user(request, org, course, name): ...@@ -648,22 +659,21 @@ def add_user(request, org, course, name):
This POST-back view will remove a user - specified by email - from the list of editors for This POST-back view will remove a user - specified by email - from the list of editors for
the specified course the specified course
''' '''
@expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def remove_user(request, org, course, name): def remove_user(request, location):
email = request.POST["email"] email = request.POST["email"]
location = ['i4x', org, course, 'course', name]
# check that logged in user has admin permissions on this course # check that logged in user has admin permissions on this course
if not has_access(request.user, location, role=ADMIN_ROLE_NAME): if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied() raise PermissionDenied()
user = get_user_by_email(email) user = get_user_by_email(email)
if user is None: if user is None:
return create_json_response('Could not find user by email address \'{0}\'.'.format(email)) return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
remove_user_from_course_group(request.user, user, location, EDITOR_ROLE_NAME) remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME)
return create_json_response() return create_json_response()
......
...@@ -42,7 +42,7 @@ CONTENTSTORE = { ...@@ -42,7 +42,7 @@ CONTENTSTORE = {
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "cms.db", 'NAME': ENV_ROOT / "db" / "mitx.db",
} }
} }
...@@ -97,3 +97,6 @@ CACHES = { ...@@ -97,3 +97,6 @@ CACHES = {
# Make the keyedcache startup warnings go away # Make the keyedcache startup warnings go away
CACHE_TIMEOUT = 0 CACHE_TIMEOUT = 0
# Dummy secret key for dev
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="title">Course Editor Manager</%block> <%block name="title">Course Staff Manager</%block>
<%include file="widgets/header.html"/> <%include file="widgets/header.html"/>
<%block name="content"> <%block name="content">
<section class="main-container"> <section class="main-container">
<h2>Course Editors</h2> <h2>Course Staff</h2>
<ul> <div>
% for user in editors: <p>
<li>${user.email} (${user.username})</li> The following list of users have been designated as course staff. This means that these users will
have permissions to modify course content. You may add additional source staff below. Please note that they
must have already registered and verified their account.
</p>
</div>
<div>
<ol>
% for user in staff:
<li>${user.email} (${user.username}) <a href='#' class='remove-user' data-id='${user.email}'>remove</a></li>
% endfor % endfor
</ul> </ol>
</div>
<label>email:&nbsp;</label><input type="text" id="email" autocomplete="Off" placeholder="email@example.com..." size="40"/>
<a href="#" id="add_user" class="button" />add as course staff</a>
<form action="add_user" id="addEditorsForm">
<label>email:&nbsp;</label><input type="text" name="email" placeholder="email@example.com..." />
<input type="submit" value="add editor" />
</form>
<div id="result"></div> <div id="result"></div>
<script> </section>
$("#addEditorsForm").submit(function(event) { </%block>
event.preventDefault();
var $form = $(this), <%block name="jsextra">
email = $form.find('input[name="email"]').val(),
url = $form.attr('action'); <script type="text/javascript">
$.post(url, {email:email}, $(document).ready(function() {
function(data) { $("#add_user").click(function() {
if(data['Status'] != 'OK') $.ajax({
$("#result").empty().append(data['ErrMsg']); url: "${add_user_postback_url}",
type: "POST",
dataType: "json",
contentType: "application/json",
data:JSON.stringify({ 'email': $('#email').val()}),
}).done(function(data) {
if (data.ErrMsg != undefined)
$("#result").empty().append(data.ErrMsg);
else else
location.reload(); location.reload();
})
})
$(".remove-user").click(function() {
$.ajax({
url: "${remove_user_postback_url}",
type: "POST",
dataType: "json",
contentType: "application/json",
data:JSON.stringify({ 'email': $(this).data('id')}),
}).done(function() {
location.reload();
})
}); });
}); });
</script> </script>
</section>
</%block> </%block>
\ No newline at end of file
...@@ -22,10 +22,11 @@ urlpatterns = ('', ...@@ -22,10 +22,11 @@ urlpatterns = ('',
'contentstore.views.preview_dispatch', name='preview_dispatch'), 'contentstore.views.preview_dispatch', name='preview_dispatch'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)/upload_asset$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)/upload_asset$',
'contentstore.views.upload_asset', name='upload_asset'), 'contentstore.views.upload_asset', name='upload_asset'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/manage_users$', url(r'^manage_users/(?P<location>.*?)$', 'contentstore.views.manage_users', name='manage_users'),
'contentstore.views.manage_users', name='manage_users'), url(r'^add_user/(?P<location>.*?)$',
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/add_user$',
'contentstore.views.add_user', name='add_user'), 'contentstore.views.add_user', name='add_user'),
url(r'^remove_user/(?P<location>.*?)$',
'contentstore.views.remove_user', name='remove_user'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/remove_user$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/remove_user$',
'contentstore.views.remove_user', name='remove_user'), 'contentstore.views.remove_user', name='remove_user'),
url(r'^assets/(?P<location>.*?)$', 'contentstore.views.asset_index', name='asset_index'), url(r'^assets/(?P<location>.*?)$', 'contentstore.views.asset_index', name='asset_index'),
......
...@@ -6,6 +6,7 @@ import logging ...@@ -6,6 +6,7 @@ import logging
import time import time
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Group
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
...@@ -306,13 +307,25 @@ def _dispatch(table, action, user, obj): ...@@ -306,13 +307,25 @@ def _dispatch(table, action, user, obj):
raise ValueError("Unknown action for object type '{0}': '{1}'".format( raise ValueError("Unknown action for object type '{0}': '{1}'".format(
type(obj), action)) type(obj), action))
def _does_course_group_name_exist(name):
return len(Group.objects.filter(name = name)) > 0
def _course_staff_group_name(location): def _course_staff_group_name(location):
""" """
Get the name of the staff group for a location. Right now, that's staff_COURSE. Get the name of the staff group for a location. Right now, that's staff_COURSE.
location: something that can passed to Location. location: something that can passed to Location.
cdodge: We're changing the name convention of the group to better epxress different runs of courses by
using course_id rather than just the course number. So first check to see if the group name exists
""" """
return 'staff_%s' % Location(location).course loc = Location(location)
legacy_name = 'staff_%s' % loc.course
if _does_course_group_name_exist(legacy_name):
return legacy_name
return 'staff_%s' & loc.course_id
def _course_instructor_group_name(location): def _course_instructor_group_name(location):
...@@ -321,8 +334,16 @@ def _course_instructor_group_name(location): ...@@ -321,8 +334,16 @@ def _course_instructor_group_name(location):
A course instructor has all staff privileges, but also can manage list of course staff (add, remove, list). A course instructor has all staff privileges, but also can manage list of course staff (add, remove, list).
location: something that can passed to Location. location: something that can passed to Location.
cdodge: We're changing the name convention of the group to better epxress different runs of courses by
using course_id rather than just the course number. So first check to see if the group name exists
""" """
return 'instructor_%s' % Location(location).course loc = Location(location)
legacy_name = 'instructor_%s' % loc.course
if _does_course_group_name_exist(legacy_name):
return legacy_name
return 'instructor_%s' % loc.course_id
def _has_global_staff_access(user): def _has_global_staff_access(user):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment