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 +<%include file="widgets/header.html"/> + +<%block name="content"> +
+ +

Course Editors

+ + +
+ + +
+
+ + + +
+ 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