Commit 27016465 by Chris Dodge

support some simple authorization scheme to view/edit courses

parent 67fb486f
import logging
import sys
from django.contrib.auth.models import User, Group
from xmodule.modulestore import Location
# 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_course_groupname_for_role(location, role):
loc = Location(location)
groupname = loc.course_id + ':' + role
return groupname
def get_users_in_course_group_by_role(location, role):
groupname = get_course_groupname_for_role(location, role)
group = Group.objects.get(name=groupname)
return group.user_set.all()
def add_user_to_course_group(caller, user, location, role):
# @todo: make sure caller has 'admin' permissions in the course
if user.is_active and user.is_authenticated:
groupname = get_course_groupname_for_role(location, role)
# see if the group exists, or create if new
(group, created) = Group.objects.get_or_create(name=groupname)
if created:
# if newly created, then we have to save it
group.save()
user.groups.add(group)
user.save()
return True
return False
def get_user_by_email(email):
user = None
# try to look up user
try:
user = User.objects.get(email=email)
except:
pass
return user
def remove_user_from_course_group(caller, user, location, role):
# @todo: make sure caller has 'admin' permissions in the course
if is_user_in_course_group_role(user, location, role) == True:
groupname = get_course_groupname_for_role(location, role)
# make sure the group actually exists
group = Group.objects.get(name=groupname)
if group is not None:
user.groups.remove(group)
user.save()
def is_user_in_course_group_role(user, location, role):
if user.is_active and user.is_authenticated:
return user.groups.filter(name=get_course_groupname_for_role(location,role)).count() > 0
return False
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
'''
cdodge: for a given Xmodule, return the course that it belongs to
NOTE: This makes a lot of assumptions about the format of the course location
Also we have to assert that this module maps to only one course item - it'll throw an
assert if not
'''
def get_course_location_for_item(location):
item_loc = Location(location)
# check to see if item is already a course, if so we can skip this
if item_loc.category != 'course':
# @hack! We need to find the course location however, we don't
# know the 'name' parameter in this context, so we have
# to assume there's only one item in this query even though we are not specifying a name
course_search_location = ['i4x', item_loc.org, item_loc.course, 'course', None]
courses = modulestore().get_items(course_search_location)
# make sure we found exactly one match on this above course search
found_cnt = len(courses)
if found_cnt == 0:
raise BaseException('Could not find course at {0}'.format(course_search_location))
if found_cnt > 1:
raise BaseException('Found more than one course at {0}. There should only be one!!!'.format(course_search_location))
location = courses[0].location
return location
...@@ -5,6 +5,7 @@ import logging ...@@ -5,6 +5,7 @@ import logging
import sys import sys
import mimetypes import mimetypes
import StringIO import StringIO
import exceptions
from collections import defaultdict from collections import defaultdict
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz' # to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
...@@ -12,6 +13,7 @@ from PIL import Image ...@@ -12,6 +13,7 @@ from PIL import Image
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -36,9 +38,10 @@ from operator import attrgetter ...@@ -36,9 +38,10 @@ from operator import attrgetter
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
#from django.core.cache import cache
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 get_user_by_email, add_user_to_course_group
from .utils import get_course_location_for_item
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -72,6 +75,10 @@ def index(request): ...@@ -72,6 +75,10 @@ def index(request):
List all courses available to the logged in user List all courses available to the logged in user
""" """
courses = modulestore().get_items(['i4x', None, None, 'course', None]) courses = modulestore().get_items(['i4x', None, None, 'course', None])
# filter out courses that we don't have access to
courses = filter(lambda course: has_access(request.user, course.location), courses)
return render_to_response('index.html', { return render_to_response('index.html', {
'courses': [(course.metadata.get('display_name'), 'courses': [(course.metadata.get('display_name'),
reverse('course_index', args=[ reverse('course_index', args=[
...@@ -84,10 +91,10 @@ def index(request): ...@@ -84,10 +91,10 @@ def index(request):
# ==== Views with per-item permissions================================ # ==== Views with per-item permissions================================
def has_access(user, location): def has_access(user, location, role='editor'):
'''Return True if user allowed to access this piece of data''' '''Return True if user allowed to access this piece of data'''
# TODO (vshnayder): actually check perms '''Note that the CMS permissions model is with respect to courses'''
return user.is_active and user.is_authenticated return is_user_in_course_group_role(user, get_course_location_for_item(location), role)
@login_required @login_required
...@@ -99,8 +106,10 @@ def course_index(request, org, course, name): ...@@ -99,8 +106,10 @@ def course_index(request, org, course, name):
org, course, name: Attributes of the Location for the item to edit org, course, name: Attributes of the Location for the item to edit
""" """
location = ['i4x', org, course, 'course', name] location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location): if not has_access(request.user, location):
raise Http404 # TODO (vshnayder): better error raise PermissionDenied()
# TODO (cpennington): These need to be read in from the active user # TODO (cpennington): These need to be read in from the active user
_course = modulestore().get_item(location) _course = modulestore().get_item(location)
...@@ -134,10 +143,12 @@ def edit_item(request): ...@@ -134,10 +143,12 @@ def edit_item(request):
id: A Location URL id: A Location URL
""" """
# TODO (vshnayder): change name from id to location in coffee+html as well.
item_location = request.GET['id'] item_location = request.GET['id']
# check that we have permissions to edit this item
if not has_access(request.user, item_location): if not has_access(request.user, item_location):
raise Http404 # TODO (vshnayder): better error raise PermissionDenied()
item = modulestore().get_item(item_location) item = modulestore().get_item(item_location)
item.get_html = wrap_xmodule(item.get_html, item, "xmodule_edit.html") item.get_html = wrap_xmodule(item.get_html, item, "xmodule_edit.html")
...@@ -359,8 +370,10 @@ def get_module_previews(request, descriptor): ...@@ -359,8 +370,10 @@ def get_module_previews(request, descriptor):
@expect_json @expect_json
def save_item(request): def save_item(request):
item_location = request.POST['id'] item_location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, item_location): if not has_access(request.user, item_location):
raise Http404 # TODO (vshnayder): better error raise PermissionDenied()
if request.POST['data']: if request.POST['data']:
data = request.POST['data'] data = request.POST['data']
...@@ -407,7 +420,7 @@ def clone_item(request): ...@@ -407,7 +420,7 @@ def clone_item(request):
display_name = request.POST['name'] display_name = request.POST['name']
if not has_access(request.user, parent_location): if not has_access(request.user, parent_location):
raise Http404 # TODO (vshnayder): better error raise PermissionDenies()
parent = modulestore().get_item(parent_location) parent = modulestore().get_item(parent_location)
dest_location = parent_location._replace(category=template.category, name=Location.clean_for_url_name(display_name)) dest_location = parent_location._replace(category=template.category, name=Location.clean_for_url_name(display_name))
...@@ -513,3 +526,82 @@ def upload_asset(request, org, course, coursename): ...@@ -513,3 +526,82 @@ def upload_asset(request, org, course, coursename):
return HttpResponse('Upload completed') return HttpResponse('Upload completed')
'''
This view will return all CMS users who are editors for the specified course
'''
@login_required
@ensure_csrf_cookie
def manage_users(request, org, course, name):
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
return render_to_response('manage_users.html', {
'editors': get_users_in_course_group_by_role(location, 'editor')
})
def create_json_response(errmsg = None):
if errmsg is not None:
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg' : errmsg}))
else:
resp = HttpResponse(json.dumps({'Status': 'OK'}))
return resp
'''
This POST-back view will add a user - specified by email - to the list of editors for
the specified course
'''
@login_required
@ensure_csrf_cookie
def add_user(request, org, course, name):
email = request.POST["email"]
if email=='':
return create_json_response('Please specify an email address.')
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
user = get_user_by_email(email)
# user doesn't exist?!? Return error.
if user is None:
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
# user exists, but hasn't activated account?!?
if not user.is_active:
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
add_user_to_course_group(request.user, user, location, 'editor')
return create_json_response()
'''
This POST-back view will remove a user - specified by email - from the list of editors for
the specified course
'''
@login_required
@ensure_csrf_cookie
def remove_user(request, org, course, name):
email = request.POST["email"]
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
user = get_user_by_email(email)
if user is not None:
remove_user_from_course_group(request.user, user, location, 'editor')
return create_json_response()
...@@ -325,6 +325,7 @@ INSTALLED_APPS = ( ...@@ -325,6 +325,7 @@ INSTALLED_APPS = (
# For CMS # For CMS
'contentstore', 'contentstore',
'auth',
'github_sync', 'github_sync',
'student', # misleading name due to sharing with lms 'student', # misleading name due to sharing with lms
......
<%inherit file="base.html" />
<%block name="title">Course Editor Manager</%block>
<%include file="widgets/header.html"/>
<%block name="content">
<section class="main-container">
<h2>Course Editors</h2>
<ul>
% for user in editors:
<li>${user.email} (${user.username})</li>
% endfor
</ul>
<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>
<script>
$("#addEditorsForm").submit(function(event) {
event.preventDefault();
var $form = $(this),
email = $form.find('input[name="email"]').val(),
url = $form.attr('action');
$.post(url, {email:email},
function(data) {
if(data['Status'] != 'OK')
$("#result").empty().append(data['ErrMsg']);
else
location.reload();
});
});
</script>
</section>
</%block>
...@@ -20,6 +20,12 @@ urlpatterns = ('', ...@@ -20,6 +20,12 @@ 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$',
'contentstore.views.manage_users', name='manage_users'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/add_user$',
'contentstore.views.add_user', name='add_user'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/remove_user$',
'contentstore.views.add_user', name='remove_user')
) )
# User creation and updating views # User creation and updating views
......
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