Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
7fe8e475
Commit
7fe8e475
authored
Nov 24, 2016
by
Vedran Karacic
Committed by
Marko Jevtic
Jan 31, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[SOL-2133] Add user deactivation endpoint.
parent
c2d7e446
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
148 additions
and
24 deletions
+148
-24
common/djangoapps/student/migrations/0009_auto_20170111_0422.py
+18
-0
common/djangoapps/student/models.py
+1
-0
common/djangoapps/student/tests/factories.py
+23
-1
openedx/core/djangoapps/user_api/accounts/permissions.py
+15
-0
openedx/core/djangoapps/user_api/accounts/tests/test_permissions.py
+40
-0
openedx/core/djangoapps/user_api/accounts/tests/test_views.py
+0
-0
openedx/core/djangoapps/user_api/accounts/views.py
+25
-2
openedx/core/djangoapps/user_api/preferences/tests/test_views.py
+20
-20
openedx/core/djangoapps/user_api/urls.py
+6
-1
No files found.
common/djangoapps/student/migrations/0009_auto_20170111_0422.py
0 → 100644
View file @
7fe8e475
# -*- coding: utf-8 -*-
from
__future__
import
unicode_literals
from
django.db
import
migrations
,
models
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'student'
,
'0008_auto_20161117_1209'
),
]
operations
=
[
migrations
.
AlterModelOptions
(
name
=
'userprofile'
,
options
=
{
'permissions'
:
((
'can_deactivate_users'
,
'Can deactivate, but NOT delete users'
),)},
),
]
common/djangoapps/student/models.py
View file @
7fe8e475
...
...
@@ -234,6 +234,7 @@ class UserProfile(models.Model):
class
Meta
(
object
):
db_table
=
"auth_userprofile"
permissions
=
((
"can_deactivate_users"
,
"Can deactivate, but NOT delete users"
),)
# CRITICAL TODO/SECURITY
# Sanitize all fields.
...
...
common/djangoapps/student/tests/factories.py
View file @
7fe8e475
...
...
@@ -6,7 +6,8 @@ from student.models import (User, UserProfile, Registration,
PendingEmailChange
,
UserStanding
,
CourseAccessRole
)
from
course_modes.models
import
CourseMode
from
django.contrib.auth.models
import
Group
,
AnonymousUser
from
django.contrib.auth.models
import
AnonymousUser
,
Group
,
Permission
from
django.contrib.contenttypes.models
import
ContentType
from
datetime
import
datetime
import
factory
from
factory
import
lazy_attribute
...
...
@@ -18,6 +19,8 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
# Factories are self documenting
# pylint: disable=missing-docstring
TEST_PASSWORD
=
'test'
class
GroupFactory
(
DjangoModelFactory
):
class
Meta
(
object
):
...
...
@@ -123,6 +126,10 @@ class AdminFactory(UserFactory):
is_staff
=
True
class
SuperuserFactory
(
UserFactory
):
is_superuser
=
True
class
CourseEnrollmentFactory
(
DjangoModelFactory
):
class
Meta
(
object
):
model
=
CourseEnrollment
...
...
@@ -161,3 +168,18 @@ class PendingEmailChangeFactory(DjangoModelFactory):
user
=
factory
.
SubFactory
(
UserFactory
)
new_email
=
factory
.
Sequence
(
u'new+email+{0}@edx.org'
.
format
)
activation_key
=
factory
.
Sequence
(
u'{:0<30d}'
.
format
)
class
ContentTypeFactory
(
DjangoModelFactory
):
class
Meta
(
object
):
model
=
ContentType
app_label
=
factory
.
Faker
(
'app_name'
)
class
PermissionFactory
(
DjangoModelFactory
):
class
Meta
(
object
):
model
=
Permission
codename
=
factory
.
Faker
(
'codename'
)
content_type
=
factory
.
SubFactory
(
ContentTypeFactory
)
openedx/core/djangoapps/user_api/accounts/permissions.py
0 → 100644
View file @
7fe8e475
"""
Permissions classes for User accounts API views.
"""
from
__future__
import
unicode_literals
from
rest_framework
import
permissions
class
CanDeactivateUser
(
permissions
.
BasePermission
):
"""
Grants access to AccountDeactivationView if the requesting user is a superuser
or has the explicit permission to deactivate a User account.
"""
def
has_permission
(
self
,
request
,
view
):
return
request
.
user
.
has_perm
(
'student.can_deactivate_users'
)
openedx/core/djangoapps/user_api/accounts/tests/test_permissions.py
0 → 100644
View file @
7fe8e475
"""
Tests for User deactivation API permissions
"""
from
django.test
import
TestCase
,
RequestFactory
from
openedx.core.djangoapps.user_api.accounts.permissions
import
CanDeactivateUser
from
student.tests.factories
import
ContentTypeFactory
,
PermissionFactory
,
SuperuserFactory
,
UserFactory
class
CanDeactivateUserTest
(
TestCase
):
""" Tests for user deactivation API permissions """
def
setUp
(
self
):
super
(
CanDeactivateUserTest
,
self
)
.
setUp
()
self
.
request
=
RequestFactory
()
.
get
(
'/test/url'
)
def
test_api_permission_superuser
(
self
):
self
.
request
.
user
=
SuperuserFactory
()
result
=
CanDeactivateUser
()
.
has_permission
(
self
.
request
,
None
)
self
.
assertTrue
(
result
)
def
test_api_permission_user_granted_permission
(
self
):
user
=
UserFactory
()
permission
=
PermissionFactory
(
codename
=
'can_deactivate_users'
,
content_type
=
ContentTypeFactory
(
app_label
=
'student'
)
)
user
.
user_permissions
.
add
(
permission
)
# pylint: disable=no-member
self
.
request
.
user
=
user
result
=
CanDeactivateUser
()
.
has_permission
(
self
.
request
,
None
)
self
.
assertTrue
(
result
)
def
test_api_permission_user_without_permission
(
self
):
self
.
request
.
user
=
UserFactory
()
result
=
CanDeactivateUser
()
.
has_permission
(
self
.
request
,
None
)
self
.
assertFalse
(
result
)
openedx/core/djangoapps/user_api/accounts/tests/test_views.py
View file @
7fe8e475
This diff is collapsed.
Click to expand it.
openedx/core/djangoapps/user_api/accounts/views.py
View file @
7fe8e475
...
...
@@ -10,15 +10,18 @@ from edx_rest_framework_extensions.authentication import JwtAuthentication
from
rest_framework
import
permissions
from
rest_framework
import
status
from
rest_framework.response
import
Response
from
rest_framework.views
import
APIView
from
rest_framework.viewsets
import
ViewSet
from
.api
import
get_account_settings
,
update_account_settings
from
.permissions
import
CanDeactivateUser
from
..errors
import
UserNotFound
,
UserNotAuthorized
,
AccountUpdateError
,
AccountValidationError
from
openedx.core.lib.api.authentication
import
(
SessionAuthenticationAllowInactiveUser
,
OAuth2AuthenticationAllowInactiveUser
,
)
from
openedx.core.lib.api.parsers
import
MergePatchParser
from
.api
import
get_account_settings
,
update_account_settings
from
..errors
import
UserNotFound
,
UserNotAuthorized
,
AccountUpdateError
,
AccountValidationError
from
student.models
import
User
class
AccountViewSet
(
ViewSet
):
...
...
@@ -219,3 +222,23 @@ class AccountViewSet(ViewSet):
)
return
Response
(
account_settings
)
class
AccountDeactivationView
(
APIView
):
"""
Account deactivation viewset. Currently only supports POST requests.
Only admins can deactivate accounts.
"""
permission_classes
=
(
permissions
.
IsAuthenticated
,
CanDeactivateUser
)
def
post
(
self
,
request
,
username
):
"""
POST /api/user/v1/accounts/{username}/deactivate/
Marks the user as having no password set for deactivation purposes.
"""
user
=
User
.
objects
.
get
(
username
=
username
)
user
.
set_unusable_password
()
user
.
save
()
account_settings
=
get_account_settings
(
request
,
[
username
])[
0
]
return
Response
(
account_settings
)
openedx/core/djangoapps/user_api/preferences/tests/test_views.py
View file @
7fe8e475
...
...
@@ -10,7 +10,7 @@ from mock import patch
from
django.core.urlresolvers
import
reverse
from
django.test.testcases
import
TransactionTestCase
from
rest_framework.test
import
APIClient
from
student.tests.factories
import
UserFactory
from
student.tests.factories
import
UserFactory
,
TEST_PASSWORD
from
openedx.core.djangolib.testing.utils
import
skip_unless_lms
from
...accounts.tests.test_views
import
UserAPITestCase
...
...
@@ -42,7 +42,7 @@ class TestPreferencesAPI(UserAPITestCase):
"""
Test that DELETE, POST, and PUT are not supported.
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
self
.
assertEqual
(
405
,
self
.
client
.
put
(
self
.
url
)
.
status_code
)
self
.
assertEqual
(
405
,
self
.
client
.
post
(
self
.
url
)
.
status_code
)
self
.
assertEqual
(
405
,
self
.
client
.
delete
(
self
.
url
)
.
status_code
)
...
...
@@ -51,7 +51,7 @@ class TestPreferencesAPI(UserAPITestCase):
"""
Test that a client (logged in) cannot get the preferences information for a different client.
"""
self
.
different_client
.
login
(
username
=
self
.
different_user
.
username
,
password
=
self
.
test_password
)
self
.
different_client
.
login
(
username
=
self
.
different_user
.
username
,
password
=
TEST_PASSWORD
)
self
.
send_get
(
self
.
different_client
,
expected_status
=
404
)
@ddt.data
(
...
...
@@ -72,7 +72,7 @@ class TestPreferencesAPI(UserAPITestCase):
Test that a client (logged in) can get her own preferences information (verifying the default
state before any preferences are stored).
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
response
=
self
.
send_get
(
self
.
client
)
self
.
assertEqual
({},
response
.
data
)
...
...
@@ -117,7 +117,7 @@ class TestPreferencesAPI(UserAPITestCase):
"""
Test the behavior of patch when an incorrect content_type is specified.
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
self
.
send_patch
(
self
.
client
,
{},
content_type
=
"application/json"
,
expected_status
=
415
)
self
.
send_patch
(
self
.
client
,
{},
content_type
=
"application/xml"
,
expected_status
=
415
)
...
...
@@ -137,7 +137,7 @@ class TestPreferencesAPI(UserAPITestCase):
"""
Internal helper to generalize the creation of a set of preferences
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
if
not
is_active
:
self
.
user
.
is_active
=
False
self
.
user
.
save
()
...
...
@@ -182,7 +182,7 @@ class TestPreferencesAPI(UserAPITestCase):
set_user_preference
(
self
.
user
,
"time_zone"
,
"Asia/Macau"
)
# Send the patch request
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
self
.
send_patch
(
self
.
client
,
{
...
...
@@ -215,7 +215,7 @@ class TestPreferencesAPI(UserAPITestCase):
set_user_preference
(
self
.
user
,
"time_zone"
,
"Pacific/Midway"
)
# Send the patch request
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
response
=
self
.
send_patch
(
self
.
client
,
{
...
...
@@ -266,7 +266,7 @@ class TestPreferencesAPI(UserAPITestCase):
"""
Test that a client (logged in) receives appropriate errors for a bad request.
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
# Verify a non-dict request
response
=
self
.
send_patch
(
self
.
client
,
"non_dict_request"
,
expected_status
=
400
)
...
...
@@ -325,7 +325,7 @@ class TestPreferencesAPITransactions(TransactionTestCase):
def
setUp
(
self
):
super
(
TestPreferencesAPITransactions
,
self
)
.
setUp
()
self
.
client
=
APIClient
()
self
.
user
=
UserFactory
.
create
(
password
=
self
.
test_password
)
self
.
user
=
UserFactory
.
create
(
password
=
TEST_PASSWORD
)
self
.
url
=
reverse
(
"preferences_api"
,
kwargs
=
{
'username'
:
self
.
user
.
username
})
@patch
(
'openedx.core.djangoapps.user_api.models.UserPreference.delete'
)
...
...
@@ -342,7 +342,7 @@ class TestPreferencesAPITransactions(TransactionTestCase):
# after one of the updates has happened, in which case the whole operation
# should be rolled back.
delete_user_preference
.
side_effect
=
[
Exception
,
None
]
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
json_data
=
{
"a"
:
"2"
,
"b"
:
None
,
...
...
@@ -396,7 +396,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
"""
Test that POST and PATCH are not supported.
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
self
.
assertEqual
(
405
,
self
.
client
.
post
(
self
.
url
)
.
status_code
)
self
.
assertEqual
(
405
,
self
.
client
.
patch
(
self
.
url
)
.
status_code
)
...
...
@@ -404,7 +404,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
"""
Test that a client (logged in) cannot manipulate a preference for a different client.
"""
self
.
different_client
.
login
(
username
=
self
.
different_user
.
username
,
password
=
self
.
test_password
)
self
.
different_client
.
login
(
username
=
self
.
different_user
.
username
,
password
=
TEST_PASSWORD
)
self
.
send_get
(
self
.
different_client
,
expected_status
=
404
)
self
.
send_put
(
self
.
different_client
,
"new_value"
,
expected_status
=
404
)
self
.
send_delete
(
self
.
different_client
,
expected_status
=
404
)
...
...
@@ -429,7 +429,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
Test that a 404 is returned if the user does not have a preference with the given preference_key.
"""
self
.
_set_url
(
"does_not_exist"
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
response
=
self
.
send_get
(
self
.
client
,
expected_status
=
404
)
self
.
assertIsNone
(
response
.
data
)
...
...
@@ -469,7 +469,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
"""
Generalization of the actual test workflow
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
if
not
is_active
:
self
.
user
.
is_active
=
False
self
.
user
.
save
()
...
...
@@ -490,7 +490,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
Test that a client (logged in) cannot create an empty preference.
"""
self
.
_set_url
(
"new_key"
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
response
=
self
.
send_put
(
self
.
client
,
preference_value
,
expected_status
=
400
)
self
.
assertEqual
(
response
.
data
,
...
...
@@ -505,7 +505,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
"""
Test that a client cannot create preferences with bad keys
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
too_long_preference_key
=
"x"
*
256
new_value
=
"new value"
...
...
@@ -544,7 +544,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
"""
Test that a client (logged in) can update a preference.
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
self
.
send_put
(
self
.
client
,
preference_value
)
response
=
self
.
send_get
(
self
.
client
)
self
.
assertEqual
(
unicode
(
preference_value
),
response
.
data
)
...
...
@@ -572,7 +572,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
"""
Test that a client (logged in) cannot update a preference to null.
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
response
=
self
.
send_put
(
self
.
client
,
preference_value
,
expected_status
=
400
)
self
.
assertEqual
(
response
.
data
,
...
...
@@ -588,7 +588,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
"""
Test that a client (logged in) can delete her own preference.
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
# Verify that a preference can be deleted
self
.
send_delete
(
self
.
client
)
...
...
openedx/core/djangoapps/user_api/urls.py
View file @
7fe8e475
...
...
@@ -6,7 +6,7 @@ from django.conf import settings
from
django.conf.urls
import
patterns
,
url
from
..profile_images.views
import
ProfileImageView
from
.accounts.views
import
AccountViewSet
from
.accounts.views
import
Account
DeactivationView
,
Account
ViewSet
from
.preferences.views
import
PreferencesView
,
PreferencesDetailView
from
.verification_api.views
import
PhotoVerificationStatusView
...
...
@@ -34,6 +34,11 @@ urlpatterns = patterns(
name
=
'accounts_profile_image_api'
),
url
(
r'^v1/accounts/{}/deactivate/$'
.
format
(
settings
.
USERNAME_PATTERN
),
AccountDeactivationView
.
as_view
(),
name
=
'accounts_deactivation'
),
url
(
r'^v1/accounts/{}/verification_status/$'
.
format
(
settings
.
USERNAME_PATTERN
),
PhotoVerificationStatusView
.
as_view
(),
name
=
'verification_status'
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment