Commit 350d1587 by Eric Fischer

Expand instructor definition

Per discussions, for the purposes of the teams API, an 'instructor'
is any of:
	-course staff
	-global staff
	-discussion privileged users
This change will include the last case, which previously did not have
instructor access. Changes will be documented on the teams API wiki:
https://openedx.atlassian.net/wiki/display/TNL/Team+API

Tests have also been added to confirm this functionality.

TNL-2984
parent 19604a4a
...@@ -375,6 +375,7 @@ class TestListTeamsAPI(TeamAPITestCase): ...@@ -375,6 +375,7 @@ class TestListTeamsAPI(TeamAPITestCase):
('student_enrolled', 200), ('student_enrolled', 200),
('staff', 200), ('staff', 200),
('course_staff', 200), ('course_staff', 200),
('community_ta', 200),
) )
@ddt.unpack @ddt.unpack
def test_access(self, user, status): def test_access(self, user, status):
...@@ -467,7 +468,8 @@ class TestCreateTeamAPI(TeamAPITestCase): ...@@ -467,7 +468,8 @@ class TestCreateTeamAPI(TeamAPITestCase):
('student_unenrolled', 403), ('student_unenrolled', 403),
('student_enrolled_not_on_team', 200), ('student_enrolled_not_on_team', 200),
('staff', 200), ('staff', 200),
('course_staff', 200) ('course_staff', 200),
('community_ta', 200),
) )
@ddt.unpack @ddt.unpack
def test_access(self, user, status): def test_access(self, user, status):
...@@ -580,6 +582,7 @@ class TestDetailTeamAPI(TeamAPITestCase): ...@@ -580,6 +582,7 @@ class TestDetailTeamAPI(TeamAPITestCase):
('student_enrolled', 200), ('student_enrolled', 200),
('staff', 200), ('staff', 200),
('course_staff', 200), ('course_staff', 200),
('community_ta', 200),
) )
@ddt.unpack @ddt.unpack
def test_access(self, user, status): def test_access(self, user, status):
...@@ -617,6 +620,7 @@ class TestUpdateTeamAPI(TeamAPITestCase): ...@@ -617,6 +620,7 @@ class TestUpdateTeamAPI(TeamAPITestCase):
('student_enrolled', 403), ('student_enrolled', 403),
('staff', 200), ('staff', 200),
('course_staff', 200), ('course_staff', 200),
('community_ta', 200),
) )
@ddt.unpack @ddt.unpack
def test_access(self, user, status): def test_access(self, user, status):
...@@ -631,6 +635,7 @@ class TestUpdateTeamAPI(TeamAPITestCase): ...@@ -631,6 +635,7 @@ class TestUpdateTeamAPI(TeamAPITestCase):
('student_enrolled', 404), ('student_enrolled', 404),
('staff', 404), ('staff', 404),
('course_staff', 404), ('course_staff', 404),
('community_ta', 404),
) )
@ddt.unpack @ddt.unpack
def test_access_bad_id(self, user, status): def test_access_bad_id(self, user, status):
...@@ -666,6 +671,7 @@ class TestListTopicsAPI(TeamAPITestCase): ...@@ -666,6 +671,7 @@ class TestListTopicsAPI(TeamAPITestCase):
('student_enrolled', 200), ('student_enrolled', 200),
('staff', 200), ('staff', 200),
('course_staff', 200), ('course_staff', 200),
('community_ta', 200),
) )
@ddt.unpack @ddt.unpack
def test_access(self, user, status): def test_access(self, user, status):
...@@ -733,6 +739,7 @@ class TestDetailTopicAPI(TeamAPITestCase): ...@@ -733,6 +739,7 @@ class TestDetailTopicAPI(TeamAPITestCase):
('student_enrolled', 200), ('student_enrolled', 200),
('staff', 200), ('staff', 200),
('course_staff', 200), ('course_staff', 200),
('community_ta', 200),
) )
@ddt.unpack @ddt.unpack
def test_access(self, user, status): def test_access(self, user, status):
...@@ -768,6 +775,7 @@ class TestListMembershipAPI(TeamAPITestCase): ...@@ -768,6 +775,7 @@ class TestListMembershipAPI(TeamAPITestCase):
('student_enrolled_both_courses_other_team', 200), ('student_enrolled_both_courses_other_team', 200),
('staff', 200), ('staff', 200),
('course_staff', 200), ('course_staff', 200),
('community_ta', 200),
) )
@ddt.unpack @ddt.unpack
def test_access(self, user, status): def test_access(self, user, status):
...@@ -784,6 +792,7 @@ class TestListMembershipAPI(TeamAPITestCase): ...@@ -784,6 +792,7 @@ class TestListMembershipAPI(TeamAPITestCase):
('student_enrolled_both_courses_other_team', 200, True), ('student_enrolled_both_courses_other_team', 200, True),
('staff', 200, True), ('staff', 200, True),
('course_staff', 200, True), ('course_staff', 200, True),
('community_ta', 200, True),
) )
@ddt.unpack @ddt.unpack
def test_access_by_username(self, user, status, has_content): def test_access_by_username(self, user, status, has_content):
...@@ -874,6 +883,7 @@ class TestCreateMembershipAPI(TeamAPITestCase): ...@@ -874,6 +883,7 @@ class TestCreateMembershipAPI(TeamAPITestCase):
('student_enrolled_both_courses_other_team', 404), ('student_enrolled_both_courses_other_team', 404),
('staff', 200), ('staff', 200),
('course_staff', 200), ('course_staff', 200),
('community_ta', 200),
) )
@ddt.unpack @ddt.unpack
def test_access(self, user, status): def test_access(self, user, status):
...@@ -948,6 +958,7 @@ class TestDetailMembershipAPI(TeamAPITestCase): ...@@ -948,6 +958,7 @@ class TestDetailMembershipAPI(TeamAPITestCase):
('student_enrolled', 200), ('student_enrolled', 200),
('staff', 200), ('staff', 200),
('course_staff', 200), ('course_staff', 200),
('community_ta', 200),
) )
@ddt.unpack @ddt.unpack
def test_access(self, user, status): def test_access(self, user, status):
...@@ -1013,6 +1024,7 @@ class TestDeleteMembershipAPI(TeamAPITestCase): ...@@ -1013,6 +1024,7 @@ class TestDeleteMembershipAPI(TeamAPITestCase):
('student_enrolled', 204), ('student_enrolled', 204),
('staff', 204), ('staff', 204),
('course_staff', 204), ('course_staff', 204),
('community_ta', 204),
) )
@ddt.unpack @ddt.unpack
def test_access(self, user, status): def test_access(self, user, status):
......
...@@ -51,7 +51,6 @@ from .serializers import ( ...@@ -51,7 +51,6 @@ from .serializers import (
) )
from .errors import AlreadyOnTeamInCourse, NotEnrolledInCourseForTeam from .errors import AlreadyOnTeamInCourse, NotEnrolledInCourseForTeam
TEAM_MEMBERSHIPS_PER_PAGE = 2 TEAM_MEMBERSHIPS_PER_PAGE = 2
TOPICS_PER_PAGE = 12 TOPICS_PER_PAGE = 12
...@@ -120,7 +119,7 @@ class TeamsDashboardView(View): ...@@ -120,7 +119,7 @@ class TeamsDashboardView(View):
def has_team_api_access(user, course_key, access_username=None): def has_team_api_access(user, course_key, access_username=None):
"""Returns True if the user has access to the Team API for the course """Returns True if the user has access to the Team API for the course
given by `course_key`. The user must either be enrolled in the course, given by `course_key`. The user must either be enrolled in the course,
be course staff, or be global staff. be course staff, be global staff, or have discussion privileges.
Args: Args:
user (User): The user to check access for. user (User): The user to check access for.
...@@ -134,6 +133,8 @@ def has_team_api_access(user, course_key, access_username=None): ...@@ -134,6 +133,8 @@ def has_team_api_access(user, course_key, access_username=None):
return True return True
if CourseStaffRole(course_key).has_user(user): if CourseStaffRole(course_key).has_user(user):
return True return True
if has_discussion_privileges(user, course_key):
return True
if not access_username or access_username == user.username: if not access_username or access_username == user.username:
return CourseEnrollment.is_enrolled(user, course_key) return CourseEnrollment.is_enrolled(user, course_key)
return False return False
...@@ -250,8 +251,9 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView): ...@@ -250,8 +251,9 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
If the user is not logged in, a 401 error is returned. If the user is not logged in, a 401 error is returned.
If the user is not enrolled in the course, or is not course or If the user is not enrolled in the course, is not course or
global staff, a 403 error is returned. global staff, or does not have discussion privileges a 403 error
is returned.
If the course_id is not valid or extra fields are included in the If the course_id is not valid or extra fields are included in the
request, a 400 error is returned. request, a 400 error is returned.
...@@ -467,8 +469,8 @@ class TeamsDetailView(ExpandableFieldViewMixin, RetrievePatchAPIView): ...@@ -467,8 +469,8 @@ class TeamsDetailView(ExpandableFieldViewMixin, RetrievePatchAPIView):
If the user is anonymous or inactive, a 401 is returned. If the user is anonymous or inactive, a 401 is returned.
If the user is logged in and the team does not exist, a 404 is returned. If the user is logged in and the team does not exist, a 404 is returned.
If the user is not course or global staff and the team does exist, If the user is not course or global staff, does not have discussion
a 403 is returned. privileges, and the team does exist, a 403 is returned.
If "application/merge-patch+json" is not the specified content type, If "application/merge-patch+json" is not the specified content type,
a 415 error is returned. a 415 error is returned.
...@@ -485,8 +487,20 @@ class TeamsDetailView(ExpandableFieldViewMixin, RetrievePatchAPIView): ...@@ -485,8 +487,20 @@ class TeamsDetailView(ExpandableFieldViewMixin, RetrievePatchAPIView):
"""Returns true if the user is enrolled or is staff.""" """Returns true if the user is enrolled or is staff."""
return has_team_api_access(request.user, obj.course_id) return has_team_api_access(request.user, obj.course_id)
class IsStaffOrPrivilegedOrReadOnly(IsStaffOrReadOnly):
"""Permission that checks to see if the user is global staff, course
staff, or has discussion privileges. If none of those conditions are
met, only read access will be granted.
"""
def has_object_permission(self, request, view, obj):
return (
has_discussion_privileges(request.user, obj.course_id) or
IsStaffOrReadOnly.has_object_permission(self, request, view, obj)
)
authentication_classes = (OAuth2Authentication, SessionAuthentication) authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated, IsStaffOrReadOnly, IsEnrolledOrIsStaff,) permission_classes = (permissions.IsAuthenticated, IsStaffOrPrivilegedOrReadOnly, IsEnrolledOrIsStaff,)
lookup_field = 'team_id' lookup_field = 'team_id'
serializer_class = CourseTeamSerializer serializer_class = CourseTeamSerializer
parser_classes = (MergePatchParser,) parser_classes = (MergePatchParser,)
...@@ -765,8 +779,9 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView): ...@@ -765,8 +779,9 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView):
**Response Values for POST** **Response Values for POST**
Any logged in user enrolled in a course can enroll themselves in a Any logged in user enrolled in a course can enroll themselves in a
team in the course. Course and global staff can enroll any user in team in the course. Course staff, global staff, and discussion
a team, with a few exceptions noted below. privileged users can enroll any user in a team, with a few
exceptions noted below.
If the user is not logged in and active, a 401 error is returned. If the user is not logged in and active, a 401 error is returned.
...@@ -775,11 +790,11 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView): ...@@ -775,11 +790,11 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView):
If the specified team does not exist, a 404 error is returned. If the specified team does not exist, a 404 error is returned.
If the user is not staff and is not enrolled in the course If the user is not staff, does not have discussion privileges,
associated with the team they are trying to join, or if they are and is not enrolled in the course associated with the team they
trying to add a user other than themselves to a team, a 404 error are trying to join, or if they are trying to add a user other
is returned. This is to prevent leaking information about the than themselves to a team, a 404 error is returned. This is to
existence of teams and users. prevent leaking information about the existence of teams and users.
If the specified user does not exist, a 404 error is returned. If the specified user does not exist, a 404 error is returned.
...@@ -789,7 +804,8 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView): ...@@ -789,7 +804,8 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView):
If the user is not enrolled in the course associated with the team If the user is not enrolled in the course associated with the team
they are trying to join, a 400 error is returned. This can occur they are trying to join, a 400 error is returned. This can occur
when a staff user posts a request adding another user to a team. when a staff or discussion privileged user posts a request adding
another user to a team.
""" """
authentication_classes = (OAuth2Authentication, SessionAuthentication) authentication_classes = (OAuth2Authentication, SessionAuthentication)
...@@ -961,18 +977,19 @@ class MembershipDetailView(ExpandableFieldViewMixin, GenericAPIView): ...@@ -961,18 +977,19 @@ class MembershipDetailView(ExpandableFieldViewMixin, GenericAPIView):
**Response Values for DELETE** **Response Values for DELETE**
Any logged in user enrolled in a course can remove themselves from Any logged in user enrolled in a course can remove themselves from
a team in the course. Course and global staff can remove any user a team in the course. Course staff, global staff, and discussion
from a team. Successfully deleting a membership will return a 204 privileged users can remove any user from a team. Successfully
response with no content. deleting a membership will return a 204 response with no content.
If the user is not logged in and active, a 401 error is returned. If the user is not logged in and active, a 401 error is returned.
If the specified team or username does not exist, a 404 error is If the specified team or username does not exist, a 404 error is
returned. returned.
If the user is not staff and is attempting to remove another user If the user is not staff or a discussion privileged user and is
from a team, a 404 error is returned. This prevents leaking attempting to remove another user from a team, a 404 error is
information about team and user existence. returned. This prevents leaking information about team and user
existence.
If the membership does not exist, a 404 error is returned. If the membership does not exist, a 404 error is returned.
""" """
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment