diff --git a/cms/djangoapps/contentstore/features/course-team.feature b/cms/djangoapps/contentstore/features/course-team.feature
deleted file mode 100644
index 05a5900..0000000
--- a/cms/djangoapps/contentstore/features/course-team.feature
+++ /dev/null
@@ -1,89 +0,0 @@
-@shard_2
-Feature: CMS.Course Team
-    As a course author, I want to be able to add others to my team
-
-    Scenario: Admins can add other users
-        Given I have opened a new course in Studio
-        And the user "alice" exists
-        And I am viewing the course team settings
-        When I add "alice" to the course team
-        And "alice" logs in
-        Then she does see the course on her page
-
-    Scenario: Added admins cannot delete or add other users
-        Given I have opened a new course in Studio
-        And the user "bob" exists
-        And I am viewing the course team settings
-        When I add "bob" to the course team
-        And "bob" logs in
-        And he selects the new course
-        And he views the course team settings
-        Then he cannot delete users
-        And he cannot add users
-
-    Scenario: Admins can delete other users
-        Given I have opened a new course in Studio
-        And the user "carol" exists
-        And I am viewing the course team settings
-        When I add "carol" to the course team
-        And I delete "carol" from the course team
-        And "carol" logs in
-        Then she does not see the course on her page
-
-    Scenario: Admins cannot add users that do not exist
-        Given I have opened a new course in Studio
-        And I am viewing the course team settings
-        When I add "dennis" to the course team
-        Then I should see "Could not find user by email address" somewhere on the page
-
-    Scenario: Admins should be able to make other people into admins
-        Given I have opened a new course in Studio
-        And the user "emily" exists
-        And I am viewing the course team settings
-        And I add "emily" to the course team
-        When I make "emily" a course team admin
-        And "emily" logs in
-        And she selects the new course
-        And she views the course team settings
-        Then "emily" should be marked as an admin
-        And she can add users
-        And she can delete users
-
-    Scenario: Admins should be able to remove other admins
-        Given I have opened a new course in Studio
-        And the user "frank" exists as a course admin
-        And I am viewing the course team settings
-        When I remove admin rights from "frank"
-        And "frank" logs in
-        And he selects the new course
-        And he views the course team settings
-        Then "frank" should not be marked as an admin
-        And he cannot add users
-        And he cannot delete users
-
-    Scenario: Admins should be able to give course ownership to someone else
-        Given I have opened a new course in Studio
-        And the user "gina" exists
-        And I am viewing the course team settings
-        When I add "gina" to the course team
-        And I make "gina" a course team admin
-        And I remove admin rights from myself
-        And "gina" logs in
-        And she selects the new course
-        And she views the course team settings
-        And she deletes me from the course team
-        And I am logged into studio
-        Then I do not see the course on my page
-
-    Scenario: Admins should be able to remove their own admin rights
-        Given I have opened a new course in Studio
-        And the user "harry" exists as a course admin
-        And I am viewing the course team settings
-        Then I should be marked as an admin
-        And I can add users
-        And I can delete users
-        When I remove admin rights from myself
-        Then I should not be marked as an admin
-        And I cannot add users
-        And I cannot delete users
-        And I cannot make myself a course team admin
diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py
deleted file mode 100644
index 130f05b..0000000
--- a/cms/djangoapps/contentstore/features/course-team.py
+++ /dev/null
@@ -1,124 +0,0 @@
-# pylint: disable=missing-docstring
-# pylint: disable=redefined-outer-name
-
-from lettuce import world, step
-from nose.tools import assert_in  # pylint: disable=no-name-in-module
-
-
-@step(u'(I am viewing|s?he views) the course team settings$')
-def view_grading_settings(_step, whom):
-    world.click_course_settings()
-    link_css = 'li.nav-course-settings-team a'
-    world.css_click(link_css)
-
-
-@step(u'I add "([^"]*)" to the course team$')
-def add_other_user(_step, name):
-    new_user_css = 'a.create-user-button'
-    world.css_click(new_user_css)
-
-    # Wait for the css animation to apply the is-shown class
-    shown_css = 'div.wrapper-create-user.is-shown'
-    world.wait_for_present(shown_css)
-
-    email_css = 'input#user-email-input'
-    world.css_fill(email_css, name + '@edx.org')
-    if world.is_firefox():
-        world.trigger_event(email_css)
-    confirm_css = 'form.create-user button.action-primary'
-    world.css_click(confirm_css)
-
-
-@step(u'I delete "([^"]*)" from the course team$')
-def delete_other_user(_step, name):
-    to_delete_css = '.user-item .item-actions a.remove-user[data-id="{email}"]'.format(
-        email="{0}{1}".format(name, '@edx.org'))
-    world.css_click(to_delete_css)
-    world.confirm_studio_prompt()
-
-
-@step(u's?he deletes me from the course team$')
-def other_delete_self(_step):
-    to_delete_css = '.user-item .item-actions a.remove-user[data-id="{email}"]'.format(
-        email="robot+studio@edx.org")
-    world.css_click(to_delete_css)
-    world.confirm_studio_prompt()
-
-
-@step(u'I make "([^"]*)" a course team admin$')
-def make_course_team_admin(_step, name):
-    admin_btn_css = '.user-item[data-email="{name}@edx.org"] .user-actions .add-admin-role'.format(
-        name=name)
-    world.css_click(admin_btn_css)
-
-
-@step(u'I remove admin rights from ("([^"]*)"|myself)$')
-def remove_course_team_admin(_step, outer_capture, name):
-    if outer_capture == "myself":
-        email = world.scenario_dict["USER"].email
-    else:
-        email = name + '@edx.org'
-    admin_btn_css = '.user-item[data-email="{email}"] .user-actions .remove-admin-role'.format(
-        email=email)
-    world.css_click(admin_btn_css)
-
-
-@step(u'I( do not)? see the course on my page$')
-@step(u's?he does( not)? see the course on (his|her) page$')
-def see_course(_step, do_not_see, gender='self'):
-    class_css = 'h3.course-title'
-    if do_not_see:
-        assert world.is_css_not_present(class_css)
-    else:
-        all_courses = world.css_find(class_css)
-        all_names = [item.html for item in all_courses]
-        assert_in(world.scenario_dict['COURSE'].display_name, all_names)
-
-
-@step(u'"([^"]*)" should( not)? be marked as an admin$')
-def marked_as_admin(_step, name, not_marked_admin):
-    flag_css = '.user-item[data-email="{name}@edx.org"] .flag-role.flag-role-admin'.format(
-        name=name)
-    if not_marked_admin:
-        assert world.is_css_not_present(flag_css)
-    else:
-        assert world.is_css_present(flag_css)
-
-
-@step(u'I should( not)? be marked as an admin$')
-def self_marked_as_admin(_step, not_marked_admin):
-    return marked_as_admin(_step, "robot+studio", not_marked_admin)
-
-
-@step(u'I can(not)? delete users$')
-@step(u's?he can(not)? delete users$')
-def can_delete_users(_step, can_not_delete):
-    to_delete_css = 'a.remove-user'
-    if can_not_delete:
-        assert world.is_css_not_present(to_delete_css)
-    else:
-        assert world.is_css_present(to_delete_css)
-
-
-@step(u'I can(not)? add users$')
-@step(u's?he can(not)? add users$')
-def can_add_users(_step, can_not_add):
-    add_css = 'a.create-user-button'
-    if can_not_add:
-        assert world.is_css_not_present(add_css)
-    else:
-        assert world.is_css_present(add_css)
-
-
-@step(u'I can(not)? make ("([^"]*)"|myself) a course team admin$')
-@step(u's?he can(not)? make ("([^"]*)"|me) a course team admin$')
-def can_make_course_admin(_step, can_not_make_admin, outer_capture, name):
-    if outer_capture == "myself":
-        email = world.scenario_dict["USER"].email
-    else:
-        email = name + '@edx.org'
-    add_button_css = '.user-item[data-email="{email}"] .add-admin-role'.format(email=email)
-    if can_not_make_admin:
-        assert world.is_css_not_present(add_button_css)
-    else:
-        assert world.is_css_present(add_button_css)
diff --git a/cms/djangoapps/contentstore/views/library.py b/cms/djangoapps/contentstore/views/library.py
index 0a7a0e2..a5aa09a 100644
--- a/cms/djangoapps/contentstore/views/library.py
+++ b/cms/djangoapps/contentstore/views/library.py
@@ -24,6 +24,7 @@ from opaque_keys.edx.locator import LibraryLocator, LibraryUsageLocator
 from xmodule.modulestore.exceptions import DuplicateCourseError
 from xmodule.modulestore import ModuleStoreEnum
 from xmodule.modulestore.django import modulestore
