diff --git a/cms/djangoapps/auth/__init__.py b/cms/djangoapps/auth/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py
new file mode 100644
index 0000000000..fec25c5ba2
--- /dev/null
+++ b/cms/djangoapps/auth/authz.py
@@ -0,0 +1,94 @@
+import logging
+import sys
+
+from django.contrib.auth.models import User, Group
+from django.core.exceptions import PermissionDenied
+
+from xmodule.modulestore import Location
+
+# define a couple of simple roles, we just need ADMIN and EDITOR now for our purposes
+ADMIN_ROLE_NAME = 'admin'
+EDITOR_ROLE_NAME = 'editor'
+
+# 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()
+
+
+'''
+Create all permission groups for a new course and subscribe the caller into those roles
+'''
+def create_all_course_groups(creator, location):
+ create_new_course_group(creator, location, ADMIN_GROUP_NAME)
+ create_new_course_group(creator, location, EDITOR_GROUP_NAME)
+
+
+def create_new_course_group(creator, location, role):
+ groupname = get_course_groupname_for_role(location, role)
+ (group, created) =Group.get_or_create(name=groupname)
+ if created:
+ group.save()
+
+ creator.groups.add(group)
+ creator.save()
+
+ return
+
+
+def add_user_to_course_group(caller, user, location, role):
+ # only admins can add/remove other users
+ if not is_user_in_course_group_role(caller, location, ADMIN_ROLE_NAME):
+ raise PermissionDenied
+
+ if user.is_active and user.is_authenticated:
+ groupname = get_course_groupname_for_role(location, role)
+
+ group = Group.objects.get(name=groupname)
+ user.groups.add(group)
+ user.save()
+ return True
+
+ return False
+
+
+def get_user_by_email(email):
+ 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):
+ # only admins can add/remove other users
+ if not is_user_in_course_group_role(caller, location, ADMIN_ROLE_NAME):
+ raise PermissionDenied
+
+ # see if the user is actually in that role, if not then we don't have to do anything
+ if is_user_in_course_group_role(user, location, role) == True:
+ groupname = get_course_groupname_for_role(location, role)
+
+ group = Group.objects.get(name=groupname)
+ 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
+
+
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
new file mode 100644
index 0000000000..fc801ac684
--- /dev/null
+++ b/cms/djangoapps/contentstore/utils.py
@@ -0,0 +1,31 @@
+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
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index 4a6cb4787b..40013c178b 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -5,6 +5,7 @@ import logging
import sys
import mimetypes
import StringIO
+import exceptions
from collections import defaultdict
from uuid import uuid4
@@ -13,6 +14,7 @@ from PIL import Image
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
from django.contrib.auth.decorators import login_required
+from django.core.exceptions import PermissionDenied
from django.core.context_processors import csrf
from django_future.csrf import ensure_csrf_cookie
from django.core.urlresolvers import reverse
@@ -37,9 +39,11 @@ from operator import attrgetter
from xmodule.contentstore.django import contentstore
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 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 ADMIN_ROLE_NAME, EDITOR_ROLE_NAME
+from .utils import get_course_location_for_item
log = logging.getLogger(__name__)
@@ -76,6 +80,10 @@ def index(request):
List all courses available to the logged in user
"""
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', {
'courses': [(course.metadata.get('display_name'),
reverse('course_index', args=[
@@ -88,10 +96,10 @@ def index(request):
# ==== Views with per-item permissions================================
-def has_access(user, location):
+def has_access(user, location, role=EDITOR_ROLE_NAME):
'''Return True if user allowed to access this piece of data'''
- # TODO (vshnayder): actually check perms
- return user.is_active and user.is_authenticated
+ '''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)
@login_required
@@ -103,8 +111,10 @@ def course_index(request, org, course, name):
org, course, name: Attributes of the Location for the item to edit
"""
location = ['i4x', org, course, 'course', name]
+
+ # check that logged in user has permissions to this item
if not has_access(request.user, location):
- raise Http404 # TODO (vshnayder): better error
+ raise PermissionDenied()
upload_asset_callback_url = reverse('upload_asset', kwargs = {
'org' : org,
@@ -130,9 +140,9 @@ def edit_unit(request, location):
id: A Location URL
"""
- # TODO (vshnayder): change name from id to location in coffee+html as well.
+ # check that we have permissions to edit this item
if not has_access(request.user, location):
- raise Http404 # TODO (vshnayder): better error
+ raise PermissionDenied()
item = modulestore().get_item(location)
@@ -361,8 +371,10 @@ def get_module_previews(request, descriptor):
@expect_json
def save_item(request):
item_location = request.POST['id']
+
+ # check permissions for this user within this course
if not has_access(request.user, item_location):
- raise Http404 # TODO (vshnayder): better error
+ raise PermissionDenied()
if request.POST['data']:
data = request.POST['data']
@@ -400,7 +412,7 @@ def clone_item(request):
template = Location(request.POST['template'])
if not has_access(request.user, parent_location):
- raise Http404 # TODO (vshnayder): better error
+ raise PermissionDenied()
parent = modulestore().get_item(parent_location)
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
@@ -504,3 +516,84 @@ def upload_asset(request, org, course, coursename):
logging.error('Failed to generate thumbnail for {0}. Continuing...'.format(name))
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, role=ADMIN_ROLE_NAME):
+ raise PermissionDenied()
+
+ return render_to_response('manage_users.html', {
+ 'editors': get_users_in_course_group_by_role(location, EDITOR_ROLE_NAME)
+ })
+
+
+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 admin permissions to this course
+ if not has_access(request.user, location, role=ADMIN_ROLE_NAME):
+ 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_ROLE_NAME)
+
+ 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 admin permissions on this course
+ if not has_access(request.user, location, role=ADMIN_ROLE_NAME):
+ raise PermissionDenied()
+
+ user = get_user_by_email(email)
+ if user is None:
+ 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)
+
+ return create_json_response()
diff --git a/cms/envs/common.py b/cms/envs/common.py
index 3832006aba..6f7a462da2 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -282,6 +282,7 @@ INSTALLED_APPS = (
# For CMS
'contentstore',
+ 'auth',
'github_sync',
'student', # misleading name due to sharing with lms
diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html
new file mode 100644
index 0000000000..3887b4cbcb
--- /dev/null
+++ b/cms/templates/manage_users.html
@@ -0,0 +1,38 @@
+<%inherit file="base.html" />
+<%block name="title">Course Editor Manager%block>
+<%include file="widgets/header.html"/>
+
+<%block name="content">
+
+
+ Course Editors
+
+ % for user in editors:
+ - ${user.email} (${user.username})
+ % endfor
+
+
+
+
+
+
+
+
+%block>
diff --git a/cms/urls.py b/cms/urls.py
index 8f84273bc1..94b247ee54 100644
--- a/cms/urls.py
+++ b/cms/urls.py
@@ -20,7 +20,14 @@ urlpatterns = ('',
url(r'^preview/modx/(?P[^/]*)/(?P.*?)/(?P[^/]*)$',
'contentstore.views.preview_dispatch', name='preview_dispatch'),
url(r'^(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)/upload_asset$',
- 'contentstore.views.upload_asset', name='upload_asset')
+ 'contentstore.views.upload_asset', name='upload_asset'),
+ url(r'^(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)/manage_users$',
+ 'contentstore.views.manage_users', name='manage_users'),
+ url(r'^(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)/add_user$',
+ 'contentstore.views.add_user', name='add_user'),
+ url(r'^(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)/remove_user$',
+ 'contentstore.views.remove_user', name='remove_user')
+
)
# User creation and updating views