+from .user import user_with_role
 
 from .component import get_component_templates, CONTAINER_TEMPATES
 from student.auth import (
@@ -220,14 +221,18 @@ def manage_library_users(request, library_key_string):
     instructors = set(CourseInstructorRole(library_key).users_with_role())
     staff = set(CourseStaffRole(library_key).users_with_role()) - instructors
     users = set(LibraryUserRole(library_key).users_with_role()) - instructors - staff
-    all_users = instructors | staff | users
+
+    formatted_users = []
+    for user in instructors:
+        formatted_users.append(user_with_role(user, 'instructor'))
+    for user in staff:
+        formatted_users.append(user_with_role(user, 'staff'))
+    for user in users:
+        formatted_users.append(user_with_role(user, 'library_user'))
 
     return render_to_response('manage_users_lib.html', {
         'context_library': library,
-        'staff': staff,
-        'instructors': instructors,
-        'users': users,
-        'all_users': all_users,
+        'users': formatted_users,
         'allow_actions': bool(user_perms & STUDIO_EDIT_ROLES),
         'library_key': unicode(library_key),
         'lib_users_url': reverse_library_url('manage_library_users', library_key_string),
diff --git a/cms/djangoapps/contentstore/views/tests/test_checklists.py b/cms/djangoapps/contentstore/views/tests/test_checklists.py
index b86a78c..17ce2e8 100644
--- a/cms/djangoapps/contentstore/views/tests/test_checklists.py
+++ b/cms/djangoapps/contentstore/views/tests/test_checklists.py
@@ -136,7 +136,7 @@ class ChecklistTestCase(CourseTestCase):
             # Verify no side effect in the original list.
             self.assertEqual(get_action_url(checklist, index), stored)
 
-        test_expansion(self.course.checklists[0], 0, 'ManageUsers', '/course_team/mitX/333/Checklists_Course/')
+        test_expansion(self.course.checklists[0], 0, 'ManageUsers', '/course_team/mitX/333/Checklists_Course')
         test_expansion(self.course.checklists[1], 1, 'CourseOutline', '/course/mitX/333/Checklists_Course')
         test_expansion(self.course.checklists[2], 0, 'http://help.edge.edx.org/', 'http://help.edge.edx.org/')
 
diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py
index 93a7c66..10373a8 100644
--- a/cms/djangoapps/contentstore/views/user.py
+++ b/cms/djangoapps/contentstore/views/user.py
@@ -61,6 +61,16 @@ def course_team_handler(request, course_key_string=None, email=None):
         return HttpResponseNotFound()
 
 
+def user_with_role(user, role):
+    """ Build user representation with attached role """
+    return {
+        'id': user.id,
+        'username': user.username,
+        'email': user.email,
+        'role': role
+    }
+
+
 def _manage_users(request, course_key):
     """
     This view will return all CMS users who are editors for the specified course
@@ -71,14 +81,20 @@ def _manage_users(request, course_key):
         raise PermissionDenied()
 
     course_module = modulestore().get_course(course_key)
-    instructors = CourseInstructorRole(course_key).users_with_role()
+    instructors = set(CourseInstructorRole(course_key).users_with_role())
     # the page only lists staff and assumes they're a superset of instructors. Do a union to ensure.
     staff = set(CourseStaffRole(course_key).users_with_role()).union(instructors)
 
+    formatted_users = []
+    for user in instructors:
+        formatted_users.append(user_with_role(user, 'instructor'))
+    for user in staff - instructors:
+        formatted_users.append(user_with_role(user, 'staff'))
+
     return render_to_response('manage_users.html', {
         'context_course': course_module,
-        'staff': staff,
-        'instructors': instructors,
+        'show_transfer_ownership_hint': request.user in instructors and len(instructors) == 1,
+        'users': formatted_users,
         'allow_actions': bool(user_perms & STUDIO_EDIT_ROLES),
     })
 
diff --git a/cms/static/js/factories/manage_users.js b/cms/static/js/factories/manage_users.js
index 2f4e5b4..d15979e 100644
--- a/cms/static/js/factories/manage_users.js
+++ b/cms/static/js/factories/manage_users.js
@@ -1,203 +1,35 @@
-define(['jquery', 'underscore', 'gettext', 'js/views/feedback_prompt'], function($, _, gettext, PromptView) {
+/*
+    Code for editing users and assigning roles within a course team context.
+*/
+define(['underscore', 'gettext', 'js/views/manage_users_and_roles'],
+function(_, gettext, ManageUsersAndRoles) {
     'use strict';
-    return function (staffEmails, tplUserURL) {
-        var unknownErrorMessage = gettext('Unknown'),
-            $createUserForm = $('#create-user-form'),
-            $createUserFormWrapper = $createUserForm.closest('.wrapper-create-user'),
-            $cancelButton;
-
-        $createUserForm.bind('submit', function(event) {
-            event.preventDefault();
-            var email = $('#user-email-input').val().trim(),
-                url, msg;
-
-            if(!email) {
-                msg = new PromptView.Error({
-                    title: gettext('A valid email address is required'),
-                    message: gettext('You must enter a valid email address in order to add a new team member'),
-                    actions: {
-                        primary: {
-                            text: gettext('Return and add email address'),
-                            click: function(view) {
-                                view.hide();
-                                $('#user-email-input').focus();
-                            }
-                        }
-                    }
-                });
-                msg.show();
-            }
-
-            if(_.contains(staffEmails, email)) {
-                msg = new PromptView.Warning({
-                    title: gettext('Already a course team member'),
-                    message: _.template(
-                        gettext("{email} is already on the {course} team. Recheck the email address if you want to add a new member."), {
-                            email: email,
-                            course: course.escape('name')
-                        }, {interpolate: /\{(.+?)\}/g}
-                    ),
-                    actions: {
-                        primary: {
-                            text: gettext('Return to team listing'),
-                            click: function(view) {
-                                view.hide();
-                                $('#user-email-input').focus();
-                            }
-                        }
-                    }
-                });
-                msg.show();
-            }
-
-            url = tplUserURL.replace('@@EMAIL@@', $('#user-email-input').val().trim());
-            $.ajax({
-                url: url,
-                type: 'POST',
-                dataType: 'json',
-                contentType: 'application/json',
-                notifyOnError: false,
-                data: JSON.stringify({role: 'staff'}),
-                success: function(data) {location.reload();},
-                error: function(jqXHR, textStatus, errorThrown) {
-                    var message, prompt;
-                    try {
-                        message = JSON.parse(jqXHR.responseText).error || unknownErrorMessage;
-                    } catch (e) {
-                        message = unknownErrorMessage;
-                    }
-                    prompt = new PromptView.Error({
-                        title: gettext('Error adding user'),
-                        message: message,
-                        actions: {
-                            primary: {
-                                text: gettext('OK'),
-                                click: function(view) {
-                                    view.hide();
-                                    $('#user-email-input').focus();
-                                }
-                            }
-                        }
-                    });
-                    prompt.show();
-                }
-            });
-        });
-
-        $cancelButton = $createUserForm.find('.action-cancel');
-        $cancelButton.bind('click', function(event) {
-            event.preventDefault();
-            $('.create-user-button').toggleClass('is-disabled').attr('aria-disabled', $('.create-user-button').hasClass('is-disabled'));
-            $createUserFormWrapper.toggleClass('is-shown');
-            $('#user-email-input').val('');
-        });
-
-        $('.create-user-button').bind('click', function(event) {
-            event.preventDefault();
-            $('.create-user-button').toggleClass('is-disabled').attr('aria-disabled', $('.create-user-button').hasClass('is-disabled'));
-            $createUserFormWrapper.toggleClass('is-shown');
-            $createUserForm.find('#user-email-input').focus();
-        });
-
-        $('body').bind('keyup', function(event) {
-            if(event.which == 27) {
-                $cancelButton.click();
-            }
-        });
-
-        $('.remove-user').click(function() {
-            var email = $(this).data('id'),
-                msg = new PromptView.Warning({
-                    title: gettext('Are you sure?'),
-                    message: _.template(gettext('Are you sure you want to delete {email} from the course team for “{course}”?'), {email: email, course: course.get('name')}, {interpolate: /\{(.+?)\}/g}),
-                    actions: {
-                        primary: {
-                            text: gettext('Delete'),
-                            click: function(view) {
-                                var url = tplUserURL.replace('@@EMAIL@@', email);
-                                view.hide();
-                                $.ajax({
-                                    url: url,
-                                    type: 'DELETE',
-                                    dataType: 'json',
-                                    contentType: 'application/json',
-                                    notifyOnError: false,
-                                    success: function(data) {location.reload();},
-                                    error: function(jqXHR, textStatus, errorThrown) {
-                                        var message;
-                                        try {
-                                            message = JSON.parse(jqXHR.responseText).error || unknownErrorMessage;
-                                        } catch (e) {
-                                            message = unknownErrorMessage;
-                                        }
-                                        var prompt = new PromptView.Error({
-                                            title: gettext('Error removing user'),
-                                            message: message,
-                                            actions: {
-                                                primary: {
-                                                    text: gettext('OK'),
-                                                    click: function(view) {
-                                                        view.hide();
-                                                    }
-                                                }
-                                            }
-                                        });
-                                        prompt.show();
-                                    }
-                                });
-                            }
-                        },
-                        secondary: {
-                            text: gettext('Cancel'),
-                            click: function(view) {
-                                view.hide();
-                            }
-                        }
-                    }
-            });
-            msg.show();
-        });
-
-        $('.toggle-admin-role').click(function(event) {
-            event.preventDefault();
-            var type, url, role;
-            if($(this).hasClass('add-admin-role')) {
-                role = 'instructor';
-            } else {
-                role = 'staff';
-            }
-
-            url = $(this).closest('li[data-url]').data('url');
-            $.ajax({
-                url: url,
-                type: 'POST',
-                dataType: 'json',
-                contentType: 'application/json',
-                notifyOnError: false,
-                data: JSON.stringify({role: role}),
-                success: function(data) {location.reload();},
-                error: function(jqXHR, textStatus, errorThrown) {
-                    var message, prompt;
-                    try {
-                        message = JSON.parse(jqXHR.responseText).error || unknownErrorMessage;
-                    } catch (e) {
-                        message = unknownErrorMessage;
-                    }
-                    prompt = new PromptView.Error({
-                        title: gettext("There was an error changing the user's role"),
-                        message: message,
-                        actions: {
-                            primary: {
-                                text: gettext('Try Again'),
-                                click: function(view) {
-                                    view.hide();
-                                }
-                            }
-                        }
-                    });
-                    prompt.show();
-                }
-            });
-        });
+    return function (containerName, users, tplUserURL, current_user_id, allow_actions) {
+        function updateMessages(messages) {
+            var local_messages = _.extend({}, messages);
+            local_messages.alreadyMember.title = gettext('Already a course team member');
+            local_messages.deleteUser.messageTpl = gettext(
+                'Are you sure you want to delete {email} from the course team for “{container}”?'
+            );
+            return local_messages;
+        }
+        // Roles order are important: first role is considered initial role (the role added to user when (s)he's added
+        // Last role is considered an admin role (unrestricted access + ability to manage other users' permissions)
+        // Changing roles is performed in promote-demote fashion, so moves only to adjacent roles is allowed
+        var roles = [{key:'staff', name:gettext('Staff')}, {key:'instructor', 'name': gettext("Admin")}];
+
+        var options = {
+            el: $("#content"),
+            containerName: containerName,
+            tplUserURL: tplUserURL,
+            roles: roles,
+            users: users,
+            messages_modifier: updateMessages,
+            current_user_id: current_user_id,
+            allow_actions: allow_actions
+        };
+
+        var view = new ManageUsersAndRoles(options);
+        view.render();
     };
 });
diff --git a/cms/static/js/factories/manage_users_lib.js b/cms/static/js/factories/manage_users_lib.js
index 388ec56..f25bf8b 100644
--- a/cms/static/js/factories/manage_users_lib.js
+++ b/cms/static/js/factories/manage_users_lib.js
@@ -1,159 +1,39 @@
 /*
     Code for editing users and assigning roles within a library context.
 */
-define(['jquery', 'underscore', 'gettext', 'js/views/feedback_prompt', 'js/views/utils/view_utils'],
-function($, _, gettext, PromptView, ViewUtils) {
+define(['underscore', 'gettext', 'js/views/manage_users_and_roles'],
+function(_, gettext, ManageUsersAndRoles) {
     'use strict';
-    return function (libraryName, allUserEmails, tplUserURL) {
-        var unknownErrorMessage = gettext('Unknown'),
-            $createUserForm = $('#create-user-form'),
-            $createUserFormWrapper = $createUserForm.closest('.wrapper-create-user'),
-            $cancelButton;
-
-        // Our helper method that calls the RESTful API to add/remove/change user roles:
-        var changeRole = function(email, newRole, opts) {
-            var url = tplUserURL.replace('@@EMAIL@@', email);
-            var errMessage = opts.errMessage || gettext("There was an error changing the user's role");
-            var onSuccess = opts.onSuccess || function(data){ ViewUtils.reload(); };
-            var onError = opts.onError || function(){};
-            $.ajax({
-                url: url,
-                type: newRole ? 'POST' : 'DELETE',
-                dataType: 'json',
-                contentType: 'application/json',
-                notifyOnError: false,
-                data: JSON.stringify({role: newRole}),
-                success: onSuccess,
-                error: function(jqXHR, textStatus, errorThrown) {
-                    var message, prompt;
-                    try {
-                        message = JSON.parse(jqXHR.responseText).error || unknownErrorMessage;
-                    } catch (e) {
-                        message = unknownErrorMessage;
-                    }
-                    prompt = new PromptView.Error({
-                        title: errMessage,
-                        message: message,
-                        actions: {
-                            primary: { text: gettext('OK'), click: function(view) { view.hide(); onError(); } }
-                        }
-                    });
-                    prompt.show();
-                }
-            });
-        };
-
-        $createUserForm.bind('submit', function(event) {
-            event.preventDefault();
-            var email = $('#user-email-input').val().trim();
-            var msg;
-
-            if(!email) {
-                msg = new PromptView.Error({
-                    title: gettext('A valid email address is required'),
-                    message: gettext('You must enter a valid email address in order to add an instructor'),
-                    actions: {
-                        primary: {
-                            text: gettext('Return and add email address'),
-                            click: function(view) { view.hide(); $('#user-email-input').focus(); }
-                        }
-                    }
-                });
-                msg.show();
-                return;
-            }
-
-            if(_.contains(allUserEmails, email)) {
-                msg = new PromptView.Warning({
-                    title: gettext('Already a library team member'),
-                    message: _.template(
-                        gettext("{email} is already on the {course} team. Recheck the email address if you want to add a new member."), {
-                            email: email,
-                            course: libraryName
-                        }, {interpolate: /\{(.+?)\}/g}
-                    ),
-                    actions: {
-                        primary: {
-                            text: gettext('Return to team listing'),
-                            click: function(view) { view.hide(); $('#user-email-input').focus(); }
-                        }
-                    }
-                });
-                msg.show();
-                return;
-            }
-
-            // Use the REST API to create the user, giving them a role of "library_user" for now:
-            changeRole(
-                $('#user-email-input').val().trim(),
-                'library_user',
-                {
-                    errMessage: gettext('Error adding user'),
-                    onError: function() { $('#user-email-input').focus(); }
-                }
+    return function (containerName, users, tplUserURL, current_user_id, allow_actions) {
+        function updateMessages(messages) {
+            var local_messages = _.extend({}, messages);
+            local_messages.alreadyMember.title = gettext('Already a library team member');
+            local_messages.deleteUser.messageTpl = gettext(
+                'Are you sure you want to delete {email} from the library “{container}”?'
             );
-        });
-
-        $cancelButton = $createUserForm.find('.action-cancel');
-        $cancelButton.on('click', function(event) {
-            event.preventDefault();
-            $('.create-user-button').toggleClass('is-disabled');
-            $createUserFormWrapper.toggleClass('is-shown');
-            $('#user-email-input').val('');
-        });
-
-        $('.create-user-button').on('click', function(event) {
-            event.preventDefault();
-            $('.create-user-button').toggleClass('is-disabled');
-            $createUserFormWrapper.toggleClass('is-shown');
-            $createUserForm.find('#user-email-input').focus();
-        });
-
-        $('body').on('keyup', function(event) {
-            if(event.which == jQuery.ui.keyCode.ESCAPE && $createUserFormWrapper.is('.is-shown')) {
-                $cancelButton.click();
-            }
-        });
-
-        $('.remove-user').click(function() {
-            var email = $(this).closest('li[data-email]').data('email'),
-                msg = new PromptView.Warning({
-                    title: gettext('Are you sure?'),
-                    message: _.template(gettext('Are you sure you want to delete {email} from the library “{library}”?'), {email: email, library: libraryName}, {interpolate: /\{(.+?)\}/g}),
-                    actions: {
-                        primary: {
-                            text: gettext('Delete'),
-                            click: function(view) {
-                                // User the REST API to delete the user:
-                                changeRole(email, null, { errMessage: gettext('Error removing user') });
-                            }
-                        },
-                        secondary: {
-                            text: gettext('Cancel'),
-                            click: function(view) { view.hide(); }
-                        }
-                    }
-            });
-            msg.show();
-        });
-
-        $('.user-actions .make-instructor').click(function(event) {
-            event.preventDefault();
-            var email = $(this).closest('li[data-email]').data('email');
-            changeRole(email, 'instructor', {});
-        });
-
-        $('.user-actions .make-staff').click(function(event) {
-            event.preventDefault();
-            var email = $(this).closest('li[data-email]').data('email');
-            changeRole(email, 'staff', {});
-        });
-
-        $('.user-actions .make-user').click(function(event) {
-            event.preventDefault();
-            var email = $(this).closest('li[data-email]').data('email');
-            changeRole(email, 'library_user', {});
-        });
+            return local_messages;
+        }
+        // Roles order are important: first role is considered initial role (the role added to user when (s)he's added
+        // Last role is considered an admin role (unrestricted access + ability to manage other users' permissions)
+        // Changing roles is performed in promote-demote fashion, so moves only to adjacent roles is allowed
+        var roles = [
+            {key:'library_user', name:gettext('Library User')},
+            {key:'staff', name:gettext('Staff')},
+            {key:'instructor', 'name': gettext("Admin")}
+        ];
+
+        var options = {
+            el: $("#content"),
+            containerName: containerName,
+            tplUserURL: tplUserURL,
+            roles: roles,
+            users: users,
+            messages_modifier: updateMessages,
+            current_user_id: current_user_id,
+            allow_actions: allow_actions
+        };
 
+        var view = new ManageUsersAndRoles(options);
+        view.render();
     };
 });
diff --git a/cms/static/js/spec/views/pages/library_users_spec.js b/cms/static/js/spec/views/pages/library_users_spec.js
index 8ec8721..bf31d00 100644
--- a/cms/static/js/spec/views/pages/library_users_spec.js
+++ b/cms/static/js/spec/views/pages/library_users_spec.js
@@ -5,76 +5,149 @@ define([
 function ($, AjaxHelpers, ViewHelpers, ManageUsersFactory, ViewUtils) {
     "use strict";
     describe("Library Instructor Access Page", function () {
-        var mockHTML = readFixtures('mock/mock-manage-users-lib.underscore');
-
-        beforeEach(function () {
-            ViewHelpers.installMockAnalytics();
-            appendSetFixtures(mockHTML);
-            ManageUsersFactory(
-                "Mock Library",
-                ["honor@example.com", "audit@example.com", "staff@example.com"],
-                "dummy_change_role_url"
-            );
-        });
+        const changeRoleUrl = "dummy_change_role_url/@@EMAIL@@";
+        var team_member_fixture = readFixtures("team-member.underscore");
+        var systemFeedbackFixture = readFixtures("system-feedback.underscore");
 
-        afterEach(function () {
-            ViewHelpers.removeMockAnalytics();
-        });
+        function setRole(email, role){
+            var user_li = $("li.user-item[data-email="+ email + "]");
+            var role_action = $("li.action-role a.make-"+role, user_li);
+            expect(role_action).toBeVisible();
+            role_action.click();
+        }
 
-        it("can give a user permission to use the library", function () {
-            var requests = AjaxHelpers.requests(this);
-            var reloadSpy = spyOn(ViewUtils, 'reload');
-            $('.create-user-button').click();
-            expect($('.wrapper-create-user')).toHaveClass('is-shown');
-            $('.user-email-input').val('other@example.com');
-            $('.form-create.create-user .action-primary').click();
-            AjaxHelpers.expectJsonRequest(requests, 'POST', 'dummy_change_role_url', {role: 'library_user'});
-            AjaxHelpers.respondWithJson(requests, {'result': 'ok'});
-            expect(reloadSpy).toHaveBeenCalled();
-        });
+        function getUrl(email) {
+            return changeRoleUrl.replace('@@EMAIL@@', email);
+        }
 
-        it("can cancel adding a user to the library", function () {
-            $('.create-user-button').click();
-            $('.form-create.create-user .action-secondary').click();
-            expect($('.wrapper-create-user')).not.toHaveClass('is-shown');
-        });
+        describe("read-write access", function() {
+            var mockHTML = readFixtures('mock/mock-manage-users-lib.underscore');
 
-        // Disabled flaky test - the following three disabled by bradenmacdonald on 2015-02-09
-        // These tests are expected to be removed or rewritten as part of SOL-194
-        xit("displays an error when the required field is blank", function () {
-            var requests = AjaxHelpers.requests(this);
-            $('.create-user-button').click();
-            $('.user-email-input').val('');
-            var errorPromptSelector = '.wrapper-prompt.is-shown .prompt.error';
-            expect($(errorPromptSelector).length).toEqual(0);
-            $('.form-create.create-user .action-primary').click();
-            expect($(errorPromptSelector).length).toEqual(1);
-            expect($(errorPromptSelector)).toContainText('You must enter a valid email address');
-            expect(requests.length).toEqual(0);
-        });
+            beforeEach(function () {
+                ViewHelpers.installMockAnalytics();
+                setFixtures(mockHTML);
+                appendSetFixtures($("<script>", { id: "team-member-tpl", type: "text/template"}).text(team_member_fixture));
+                appendSetFixtures($("<script>", { id: "system-feedback-tpl", type: "text/template"}).text(systemFeedbackFixture));
+                ManageUsersFactory(
+                    "Mock Library",
+                    [
+                        {id: 1, email: "honor@example.com", username:"honor", role: 'staff'},
+                        {id: 2, email: "audit@example.com", username:"audit", role: 'instructor'},
+                        {id: 3, email: "staff@example.com", username:"staff", role: 'library_user'}
+                    ],
+                    changeRoleUrl,
+                    10000,
+                    true
+                );
+                waitsFor(function(){
+                   return $(".ui-loading").length === 0;
+                }, "Waiting for backbone render to happen", 1000);
+            });
+
+            afterEach(function () {
+                ViewHelpers.removeMockAnalytics();
+            });
+
+            it("can give a user permission to use the library", function () {
+                const email = 'other@example.com';
+                var requests = AjaxHelpers.requests(this);
+                var reloadSpy = spyOn(ViewUtils, 'reload');
+                $('.create-user-button').click();
+                expect($('.wrapper-create-user')).toHaveClass('is-shown');
+                $('.user-email-input').val(email);
+                $('.form-create.create-user .action-primary').click();
+                AjaxHelpers.expectJsonRequest(requests, 'POST', getUrl(email), {role: 'library_user'});
+                AjaxHelpers.respondWithJson(requests, {'result': 'ok'});
+                expect(reloadSpy).toHaveBeenCalled();
+            });
+
+            it("can promote user", function() {
+                const email = "staff@example.com";
+                var requests = AjaxHelpers.requests(this);
+                var reloadSpy = spyOn(ViewUtils, 'reload');
+                setRole("staff@example.com", 'staff');
+                AjaxHelpers.expectJsonRequest(requests, 'POST', getUrl(email), {role: 'staff'});
+                AjaxHelpers.respondWithJson(requests, {'result': 'ok'});
+                expect(reloadSpy).toHaveBeenCalled();
+            });
 
-        xit("displays an error when the user has already been added", function () {
-            var requests = AjaxHelpers.requests(this);
-            $('.create-user-button').click();
-            $('.user-email-input').val('honor@example.com');
-            var warningPromptSelector = '.wrapper-prompt.is-shown .prompt.warning';
-            expect($(warningPromptSelector).length).toEqual(0);
-            $('.form-create.create-user .action-primary').click();
-            expect($(warningPromptSelector).length).toEqual(1);
-            expect($(warningPromptSelector)).toContainText('Already a library team member');
-            expect(requests.length).toEqual(0);
+            it("can cancel adding a user to the library", function () {
+                $('.create-user-button').click();
+                $('.form-create.create-user .action-secondary').click();
+                expect($('.wrapper-create-user')).not.toHaveClass('is-shown');
+            });
+
+            it("displays an error when the required field is blank", function () {
+                var requests = AjaxHelpers.requests(this);
+                $('.create-user-button').click();
+                $('.user-email-input').val('');
+                var errorPromptSelector = '.wrapper-prompt.is-shown .prompt.error';
+                expect($(errorPromptSelector).length).toEqual(0);
+                $('.form-create.create-user .action-primary').click();
+                expect($(errorPromptSelector).length).toEqual(1);
+                expect($(errorPromptSelector)).toContainText('You must enter a valid email address');
+                expect(requests.length).toEqual(0);
+            });
+
+            it("displays an error when the user has already been added", function () {
+                var requests = AjaxHelpers.requests(this);
+                $('.create-user-button').click();
+                $('.user-email-input').val('honor@example.com');
+                var warningPromptSelector = '.wrapper-prompt.is-shown .prompt.warning';
+                expect($(warningPromptSelector).length).toEqual(0);
+                $('.form-create.create-user .action-primary').click();
+                expect($(warningPromptSelector).length).toEqual(1);
+                expect($(warningPromptSelector)).toContainText('Already a library team member');
+                expect(requests.length).toEqual(0);
+            });
+
+
+            it("can remove a user's permission to access the library", function () {
+                var requests = AjaxHelpers.requests(this);
+                var reloadSpy = spyOn(ViewUtils, 'reload');
+                var email = "honor@example.com";
+                $('.user-item[data-email="'+email+'"] .action-delete .delete').click();
+                expect($('.wrapper-prompt.is-shown .prompt.warning').length).toEqual(1);
+                $('.wrapper-prompt.is-shown .action-primary').click();
+                AjaxHelpers.expectJsonRequest(requests, 'DELETE', getUrl(email), {role: null});
+                AjaxHelpers.respondWithJson(requests, {'result': 'ok'});
+                expect(reloadSpy).toHaveBeenCalled();
+            });
         });
 
+        describe("read-only access", function() {
+            var mockHTML = readFixtures('mock/mock-manage-users-lib-ro.underscore');
+
+            beforeEach(function () {
+                ViewHelpers.installMockAnalytics();
+                setFixtures(mockHTML);
+                appendSetFixtures($("<script>", { id: "team-member-tpl", type: "text/template"}).text(team_member_fixture));
+                appendSetFixtures($("<script>", { id: "system-feedback-tpl", type: "text/template"}).text(systemFeedbackFixture));
+                ManageUsersFactory(
+                    "Mock Library",
+                    [
+                        {id: 1, email: "honor@example.com", username:"honor", role: 'staff'},
+                        {id: 2, email: "audit@example.com", username:"audit", role: 'instructor'},
+                        {id: 3, email: "staff@example.com", username:"staff", role: 'library_user'}
+                    ],
+                    "dummy_change_role_url",
+                    10000,
+                    false
+                );
+            });
+
+            afterEach(function () {
+                ViewHelpers.removeMockAnalytics();
+            });
+
+            it("can't give a user permission to use the library", function () {
+                expect($('.create-user-button')).not.toBeVisible();
+                expect($('.wrapper-create-user')).not.toBeVisible();
+            });
 
-        xit("can remove a user's permission to access the library", function () {
-            var requests = AjaxHelpers.requests(this);
-            var reloadSpy = spyOn(ViewUtils, 'reload');
-            $('.user-item[data-email="honor@example.com"] .action-delete .delete').click();
-            expect($('.wrapper-prompt.is-shown .prompt.warning').length).toEqual(1);
-            $('.wrapper-prompt.is-shown .action-primary').click();
-            AjaxHelpers.expectJsonRequest(requests, 'DELETE', 'dummy_change_role_url', {role: null});
-            AjaxHelpers.respondWithJson(requests, {'result': 'ok'});
-            expect(reloadSpy).toHaveBeenCalled();
+            it("can't promote or demote user", function () {
+                expect($('.action-role')).not.toBeVisible();
+            });
         });
     });
 });
diff --git a/cms/static/js/views/manage_users_and_roles.js b/cms/static/js/views/manage_users_and_roles.js
new file mode 100644
index 0000000..db4e5bc
--- /dev/null
+++ b/cms/static/js/views/manage_users_and_roles.js
@@ -0,0 +1,309 @@
+/*
+ Code for editing users and assigning roles within a course or library team context.
+ */
+define(['jquery', 'underscore', 'gettext', "js/views/baseview", 'js/views/feedback_prompt', 'js/views/utils/view_utils'],
+    function ($, _, gettext, BaseView, PromptView, ViewUtils) {
+        'use strict';
+        var default_messages = {
+            defaults: {
+                confirmation: gettext("Ok"),
+                changeRoleError: gettext("There was an error changing the user's role"),
+                unknown: gettext('Unknown')
+            },
+            errors: {
+                addUser: gettext('Error adding user'),
+                deleteUser: gettext('Error removing user')
+            },
+            invalidEmail: {
+                title: gettext('A valid email address is required'),
+                message: gettext('You must enter a valid email address in order to add a new team member'),
+                primaryAction: gettext('Return and add email address')
+            },
+            alreadyMember: {
+                title: gettext('Already a member'),
+                messageTpl: gettext("{email} is already on the {container} team. Recheck the email address if you want to add a new member."),
+                primaryAction: gettext('Return to team listing')
+            },
+            deleteUser: {
+                title: gettext('Are you sure?'),
+                messageTpl: gettext('Are you sure you want to restrict {email} access to “{container}”?'),
+                primaryAction: gettext('Delete'),
+                secondaryAction: gettext('Cancel')
+            }
+        };
+
+        function makeInvalidEmailMessage(messages) {
+            return new PromptView.Error({
+                title: messages.invalidEmail.title,
+                message: messages.invalidEmail.message,
+                actions: {
+                    primary: {
+                        text: messages.invalidEmail.primaryAction,
+                        click: function (view) {
+                            view.hide();
+                            $('#user-email-input').focus();
+                        }
+                    }
+                }
+            });
+        }
+
+        function makeAlreadyMemberMessage(messages, email, containerName) {
+            return new PromptView.Warning({
+                title: messages.alreadyMember.title,
+                message: _.template(
+                    messages.alreadyMember.messageTpl,
+                    {email: email, container: containerName},
+                    {interpolate: /\{(.+?)}/g}
+                ),
+                actions: {
+                    primary: {
+                        text: messages.alreadyMember.primaryAction,
+                        click: function (view) {
+                            view.hide();
+                            $('#user-email-input').focus();
+                        }
+                    }
+                }
+            });
+        }
+
+        function makeChangeRoleErrorMessage(messages, title, message, onErrorCallback) {
+            return new PromptView.Error({
+                title: title,
+                message: message,
+                actions: {
+                    primary: {
+                        text: messages.defaults.confirmation,
+                        click: function (view) {
+                            view.hide();
+                            onErrorCallback();
+                        }
+                    }
+                }
+            });
+        }
+
+        function getEmail(button) {
+            return $(button).closest('li[data-email]').data('email');
+        }
+
+        var ManageUsersAndRoles = BaseView.extend({
+            events: function () {
+                var baseEvents = {
+                    'click .create-user-button': "addUserHandler",
+                    'submit #create-user-form': "createUserFormSubmit",
+                    'click .action-cancel': "cancelEditHandler",
+                    'keyup': "keyUpHandler",
+                    'click .remove-user': "removeUserHandler"
+                };
+                var roleEvents = {};
+                var self = this;
+                for (var i = 0; i < self.roles.length; i++) {
+                    var role_name = self.roles[i].key;
+                    var role_selector = 'click .user-actions .make-' + role_name;
+
+                    (function (role) {
+                        roleEvents[role_selector] = function (event) { self.handleRoleButtonClick(event.target, role); };
+                    })(role_name);
+                }
+                return _.extend(baseEvents, roleEvents);
+            },
+
+            initialize: function (options) {
+                BaseView.prototype.initialize.call(this);
+                this.containerName = options.containerName;
+                this.tplUserURL = options.tplUserURL;
+
+                this.roles = options.roles; // [{key:role_key, name:Human-readable Name}, {key: admin, name: Admin}]
+                this.users = options.users; // [{username: username, email: email, role: role}, ...]
+                this.allow_actions = options.allow_actions;
+                this.current_user_id = options.current_user_id;
+
+                this.initial_role = this.roles[0];
+                this.admin_role = this.roles[this.roles.length - 1];
+
+                var message_mod = options.messages_modifier || function (messages) { return messages; };
+                this.messages = message_mod(default_messages);
+
+                this.$userEmailInput = this.$el.find('#user-email-input');
+                this.$createUserButton = this.$el.find('.create-user-button');
+                this.$createUserFormWrapper = this.$el.find('.wrapper-create-user');
+                this.$cancelButton = this.$el.find('.action-cancel');
+                this.$userList = this.$el.find('#user-list');
+            },
+
+            render: function () {
+                this.$userList.empty();
+                var templateFn = this.loadTemplate("team-member"),
+                    roles = _.object(_.pluck(this.roles, 'key'), _.pluck(this.roles, "name")),
+                    adminRoleCount = this.getAdminRoleCount(),
+                    viewHelpers = {
+                        format: function (template, data) { return _.template(template, data, {interpolate: /\{(.+?)}/g}); }
+                    };
+                for (var i = 0; i < this.users.length; i++) {
+                    var user = this.users[i],
+                        is_current_user = this.current_user_id == user.id;
+                    var template_data = {
+                        user: user,
+                        actions: this.getPossibleRoleChangesForRole(user.role, adminRoleCount),
+                        roles: roles,
+                        allow_delete: !(user.role === this.admin_role.key && adminRoleCount === 1),
+                        allow_actions: this.allow_actions,
+                        is_current_user: is_current_user,
+                        viewHelpers: viewHelpers
+                    };
+
+                    this.$userList.append(templateFn(template_data));
+                }
+            },
+
+            getAdminRoleCount: function () {
+                var self = this;
+                return _.filter(this.users, function (user) { return user.role === self.admin_role.key; }).length;
+            },
+
+            getPossibleRoleChangesForRole: function (role, adminRoleCount) {
+                var result = [],
+                    role_names = _.map(this.roles, function (role) { return role.key });
+                if (role === this.admin_role.key && adminRoleCount === 1) {
+                    result.push({notoggle: true});
+                }
+                else {
+                    var currentRoleIdx = _.indexOf(role_names, role);
+                    // in reverse order to show "Add" buttons to the left, "Remove" to the right
+                    for (var i = this.roles.length - 1; i >= 0; i--) {
+                        var other_role = this.roles[i];
+                        if (Math.abs(currentRoleIdx - i) !== 1) continue; // allows moving only to adjacent roles
+                        result.push({
+                            to_role: other_role.key,
+                            label: (i < currentRoleIdx) ? this.roles[currentRoleIdx].name : other_role.name,
+                            direction: (i < currentRoleIdx) ? "remove" : "add"
+                        });
+                    }
+                }
+                return result;
+            },
+
+            checkEmail: function (email) {
+                var allUsersEmails = _.map(this.users, function (user) { return user.email; });
+
+                if (!email) {
+                    return {valid: false, msg: makeInvalidEmailMessage(this.messages)};
+                }
+
+                if (_.contains(allUsersEmails, email)) {
+                    return {valid: false, msg: makeAlreadyMemberMessage(this.messages, email, this.containerName)};
+                }
+                return {valid: true};
+            },
+
+            // Our helper method that calls the RESTful API to add/remove/change user roles:
+            changeRole: function (email, newRole, opts) {
+                var self = this;
+                var url = this.tplUserURL.replace('@@EMAIL@@', email);
+                var errMessage = opts.errMessage || this.messages.defaults.changeRoleError;
+                var onSuccess = opts.onSuccess || function (data) { ViewUtils.reload(); };
+                var onError = opts.onError || function () {};
+                $.ajax({
+                    url: url,
+                    type: newRole ? 'POST' : 'DELETE',
+                    dataType: 'json',
+                    contentType: 'application/json',
+                    data: JSON.stringify({role: newRole}),
+                    success: onSuccess,
+                    error: function (jqXHR, textStatus, errorThrown) {
+                        var message, prompt;
+                        try {
+                            message = JSON.parse(jqXHR.responseText).error || self.messages.defaults.unknown;
+                        } catch (e) {
+                            message = self.messages.defaults.unknown;
+                        }
+                        prompt = makeChangeRoleErrorMessage(self.messages, errMessage, message, onError);
+                        prompt.show();
+                    }
+                });
+            },
+
+            handleRoleButtonClick: function (button, role) {
+                this.changeRole(getEmail(button), role, {});
+            },
+
+            addUserHandler: function (event) {
+                event.preventDefault();
+                this.$createUserButton
+                    .toggleClass('is-disabled')
+                    .attr('aria-disabled', this.$createUserButton.hasClass('is-disabled'));
+                this.$createUserFormWrapper.toggleClass('is-shown');
+                this.$userEmailInput.focus();
+            },
+
+            cancelEditHandler: function (event) {
+                event.preventDefault();
+                this.$createUserButton
+                    .toggleClass('is-disabled')
+                    .attr('aria-disabled', this.$createUserButton.hasClass('is-disabled'));
+                this.$createUserFormWrapper.toggleClass('is-shown');
+                this.$userEmailInput.val('');
+            },
+
+            createUserFormSubmit: function (event) {
+                event.preventDefault();
+                var self = this;
+                var email = this.$userEmailInput.val().trim();
+                var emailCheck = this.checkEmail(email);
+
+                if (!emailCheck.valid) {
+                    emailCheck.msg.show();
+                    return;
+                }
+
+                // Use the REST API to create the user, assigning them initial role for now:
+                this.changeRole(
+                    email,
+                    this.initial_role.key,
+                    {
+                        errMessage: this.messages.errors.addUser,
+                        onError: function () { self.$userEmailInput.focus(); }
+                    }
+                );
+            },
+
+            keyUpHandler: function (event) {
+                if (event.which === jQuery.ui.keyCode.ESCAPE && this.$createUserFormWrapper.is('.is-shown')) {
+                    this.$cancelButton.click();
+                }
+            },
+
+            removeUserHandler: function (event) {
+                event.preventDefault();
+                var self = this;
+                var email = getEmail(event.target);
+                var msg = new PromptView.Warning({
+                    title: self.messages.deleteUser.title,
+                    message: _.template(
+                        self.messages.deleteUser.messageTpl,
+                        {email: email, container: self.containerName},
+                        {interpolate: /\{(.+?)}/g}
+                    ),
+                    actions: {
+                        primary: {
+                            text: self.messages.deleteUser.primaryAction,
+                            click: function (view) {
+                                view.hide();
+                                // Use the REST API to delete the user:
+                                self.changeRole(email, null, {errMessage: self.messages.errors.deleteUser});
+                            }
+                        },
+                        secondary: {
+                            text: self.messages.deleteUser.secondaryAction,
+                            click: function (view) { view.hide(); }
+                        }
+                    }
+                });
+                msg.show();
+            }
+        });
+
+        return ManageUsersAndRoles;
+    });
diff --git a/cms/static/sass/views/_users.scss b/cms/static/sass/views/_users.scss
index a729d94..58640f6 100644
--- a/cms/static/sass/views/_users.scss
+++ b/cms/static/sass/views/_users.scss
@@ -113,11 +113,13 @@
           background: $pink-u3;
         }
 
-        &.flag-role-admin {
+        &.flag-role-admin,
+        &.flag-role-instructor {
           background: $pink;
         }
 
-        &.flag-role-user {
+        &.flag-role-user,
+        &.flag-role-library_user {
           background: $yellow-d1;
           .msg-you { color: $yellow-l1; }
         }
diff --git a/cms/templates/js/mock/mock-manage-users-lib-ro.underscore b/cms/templates/js/mock/mock-manage-users-lib-ro.underscore
new file mode 100644
index 0000000..2c1a5e2
--- /dev/null
+++ b/cms/templates/js/mock/mock-manage-users-lib-ro.underscore
@@ -0,0 +1,30 @@
+<div id='page-alert'></div>
+<div id='page-prompt'></div>
+
+<div id="content">
+    <div class="wrapper-mast wrapper">
+      <header class="mast has-actions has-subtitle">
+        <h1 class="page-header">
+          <small class="subtitle">Settings</small>
+          <span class="sr">&gt; </span>Instructor Access
+        </h1>
+
+        <nav class="nav-actions">
+          <h3 class="sr">Page Actions</h3>
+          <ul></ul>
+        </nav>
+      </header>
+    </div>
+
+    <div class="wrapper-content wrapper">
+      <section class="content">
+        <article class="content-primary" role="main">
+          <ol id="user-list" class="user-list">
+            <div class="ui-loading">
+              <p><span class="spin"><i class="icon fa fa-refresh"></i></span> <span class="copy">Loading</span></p>
+            </div>
+          </ol>
+        </article>
+      </section>
+    </div>
+</div>
diff --git a/cms/templates/js/mock/mock-manage-users-lib.underscore b/cms/templates/js/mock/mock-manage-users-lib.underscore
index fb0ea5e..ad0dcb9 100644
--- a/cms/templates/js/mock/mock-manage-users-lib.underscore
+++ b/cms/templates/js/mock/mock-manage-users-lib.underscore
@@ -1,146 +1,60 @@
-<div class="wrapper-mast wrapper">
-  <header class="mast has-actions has-subtitle">
-    <h1 class="page-header">
-      <small class="subtitle">Settings</small>
-      <span class="sr">&gt; </span>Instructor Access
-    </h1>
-
-    <nav class="nav-actions">
-      <h3 class="sr">Page Actions</h3>
-      <ul>
-        <li class="nav-item">
-          <a href="#" class="button new-button create-user-button"><i class="icon fa fa-plus"></i> Add Instructor</a>
-        </li>
-      </ul>
-    </nav>
-  </header>
-</div>
-
-<div class="wrapper-content wrapper">
-  <section class="content">
-    <article class="content-primary" role="main">
-      <div class="wrapper-create-element animate wrapper-create-user">
-        <form class="form-create create-user" id="create-user-form" name="create-user-form">
-          <div class="wrapper-form">
-            <h3 class="title">Grant Instructor Access to This Library</h3>
-
-            <fieldset class="form-fields">
-              <legend class="sr">New Instructor Information</legend>
-
-                <ol class="list-input">
-                  <li class="field text required create-user-email">
-                    <label for="user-email-input">User's Email Address</label>
-                    <input id="user-email-input" class="user-email-input" name="user-email" type="text" placeholder="example: username@domain.com" value="">
-                    <span class="tip tip-stacked">Please provide the email address of the instructor you'd like to add</span>
-                  </li>
-                </ol>
-            </fieldset>
-          </div>
-
-          <div class="actions">
-            <button class="action action-primary" type="submit">Add User</button>
-            <button class="action action-secondary action-cancel">Cancel</button>
-          </div>
-        </form>
-      </div>
-
-      <ol class="user-list">
-
-        <li class="user-item" data-email="honor@example.com">
-
-          <span class="wrapper-ui-badge">
-            <span class="flag flag-role flag-role-staff is-hanging">
-              <span class="label sr">Current Role:</span>
-              <span class="value">
-                Staff
-              </span>
-            </span>
-          </span>
-
-          <div class="item-metadata">
-            <h3 class="user-name">
-              <span class="user-username">honor</span>
-              <span class="user-email">
-                <a class="action action-email" href="mailto:honor@example.com" title="send an email message to honor@example.com">honor@example.com</a>
-              </span>
-            </h3>
-          </div>
-
-          <ul class="item-actions user-actions">
-              <li class="action action-role">
-                <a href="#" class="make-instructor admin-role add-admin-role">Add Admin Access</span></a>
-                <a href="#" class="make-user admin-role remove-admin-role">Remove Staff Access</span></a>
-              </li>
-            <li class="action action-delete ">
-                <a href="#" class="delete remove-user action-icon" data-tooltip="Remove this user"><i class="icon fa fa-trash-o"></i><span class="sr">Delete the user, honor</span></a>
+<div id='page-alert'></div>
+<div id='page-prompt'></div>
+
+<div id="content">
+    <div class="wrapper-mast wrapper">
+      <header class="mast has-actions has-subtitle">
+        <h1 class="page-header">
+          <small class="subtitle">Settings</small>
+          <span class="sr">&gt; </span>Instructor Access
+        </h1>
+
+        <nav class="nav-actions">
+          <h3 class="sr">Page Actions</h3>
+          <ul>
+            <li class="nav-item">
+              <a href="#" class="button new-button create-user-button"><i class="icon fa fa-plus"></i> Add Instructor</a>
             </li>
           </ul>
-
-        </li>
-
-        <li class="user-item" data-email="audit@example.com">
-
-          <span class="wrapper-ui-badge">
-            <span class="flag flag-role flag-role-admin is-hanging">
-              <span class="label sr">Current Role:</span>
-              <span class="value">
-                Admin
-              </span>
-            </span>
-          </span>
-
-          <div class="item-metadata">
-            <h3 class="user-name">
-              <span class="user-username">audit</span>
-              <span class="user-email">
-                <a class="action action-email" href="mailto:audit@example.com" title="send an email message to audit@example.com">audit@example.com</a>
-              </span>
-            </h3>
-          </div>
-
-          <ul class="item-actions user-actions">
-                <li class="action action-role">
-                  <a href="#" class="make-staff admin-role remove-admin-role">Remove Admin Access</span></a>
-                </li>
-            <li class="action action-delete ">
-                <a href="#" class="delete remove-user action-icon" data-tooltip="Remove this user"><i class="icon fa fa-trash-o"></i><span class="sr">Delete the user, audit</span></a>
-            </li>
-          </ul>
-
-        </li>
-
-        <li class="user-item" data-email="staff@example.com">
-
-          <span class="wrapper-ui-badge">
-            <span class="flag flag-role flag-role-user is-hanging">
-              <span class="label sr">Current Role:</span>
-              <span class="value">
-                User
-              </span>
-            </span>
-          </span>
-
-          <div class="item-metadata">
-            <h3 class="user-name">
-              <span class="user-username">staff</span>
-              <span class="user-email">
-                <a class="action action-email" href="mailto:staff@example.com" title="send an email message to staff@example.com">staff@example.com</a>
-              </span>
-            </h3>
+        </nav>
+      </header>
+    </div>
+
+    <div class="wrapper-content wrapper">
+      <section class="content">
+        <article class="content-primary" role="main">
+          <div class="wrapper-create-element animate wrapper-create-user">
+            <form class="form-create create-user" id="create-user-form" name="create-user-form">
+              <div class="wrapper-form">
+                <h3 class="title">Grant Instructor Access to This Library</h3>
+
+                <fieldset class="form-fields">
+                  <legend class="sr">New Instructor Information</legend>
+
+                    <ol class="list-input">
+                      <li class="field text required create-user-email">
+                        <label for="user-email-input">User's Email Address</label>
+                        <input id="user-email-input" class="user-email-input" name="user-email" type="text" placeholder="example: username@domain.com" value="">
+                        <span class="tip tip-stacked">Please provide the email address of the instructor you'd like to add</span>
+                      </li>
+                    </ol>
+                </fieldset>
+              </div>
+
+              <div class="actions">
+                <button class="action action-primary" type="submit">Add User</button>
+                <button class="action action-secondary action-cancel">Cancel</button>
+              </div>
+            </form>
           </div>
 
-          <ul class="item-actions user-actions">
-              <li class="action action-role">
-                <a href="#" class="make-staff admin-role add-admin-role">Add Staff Access</span></a>
-              </li>
-            <li class="action action-delete ">
-                <a href="#" class="delete remove-user action-icon" data-tooltip="Remove this user"><i class="icon fa fa-trash-o"></i><span class="sr">Delete the user, staff</span></a>
-            </li>
-          </ul>
-
-        </li>
-      </ol>
+          <ol id="user-list" class="user-list">
+            <div class="ui-loading">
+              <p><span class="spin"><i class="icon fa fa-refresh"></i></span> <span class="copy">Loading</span></p>
+            </div>
+          </ol>
 
-    </article>
-  </section>
+        </article>
+      </section>
+    </div>
 </div>
diff --git a/cms/templates/js/team-member.underscore b/cms/templates/js/team-member.underscore
new file mode 100644
index 0000000..3d820ab
--- /dev/null
+++ b/cms/templates/js/team-member.underscore
@@ -0,0 +1,50 @@
+<li class="user-item" data-email="<%= user.email %>">
+    <span class="wrapper-ui-badge">
+    <span class="flag flag-role flag-role-<%= user.role %> is-hanging">
+      <span class="label sr"><%= gettext("Current Role:") %></span>
+      <span class="value">
+        <%= roles[user.role] %>
+        <% if (is_current_user) { %>
+            <span class="msg-you"><%= gettext("You!") %></span>
+        <% } %>
+      </span>
+    </span>
+    </span>
+
+    <div class="item-metadata">
+    <h3 class="user-name">
+      <span class="user-username"><%= user.username %></span>
+      <span class="user-email">
+        <a class="action action-email" href="mailto:<%= user.email %>"
+                title="<%= viewHelpers.format(gettext("send an email message to {email}"), {email: user.email})%>">
+            <%= user.email %>
+        </a>
+      </span>
+    </h3>
+    </div>
+
+    <% if (allow_actions) { %>
+    <ul class="item-actions user-actions">
+        <li class="action action-role">
+        <% for (var i=0; i < actions.length; i++) { %>
+            <% var action = actions[i]; %>
+                <% if (action.notoggle) { %>
+                    <span class="admin-role notoggleforyou"><%= gettext("Promote another member to Admin to remove your admin rights") %></span>
+                <% } else { %>
+                    <a href="#" class="make-<%= action.to_role %> admin-role <%= action.direction %>-admin-role">
+                        <% var template = (action.direction === 'add') ? gettext("Add {role} Access") : gettext("Remove {role} Access"); %>
+                        <%= viewHelpers.format(template, {role: action.label}) %></span>
+                    </a>
+                <% } %>
+        <% } %>
+        </li>
+        <li class="action action-delete <%=!allow_delete ? "is-disabled" : "" %> aria-disabled="<%=!allow_delete%>">
+            <a href="#" class="delete remove-user action-icon" data-id="<%= user.email %>">
+                <i class="icon fa fa-trash-o"></i>
+                <span class="sr"><%= viewHelpers.format(gettext("Delete the user, {username}"), {username:user.username}) %></span>
+            </a>
+        </li>
+    </ul>
+    <% } %>
+
+</li>
diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html
index 5fb6bff..64f3601 100644
--- a/cms/templates/manage_users.html
+++ b/cms/templates/manage_users.html
@@ -1,11 +1,17 @@
 <%! import json %>
 <%! from django.utils.translation import ugettext as _ %>
 <%! from django.core.urlresolvers import reverse %>
-<%! from student.roles import CourseInstructorRole %>
 <%inherit file="base.html" />
 <%def name="online_help_token()"><% return "team_course" %></%def>
 <%block name="title">${_("Course Team Settings")}</%block>
 <%block name="bodyclass">is-signedin course users view-team</%block>
+<%namespace name='static' file='static_content.html'/>
+
+<%block name="header_extras">
+<script type="text/template" id="team-member-tpl">
+    <%static:include path="js/team-member.underscore" />
+</script>
+</%block>
 
 <%block name="content">
 
@@ -44,7 +50,7 @@
                 <ol class="list-input">
                   <li class="field text required create-user-email">
                     <label for="user-email-input">${_("User's Email Address")}</label>
-                    <input id="user-email-input" name="user-email" type="text" placeholder="${_('example: username@domain.com')}" value="">
+                    <input id="user-email-input" class="user-email-input" name="user-email" type="text" placeholder="${_('example: username@domain.com')}" value="">
                     <span class="tip tip-stacked">${_("Please provide the email address of the course staff member you'd like to add")}</span>
                   </li>
                 </ol>
@@ -59,73 +65,13 @@
       </div>
       %endif
 
-      <ol class="user-list">
-        % for user in staff:
-          <% course_team_url = reverse(
-             'contentstore.views.course_team_handler',
-             kwargs={'course_key_string': unicode(context_course.id), 'email': user.email}
-          )
-          %>
-
-        <li class="user-item" data-email="${user.email}" data-url="${course_team_url}">
-
-        <% is_instuctor = CourseInstructorRole(context_course.id).has_user(user) %>
-          % if is_instuctor:
-          <span class="wrapper-ui-badge">
-            <span class="flag flag-role flag-role-admin is-hanging">
-              <span class="label sr">${_("Current Role:")}</span>
-              <span class="value">
-                ${_("Admin")}
-                % if request.user.id == user.id:
-                    <span class="msg-you">${_("You!")}</span>
-                % endif
-              </span>
-            </span>
-          </span>
-          % else:
-          <span class="wrapper-ui-badge">
-            <span class="flag flag-role flag-role-staff is-hanging">
-              <span class="label sr">${_("Current Role:")}</span>
-              <span class="value">
-                ${_("Staff")}
-                % if request.user.id == user.id:
-                    <span class="msg-you">${_("You!")}</span>
-                % endif
-              </span>
-            </span>
-          </span>
-          % endif
-
-          <div class="item-metadata">
-            <h3 class="user-name">
-              <span class="user-username">${user.username}</span>
-              <span class="user-email">
-                <a class="action action-email" href="mailto:${user.email}" title="${_("send an email message to {email}").format(email=user.email)}">${user.email}</a>
-              </span>
-            </h3>
-          </div>
-
-          % if allow_actions:
-          <ul  class="item-actions user-actions">
-            <li class="action action-role">
-              % if is_instuctor and len(instructors) == 1:
-                <span class="admin-role notoggleforyou">${_("Promote another member to Admin to remove your admin rights")}</span>
-              % else:
-                <a href="#" class="admin-role toggle-admin-role ${'remove' if is_instuctor else 'add'}-admin-role">${_("Remove Admin Access") if is_instuctor else _("Add Admin Access")}</a>
-              % endif
-            </li>
-            <li class="action action-delete ${"is-disabled" if request.user.id == user.id else ""}" aria-disabled="${'true' if request.user.id == user.id else 'false'}">
-                <a href="#" class="delete remove-user action-icon" data-id="${user.email}"><i class="icon fa fa-trash-o"></i><span class="sr">${_("Delete the user, {username}").format(username=user.username)}</span></a>
-            </li>
-          </ul>
-          % endif
-
-        </li>
-        % endfor
+      <ol class="user-list" id="user-list">
+        <div class="ui-loading">
+          <p><span class="spin"><i class="icon fa fa-refresh"></i></span> <span class="copy">Loading</span></p>
+        </div>
       </ol>
 
-      <% user_is_instuctor = CourseInstructorRole(context_course.id).has_user(request.user) %>
-      % if user_is_instuctor and len(staff) == 1:
+      % if allow_actions and len(users) == 1:
       <div class="notice notice-incontext notice-create has-actions">
         <div class="msg">
           <h3 class="title">${_('Add Team Members to This Course')}</h3>
@@ -150,7 +96,7 @@
         <p>${_("Admins are course team members who can add and remove other course team members.")}</p>
       </div>
 
-      % if user_is_instuctor and len(instructors) == 1:
+      % if show_transfer_ownership_hint:
       <div class="bit">
         <h3 class="title-3">${_("Transferring Ownership")}</h3>
         <p>${_("Every course must have an Admin. If you're the Admin and you want transfer ownership of the course, click Add admin access to make another user the Admin, then ask that user to remove you from the Course Team list.")}</p>
@@ -162,7 +108,13 @@
 </%block>
 
 <%block name="requirejs">
-  require(["js/factories/manage_users"], function(ManageUsersFactory) {
-      ManageUsersFactory(${json.dumps([user.email for user in staff])}, "${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': unicode(context_course.id), 'email': '@@EMAIL@@'})}");
+  require(["js/factories/manage_users"], function(ManageCourseUsersFactory) {
+      ManageCourseUsersFactory(
+        "${context_course.display_name | h}",
+        ${json.dumps(users)},
+        "${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': unicode(context_course.id), 'email': '@@EMAIL@@'})}",
+        ${ request.user.id },
+        ${str(allow_actions).lower()}
+      );
   });
 </%block>
diff --git a/cms/templates/manage_users_lib.html b/cms/templates/manage_users_lib.html
index 8a4bb26..2d40585 100644
--- a/cms/templates/manage_users_lib.html
+++ b/cms/templates/manage_users_lib.html
@@ -5,6 +5,13 @@
 <%def name="online_help_token()"><% return "team_library" %></%def>
 <%block name="title">${_("Library User Access")}</%block>
 <%block name="bodyclass">is-signedin course users view-team</%block>
+<%namespace name='static' file='static_content.html'/>
+
+<%block name="header_extras">
+<script type="text/template" id="team-member-tpl">
+    <%static:include path="js/team-member.underscore" />
+</script>
+</%block>
 
 <%block name="content">
 
@@ -58,77 +65,13 @@
       </div>
       %endif
 
-      <ol class="user-list">
-        % for user in all_users:
-          <%
-          is_instructor = user in instructors
-          is_staff = user in staff
-          role_id = 'admin' if is_instructor else ('staff' if is_staff else 'user')
-          role_desc = _("Admin") if is_instructor else (_("Staff") if is_staff else _("User"))
-          %>
-
-        <li class="user-item" data-email="${user.email}">
-
-          <span class="wrapper-ui-badge">
-            <span class="flag flag-role flag-role-${role_id} is-hanging">
-              <span class="label sr">${_("Current Role:")}</span>
-              <span class="value">
-                ${role_desc}
-                % if request.user.id == user.id:
-                    <span class="msg-you">${_("You!")}</span>
-                % endif
-              </span>
-            </span>
-          </span>
-
-          <div class="item-metadata">
-            <h3 class="user-name">
-              <span class="user-username">${user.username}</span>
-              <span class="user-email">
-                <a class="action action-email" href="mailto:${user.email}" title="${_("send an email message to {email}").format(email=user.email)}">${user.email}</a>
-              </span>
-            </h3>
-          </div>
-
-          % if allow_actions:
-          <ul class="item-actions user-actions">
-            % if is_instructor:
-              % if len(instructors) > 1:
-                <li class="action action-role">
-                  <a href="#" class="make-staff admin-role remove-admin-role">${_("Remove Admin Access")}</span></a>
-                </li>
-              % else:
-                <li class="action action-role">
-                  <span class="admin-role notoggleforyou">${_("Promote another member to Admin to remove your admin rights")}</span>
-                </li>
-              % endif
-            % elif is_staff:
-              <li class="action action-role">
-                <a href="#" class="make-instructor admin-role add-admin-role">${_("Add Admin Access")}</span></a>
-                <a href="#" class="make-user admin-role remove-admin-role">${_("Remove Staff Access")}</span></a>
-              </li>
-            % else:
-              <li class="action action-role">
-                <a href="#" class="make-staff admin-role add-admin-role">${_("Add Staff Access")}</span></a>
-              </li>
-            % endif
-            <li class="action action-delete ${"is-disabled" if request.user.id == user.id and is_instructor and len(instructors) == 1 else ""}">
-                <a href="#" class="delete remove-user action-icon" data-tooltip="${_("Remove this user")}"><i class="icon fa fa-trash-o"></i><span class="sr">${_("Delete the user, {username}").format(username=user.username)}</span></a>
-            </li>
-          </ul>
-          % elif request.user.id == user.id:
-          <ul class="item-actions user-actions">
-            <li class="action action-delete">
-                <a href="#" class="delete remove-user action-icon" data-tooltip="${_("Remove me")}"><i class="icon fa fa-trash-o"></i><span class="sr">${_("Remove me from this library")}</span></a>
-            </li>
-          </ul>
-          % endif
-
-        </li>
-        % endfor
+      <ol class="user-list" id="user-list">
+        <div class="ui-loading">
+          <p><span class="spin"><i class="icon fa fa-refresh"></i></span> <span class="copy">Loading</span></p>
+        </div>
       </ol>
 
-      % if allow_actions and len(all_users) == 1:
+      % if allow_actions and len(users) == 1:
       <div class="notice notice-incontext notice-create has-actions">
         <div class="msg">
           <h3 class="title">${_('Add More Users to This Library')}</h3>
@@ -146,7 +89,7 @@
       %endif
     </article>
 
-    <aside class="content-supplementary" role="complimentary">
+    <aside class="content-supplementary" role="complementary">
       <div class="bit">
         <h3 class="title-3">${_("Library Access Roles")}</h3>
         <p>${_("There are three access roles for libraries: User, Staff, and Admin.")}</p>
@@ -160,11 +103,13 @@
 </%block>
 
 <%block name="requirejs">
-  require(["js/factories/manage_users_lib"], function(ManageUsersFactory) {
-      ManageUsersFactory(
+  require(["js/factories/manage_users_lib"], function(ManageLibraryUsersFactory) {
+      ManageLibraryUsersFactory(
         "${context_library.display_name_with_default | h}",
-        ${json.dumps([user.email for user in all_users])},
-        "${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': library_key, 'email': '@@EMAIL@@'})}"
+        ${json.dumps(users)},
+        "${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': library_key, 'email': '@@EMAIL@@'})}",
+        ${ request.user.id },
+        ${str(allow_actions).lower()}
       );
   });
 </%block>
diff --git a/cms/urls.py b/cms/urls.py
index a2a614a..79350d9 100644
--- a/cms/urls.py
+++ b/cms/urls.py
@@ -75,13 +75,13 @@ urlpatterns += patterns(
     url(r'^signin$', 'login_page', name='login'),
     url(r'^request_course_creator$', 'request_course_creator'),
 
-    url(r'^course_team/{}/(?P<email>.+)?$'.format(COURSELIKE_KEY_PATTERN), 'course_team_handler'),
+    url(r'^course_team/{}(?:/(?P<email>.+))?$'.format(COURSELIKE_KEY_PATTERN), 'course_team_handler'),
     url(r'^course_info/{}$'.format(settings.COURSE_KEY_PATTERN), 'course_info_handler'),
     url(
         r'^course_info_update/{}/(?P<provided_id>\d+)?$'.format(settings.COURSE_KEY_PATTERN),
         'course_info_update_handler'
     ),
-    url(r'^home/$', 'course_listing', name='home'),
+    url(r'^home/?$', 'course_listing', name='home'),
     url(
         r'^course/{}/search_reindex?$'.format(settings.COURSE_KEY_PATTERN),
         'course_search_index_handler',
diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py
index 5ea05dd..616991c 100644
--- a/common/djangoapps/terrain/steps.py
+++ b/common/djangoapps/terrain/steps.py
@@ -233,3 +233,11 @@ def run_ipdb(_step):
     import ipdb
     ipdb.set_trace()
     assert True
+
+
+@step(u'(I am viewing|s?he views) the course team settings$')
+def view_course_team_settings(_step, whom):
+    """ navigates to course team settings page """
+    world.click_course_settings()
+    link_css = 'li.nav-course-settings-team a'
+    world.css_click(link_css)
diff --git a/common/test/acceptance/pages/studio/index.py b/common/test/acceptance/pages/studio/index.py
index c23f0ab..4165ae9 100644
--- a/common/test/acceptance/pages/studio/index.py
+++ b/common/test/acceptance/pages/studio/index.py
@@ -84,6 +84,23 @@ class DashboardPage(PageObject):
         """
         self.q(css='.wrapper-create-library .new-library-save').click()
 
+    def list_courses(self):
+        """
+        List all the courses found on the page's list of libraries.
+        """
+        # Workaround Selenium/Firefox bug: `.text` property is broken on invisible elements
+        course_tab_link = self.q(css='#course-index-tabs .courses-tab a')
+        if course_tab_link:
+            course_tab_link.click()
+        div2info = lambda element: {
+            'name': element.find_element_by_css_selector('.course-title').text,
+            'org': element.find_element_by_css_selector('.course-org .value').text,
+            'number': element.find_element_by_css_selector('.course-num .value').text,
+            'run': element.find_element_by_css_selector('.course-run .value').text,
+            'url': element.find_element_by_css_selector('a.course-link').get_attribute('href'),
+        }
+        return self.q(css='.courses li.course-item').map(div2info).results
+
     def list_libraries(self):
         """
         Click the tab to display the available libraries, and return detail of them.
diff --git a/common/test/acceptance/pages/studio/library.py b/common/test/acceptance/pages/studio/library.py
index d2a5e6f..167f2b8 100644
--- a/common/test/acceptance/pages/studio/library.py
+++ b/common/test/acceptance/pages/studio/library.py
@@ -7,8 +7,8 @@ from bok_choy.promise import EmptyPromise
 from selenium.webdriver.support.select import Select
 from .component_editor import ComponentEditorView
 from .container import XBlockWrapper
+from ...pages.studio.users import UsersPageMixin
 from ...pages.studio.pagination import PaginatedMixin
-from ...tests.helpers import disable_animations
 from .utils import confirm_prompt, wait_for_notification
 from . import BASE_URL
 
@@ -35,7 +35,7 @@ class LibraryPage(PageObject):
         return self.q(css='body.view-library').present
 
 
-class LibraryEditPage(LibraryPage, PaginatedMixin):
+class LibraryEditPage(LibraryPage, PaginatedMixin, UsersPageMixin):
     """
     Library edit page in Studio
     """
@@ -56,11 +56,7 @@ class LibraryEditPage(LibraryPage, PaginatedMixin):
         for improved test reliability.
         """
         self.wait_for_ajax()
-        self.wait_for_element_invisibility(
-            '.ui-loading',
-            'Wait for the page to complete its initial loading of XBlocks via AJAX'
-        )
-        disable_animations(self)
+        super(LibraryEditPage, self).wait_until_ready()
 
     @property
     def xblocks(self):
@@ -146,6 +142,7 @@ class StudioLibraryContentEditor(ComponentEditorView):
 
     @property
     def library_name(self):
+        """ Gets name of library """
         return self.get_selected_option_text(self.LIBRARY_LABEL)
 
     @library_name.setter
diff --git a/common/test/acceptance/pages/studio/manage_users.py b/common/test/acceptance/pages/studio/manage_users.py
deleted file mode 100644
index 8abc113..0000000
--- a/common/test/acceptance/pages/studio/manage_users.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""
-Course Team page in Studio.
-"""
-
-from .course_page import CoursePage
-
-
-class CourseTeamPage(CoursePage):
-    """
-    Course Team page in Studio.
-    """
-
-    url_path = "course_team"
-
-    def is_browser_on_page(self):
-        return self.q(css='body.view-team').present
diff --git a/common/test/acceptance/pages/studio/users.py b/common/test/acceptance/pages/studio/users.py
index 5c0216f..0827a6b 100644
--- a/common/test/acceptance/pages/studio/users.py
+++ b/common/test/acceptance/pages/studio/users.py
@@ -4,6 +4,7 @@ Page classes to test either the Course Team page or the Library Team page.
 from bok_choy.promise import EmptyPromise
 from bok_choy.page_object import PageObject
 from ...tests.helpers import disable_animations
+from .course_page import CoursePage
 from . import BASE_URL
 
 
@@ -20,16 +21,10 @@ def wait_for_ajax_or_reload(browser):
     EmptyPromise(_is_ajax_finished, "Finished waiting for ajax requests.").fulfill()
 
 
-class UsersPage(PageObject):
-    """
-    Base class for either the Course Team page or the Library Team page
-    """
-
-    def __init__(self, browser, locator):
-        super(UsersPage, self).__init__(browser)
-        self.locator = locator
+class UsersPageMixin(PageObject):
+    """ Common functionality for course/library team pages """
+    new_user_form_selector = '.form-create.create-user .user-email-input'
 
-    @property
     def url(self):
         """
         URL to this page - override in subclass
@@ -52,6 +47,13 @@ class UsersPage(PageObject):
         ).results
 
     @property
+    def usernames(self):
+        """
+        Returns a list of user names for users listed on this page
+        """
+        return [user.name for user in self.users]
+
+    @property
     def has_add_button(self):
         """
         Is the "New Team Member" button present?
@@ -78,11 +80,55 @@ class UsersPage(PageObject):
         self.q(css='.form-create.create-user .action-primary').click()
         wait_for_ajax_or_reload(self.browser)
 
+    def get_user(self, email):
+        """ Gets user wrapper by email """
+        target_users = [user for user in self.users if user.email == email]
+        assert len(target_users) == 1
+        return target_users[0]
+
+    def add_user_to_course(self, email):
+        """ Adds user to a course/library """
+        self.click_add_button()
+        self.wait_for(lambda: self.new_user_form_visible, "Add user form is visible")
+        self.set_new_user_email(email)
+        self.click_submit_new_user_form()
+
+    def delete_user_from_course(self, email):
+        """ Deletes user from course/library """
+        target_user = self.get_user(email)
+        target_user.click_delete()
+
+    def modal_dialog_visible(self, dialog_type):
+        """ Checks if modal dialog of specified class is displayed """
+        return self.q(css='.prompt.{dialog_type}'.format(dialog_type=dialog_type)).visible
+
+    def modal_dialog_text(self, dialog_type):
+        """ Gets modal dialog text """
+        return self.q(css='.prompt.{dialog_type} .message'.format(dialog_type=dialog_type)).text[0]
+
+    def wait_until_ready(self):
+        """
+        When the page first loads, there is a loading indicator and most
+        functionality is not yet available. This waits for that loading to
+        finish.
+
+        Always call this before using the page. It also disables animations
+        for improved test reliability.
+        """
+        self.wait_for_element_invisibility(
+            '.ui-loading',
+            'Wait for the page to complete its initial loading and rendering via Backbone'
+        )
+        disable_animations(self)
+
 
-class LibraryUsersPage(UsersPage):
+class LibraryUsersPage(UsersPageMixin):
     """
     Library Team page in Studio
     """
+    def __init__(self, browser, locator):
+        super(LibraryUsersPage, self).__init__(browser)
+        self.locator = locator
 
     @property
     def url(self):
@@ -92,6 +138,14 @@ class LibraryUsersPage(UsersPage):
         return "{}/library/{}/team/".format(BASE_URL, unicode(self.locator))
 
 
+class CourseTeamPage(CoursePage, UsersPageMixin):
+    """
+    Course Team page in Studio.
+    """
+
+    url_path = "course_team"
+
+
 class UserWrapper(PageObject):
     """
     A PageObject representing a wrapper around a user listed on the course/library team page.
diff --git a/common/test/acceptance/tests/studio/base_studio_test.py b/common/test/acceptance/tests/studio/base_studio_test.py
index 066117a..b847c7b 100644
--- a/common/test/acceptance/tests/studio/base_studio_test.py
+++ b/common/test/acceptance/tests/studio/base_studio_test.py
@@ -42,6 +42,10 @@ class StudioCourseTest(UniqueCourseTest):
         Log in as the user that created the course. The user will be given instructor access
         to the course and enrolled in it. By default the user will not have staff access unless
         is_staff is passed as True.
+
+        Args:
+            user(dict): dictionary containing user data: {'username': ..., 'email': ..., 'password': ...}
+            is_staff(bool): register this user as staff
         """
         self.auth_page = AutoAuthPage(
             self.browser,
diff --git a/common/test/acceptance/tests/studio/test_studio_course_team.py b/common/test/acceptance/tests/studio/test_studio_course_team.py
new file mode 100644
index 0000000..70d7554
--- /dev/null
+++ b/common/test/acceptance/tests/studio/test_studio_course_team.py
@@ -0,0 +1,343 @@
+"""
+Acceptance tests for course in studio
+"""
+from nose.plugins.attrib import attr
+
+from .base_studio_test import StudioCourseTest
+from ...pages.studio.auto_auth import AutoAuthPage
+
+from ...pages.studio.users import CourseTeamPage
+from ...pages.studio.index import DashboardPage
+
+
+@attr('shard_2')
+class CourseTeamPageTest(StudioCourseTest):
+    """ As a course author, I want to be able to add others to my team """
+    def _make_user(self, username):
+        """ Registers user and returns user representation dictionary as expected by `log_in` function """
+        user = {
+            'username': username,
+            'email': username + "@example.com",
+            'password': username + '123'
+        }
+        AutoAuthPage(
+            self.browser, no_login=True,
+            username=user.get('username'), email=user.get('email'), password=user.get('password')
+        ).visit()
+        return user
+
+    def setUp(self, is_staff=False):
+        """
+        Install a course with no content using a fixture.
+        """
+        super(CourseTeamPageTest, self).setUp(is_staff)
+
+        self.other_user = self._make_user('other')  # pylint:disable=attribute-defined-outside-init
+        self.dashboard_page = DashboardPage(self.browser)  # pylint:disable=attribute-defined-outside-init
+        self.page = CourseTeamPage(  # pylint:disable=attribute-defined-outside-init
+            self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run']
+        )
+        self._go_to_course_team_page()
+
+    def _go_to_course_team_page(self):
+        """ Opens Course Team page """
+        self.page.visit()
+        self.page.wait_until_ready()
+
+    def _expect_refresh(self):
+        """
+        Wait for the page to reload.
+        """
+        self.page = CourseTeamPage(
+            self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run']
+        )
+        self._go_to_course_team_page()
+
+    def _assert_current_course(self, visible=True):
+        """ Checks if current course is accessible to current user """
+        self.dashboard_page.visit()
+        courses = self.dashboard_page.list_courses()
+
+        def check_course_equality(course1, course2):
+            """ Compares to course dictionaries using org, number and run as keys"""
+            return (
+                course1['org'] == course2['org'] and
+                course1['number'] == course2['number'] and
+                course1['run'] == course2['run']
+            )
+
+        actual_visible = any((check_course_equality(course, self.course_info) for course in courses))
+
+        self.assertEqual(actual_visible, visible)
+
+    def _assert_user_present(self, user, present=True):
+        """ Checks if specified user present on Course Team page """
+        if present:
+            self.assertIn(user.get('username'), self.page.usernames)
+        else:
+            self.assertNotIn(user.get('username'), self.page.usernames)
+
+    def _should_see_dialog(self, dialog_type, dialog_message):
+        """ Asserts dialog with specified message is shown """
+        self.page.modal_dialog_visible(dialog_type)
+        self.assertIn(dialog_message, self.page.modal_dialog_text(dialog_type))
+
+    def _assert_is_staff(self, user, can_manage=True):
+        """ Checks if user have staff permissions, can be promoted and can't be demoted """
+        self.assertIn("staff", user.role_label.lower())
+        if can_manage:
+            self.assertTrue(user.can_promote)
+            self.assertFalse(user.can_demote)
+            self.assertIn("Add Admin Access", user.promote_button_text)
+
+    def _assert_is_admin(self, user):
+        """ Checks if user have admin permissions, can't be promoted and can be demoted """
+        self.assertIn("admin", user.role_label.lower())
+        self.assertFalse(user.can_promote)
+        self.assertTrue(user.can_demote)
+        self.assertIn("Remove Admin Access", user.demote_button_text)
+
+    def _assert_can_manage_users(self):
+        """ Checks if current user can manage course team """
+        self.assertTrue(self.page.has_add_button)
+        for user in self.page.users:
+            self.assertTrue(user.can_promote or user.can_demote)  # depending on actual user role
+            self.assertTrue(user.can_delete)
+
+    def _assert_can_not_manage_users(self):
+        """ Checks if current user can't manage course team """
+        self.assertFalse(self.page.has_add_button)
+        for user in self.page.users:
+            self.assertFalse(user.can_promote)
+            self.assertFalse(user.can_demote)
+            self.assertFalse(user.can_delete)
+
+    def test_admins_can_add_other_users(self):
+        """
+        Scenario: Admins can add other users
+        Given I have opened a new course in Studio
+        And I am viewing the course team settings
+        When I add other user to the course team
+        And other user logs in
+        Then he does see the course on her page
+        """
+        self.page.add_user_to_course(self.other_user.get('email'))
+        self._assert_user_present(self.other_user, present=True)
+        self.log_in(self.other_user)
+        self._assert_current_course(visible=True)
+
+    def test_added_users_cannot_add_or_delete_other_users(self):
+        """
+        Scenario: Added users cannot delete or add other users
+        Given I have opened a new course in Studio
+        And I am viewing the course team settings
+        When I add other user to the course team
+        And other user logs in
+        And he selects the new course
+        And he views the course team settings
+        Then he cannot manage users
+        """
+        self.page.add_user_to_course(self.other_user.get('email'))
+        self._assert_user_present(self.other_user, present=True)
+
+        self.log_in(self.other_user)
+        self._assert_current_course(visible=True)
+        self._go_to_course_team_page()
+
+        bob = self.page.get_user(self.other_user.get('email'))
+        self.assertTrue(bob.is_current_user)
+        self.assertFalse(self.page.has_add_button)
+
+        self._assert_can_not_manage_users()
+
+    def test_admins_can_delete_other_users(self):
+        """
+        Scenario: Admins can delete other users
+        Given I have opened a new course in Studio
+        And I am viewing the course team settings
+        When I add other user to the course team
+        And I delete other user from the course team
+        And other user logs in
+        Then he does not see the course on her page
+        """
+        self.page.add_user_to_course(self.other_user.get('email'))
+        self._assert_user_present(self.other_user, present=True)
+        self.page.delete_user_from_course(self.other_user.get('email'))
+        self._assert_user_present(self.other_user, present=False)
+
+        self.log_in(self.other_user)
+        self._assert_current_course(visible=False)
+
+    def test_admins_cannot_add_users_that_do_not_exist(self):
+        """
+        Scenario: Admins cannot add users that do not exist
+        Given I have opened a new course in Studio
+        And I am viewing the course team settings
+        When I add "dennis" to the course team
+        Then I should see "Could not find user by email address" somewhere on the page
+        """
+        self.page.add_user_to_course("dennis@example.com")
+        self._should_see_dialog('error', "Could not find user by email address")
+
+    def test_admins_should_be_able_to_make_other_people_into_admins(self):
+        """
+        Scenario: Admins should be able to make other people into admins
+        Given I have opened a new course in Studio
+        And I am viewing the course team settings
+        And I add other user to the course team
+        When I make other user a course team admin
+        And other user logs in
+        And he selects the new course
+        And he views the course team settings
+        Then other user should be marked as an admin
+        And he can manage users
+        """
+        self.page.add_user_to_course(self.other_user.get('email'))
+        self._assert_user_present(self.other_user, present=True)
+
+        other = self.page.get_user(self.other_user.get('email'))
+        self._assert_is_staff(other)
+        other.click_promote()
+        self._expect_refresh()
+        self._assert_is_admin(other)
+
+        self.log_in(self.other_user)
+        self._go_to_course_team_page()
+        other = self.page.get_user(self.other_user.get('email'))
+        self.assertTrue(other.is_current_user)
+        self._assert_can_manage_users()
+
+    def test_admins_should_be_able_to_remove_other_admins(self):
+        """
+        Scenario: Admins should be able to remove other admins
+        Given I have opened a new course in Studio
+        And I grant admin rights to other user
+        Then he can add, delete, promote and demote users
+        And I am viewing the course team settings
+        When I remove admin rights from other user
+        And other user logs in
+        And he selects the new course
+        And he views the course team settings
+        Then other user should not be marked as an admin
+        And he cannot manage users
+        """
+        self.page.add_user_to_course(self.other_user.get('email'))
+        self._assert_user_present(self.other_user, present=True)
+
+        other = self.page.get_user(self.other_user.get('email'))
+        self._assert_is_staff(other)
+        other.click_promote()
+        self._expect_refresh()
+        other = self.page.get_user(self.other_user.get('email'))
+        self._assert_is_admin(other)
+
+        # precondition check - frank is an admin and can add/delete/promote/demote users
+        self.log_in(self.other_user)
+        self._go_to_course_team_page()
+        other = self.page.get_user(self.other_user.get('email'))
+        self.assertTrue(other.is_current_user)
+        self._assert_can_manage_users()
+
+        self.log_in(self.user)
+        self._go_to_course_team_page()
+        other = self.page.get_user(self.other_user.get('email'))
+        other.click_demote()
+        self._expect_refresh()
+        other = self.page.get_user(self.other_user.get('email'))
+        self._assert_is_staff(other)
+
+        self.log_in(self.other_user)
+        self._go_to_course_team_page()
+        other = self.page.get_user(self.other_user.get('email'))
+        self.assertTrue(other.is_current_user)
+        self._assert_can_not_manage_users()
+
+    def test_admins_should_be_able_to_remove_themself_if_other_admin_exists(self):
+        """
+        Scenario: Admins should be able to give course ownership to someone else
+        Given I have opened a new course in Studio
+        And I am viewing the course team settings
+        And I'm the only course admin
+        Then I cannot delete or demote myself
+        When I add other user to the course team
+        And I make other user a course team admin
+        Then I can delete or demote myself
+        When I delete myself from the course team
+        And I am logged into studio
+        Then I do not see the course on my page
+        """
+        self.page.add_user_to_course(self.other_user.get('email'))
+        self._assert_user_present(self.other_user, present=True)
+
+        current = self.page.get_user(self.user.get('email'))
+        self.assertFalse(current.can_demote)
+        self.assertFalse(current.can_delete)
+        self.assertIn("Promote another member to Admin to remove your admin rights", current.no_change_warning_text)
+
+        other = self.page.get_user(self.other_user.get('email'))
+        other.click_promote()
+        self._expect_refresh()
+        other = self.page.get_user(self.other_user.get('email'))
+        self._assert_is_admin(other)
+
+        current = self.page.get_user(self.user.get('email'))
+        self.assertTrue(current.can_demote)
+        self.assertTrue(current.can_delete)
+        current.click_delete()
+
+        self.log_in(self.user)
+        self._assert_current_course(visible=False)
+
+    def test_admins_should_be_able_to_give_course_ownership_to_someone_else(self):
+        """
+        Scenario: Admins should be able to give course ownership to someone else
+        Given I have opened a new course in Studio
+        And I am viewing the course team settings
+        When I add other user to the course team
+        And I make other user a course team admin
+        When I remove admin rights from myself
+        Then I should not be marked as an admin
+        And I cannot manage users
+        And I cannot make myself a course team admin
+        When other user logs in
+        And he selects the new course
+        And he views the course team settings
+        And he deletes me from the course team
+        And I am logged into studio
+        Then I do not see the course on my page
+        """
+        self.page.add_user_to_course(self.other_user.get('email'))
+        self._assert_user_present(self.other_user, present=True)
+
+        current = self.page.get_user(self.user.get('email'))
+        self.assertFalse(current.can_demote)
+        self.assertFalse(current.can_delete)
+        self.assertIn("Promote another member to Admin to remove your admin rights", current.no_change_warning_text)
+
+        other = self.page.get_user(self.other_user.get('email'))
+        other.click_promote()
+        self._expect_refresh()
+
+        other = self.page.get_user(self.other_user.get('email'))
+        self._assert_is_admin(other)
+
+        current = self.page.get_user(self.user.get('email'))
+        self.assertTrue(current.can_demote)
+        self.assertTrue(current.can_delete)
+        current.click_demote()
+        self._expect_refresh()
+        current = self.page.get_user(self.user.get('email'))
+        self._assert_is_staff(current, can_manage=False)
+        self._assert_can_not_manage_users()
+        self.assertFalse(current.can_promote)
+
+        self.log_in(self.other_user)
+        self._go_to_course_team_page()
+
+        current = self.page.get_user(self.user.get('email'))
+        current.click_delete()
+        self._expect_refresh()
+        self._assert_user_present(self.user, present=False)
+
+        self.log_in(self.user)
+        self._assert_current_course(visible=False)
diff --git a/common/test/acceptance/tests/studio/test_studio_general.py b/common/test/acceptance/tests/studio/test_studio_general.py
index 7a3bc71..5766c02 100644
--- a/common/test/acceptance/tests/studio/test_studio_general.py
+++ b/common/test/acceptance/tests/studio/test_studio_general.py
@@ -14,7 +14,7 @@ from ...pages.studio.import_export import ExportCoursePage, ImportCoursePage
 from ...pages.studio.howitworks import HowitworksPage
 from ...pages.studio.index import DashboardPage
 from ...pages.studio.login import LoginPage
-from ...pages.studio.manage_users import CourseTeamPage
+from ...pages.studio.users import CourseTeamPage
 from ...pages.studio.overview import CourseOutlinePage
 from ...pages.studio.settings import SettingsPage
 from ...pages.studio.settings_advanced import AdvancedSettingsPage