Account API: enforce Enterprise policy on backend
This commit is contained in:
@@ -41,6 +41,7 @@ from openedx.core.djangoapps.user_api.errors import (
|
||||
)
|
||||
from openedx.core.djangoapps.user_api.preferences.api import update_user_preferences
|
||||
from openedx.core.lib.api.view_utils import add_serializer_errors
|
||||
from openedx.features.enterprise_support.utils import get_enterprise_readonly_account_fields
|
||||
|
||||
from .serializers import (
|
||||
AccountLegacyProfileSerializer, AccountUserSerializer,
|
||||
@@ -143,6 +144,25 @@ def update_account_settings(requesting_user, update, username=None):
|
||||
if requesting_user.username != username:
|
||||
raise errors.UserNotAuthorized()
|
||||
|
||||
# Check for fields that are not editable. Marking them read-only causes them to be ignored, but we wish to 400.
|
||||
read_only_fields = set(update.keys()).intersection(
|
||||
# Remove email since it is handled separately below when checking for changing_email.
|
||||
(set(AccountUserSerializer.get_read_only_fields()) - set(["email"])) |
|
||||
set(AccountLegacyProfileSerializer.get_read_only_fields() or set()) |
|
||||
get_enterprise_readonly_account_fields(existing_user)
|
||||
)
|
||||
|
||||
# Build up all field errors, whether read-only, validation, or email errors.
|
||||
field_errors = {}
|
||||
|
||||
if read_only_fields:
|
||||
for read_only_field in read_only_fields:
|
||||
field_errors[read_only_field] = {
|
||||
"developer_message": u"This field is not editable via this API",
|
||||
"user_message": _(u"The '{field_name}' field cannot be edited.").format(field_name=read_only_field)
|
||||
}
|
||||
del update[read_only_field]
|
||||
|
||||
# If user has requested to change email, we must call the multi-step process to handle this.
|
||||
# It is not handled by the serializer (which considers email to be read-only).
|
||||
changing_email = False
|
||||
@@ -163,22 +183,6 @@ def update_account_settings(requesting_user, update, username=None):
|
||||
if "secondary_email" in update:
|
||||
changing_secondary_email = True
|
||||
|
||||
# Check for fields that are not editable. Marking them read-only causes them to be ignored, but we wish to 400.
|
||||
read_only_fields = set(update.keys()).intersection(
|
||||
AccountUserSerializer.get_read_only_fields() + AccountLegacyProfileSerializer.get_read_only_fields()
|
||||
)
|
||||
|
||||
# Build up all field errors, whether read-only, validation, or email errors.
|
||||
field_errors = {}
|
||||
|
||||
if read_only_fields:
|
||||
for read_only_field in read_only_fields:
|
||||
field_errors[read_only_field] = {
|
||||
"developer_message": u"This field is not editable via this API",
|
||||
"user_message": _(u"The '{field_name}' field cannot be edited.").format(field_name=read_only_field)
|
||||
}
|
||||
del update[read_only_field]
|
||||
|
||||
user_serializer = AccountUserSerializer(existing_user, data=update)
|
||||
legacy_profile_serializer = AccountLegacyProfileSerializer(existing_user_profile, data=update)
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ def account_settings_context(request):
|
||||
'beta_language': beta_language
|
||||
}
|
||||
|
||||
enterprise_customer = get_enterprise_customer_for_learner(site=request.site, user=request.user)
|
||||
enterprise_customer = get_enterprise_customer_for_learner(user=request.user)
|
||||
update_account_settings_context_for_enterprise(context, enterprise_customer)
|
||||
|
||||
if third_party_auth.is_enabled():
|
||||
|
||||
@@ -58,6 +58,7 @@ from openedx.core.djangoapps.user_api.errors import (
|
||||
UserNotFound
|
||||
)
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from openedx.features.enterprise_support.tests.factories import EnterpriseCustomerUserFactory
|
||||
from student.models import PendingEmailChange
|
||||
from student.tests.factories import UserFactory
|
||||
from student.tests.tests import UserSettingsEventTestMixin
|
||||
@@ -69,6 +70,7 @@ def mock_render_to_string(template_name, context):
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
@ddt.ddt
|
||||
class TestAccountApi(UserSettingsEventTestMixin, EmailTemplateTagMixin, RetirementTestCase):
|
||||
"""
|
||||
These tests specifically cover the parts of the API methods that are not covered by test_views.py.
|
||||
@@ -220,6 +222,31 @@ class TestAccountApi(UserSettingsEventTestMixin, EmailTemplateTagMixin, Retireme
|
||||
with self.assertRaises(AccountUpdateError):
|
||||
update_account_settings(self.user, {"social_links": social_links})
|
||||
|
||||
def test_update_success_for_enterprise(self):
|
||||
EnterpriseCustomerUserFactory(user_id=self.user.id)
|
||||
level_of_education = "m"
|
||||
successful_update = {
|
||||
"level_of_education": level_of_education,
|
||||
}
|
||||
update_account_settings(self.user, successful_update)
|
||||
account_settings = get_account_settings(self.default_request)[0]
|
||||
self.assertEqual(level_of_education, account_settings["level_of_education"])
|
||||
|
||||
@ddt.data(
|
||||
("email", "new_email@example.com"),
|
||||
("name", "New Name"),
|
||||
("country", "New Country"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_update_validation_error_for_enterprise(self, field_name, field_value):
|
||||
EnterpriseCustomerUserFactory(user_id=self.user.id)
|
||||
update_data = {field_name: field_value}
|
||||
|
||||
with self.assertRaises(AccountValidationError) as validation_error:
|
||||
update_account_settings(self.user, update_data)
|
||||
field_errors = validation_error.exception.field_errors
|
||||
self.assertEqual("This field is not editable via this API", field_errors[field_name]["developer_message"])
|
||||
|
||||
def test_update_error_validating(self):
|
||||
"""Test that AccountValidationError is thrown if incorrect values are supplied."""
|
||||
with self.assertRaises(AccountValidationError):
|
||||
|
||||
@@ -165,6 +165,9 @@ class AccountViewSet(ViewSet):
|
||||
* email: Email address for the user. New email addresses must be confirmed
|
||||
via a confirmation email, so GET does not reflect the change until
|
||||
the address has been confirmed.
|
||||
* secondary_email: A secondary email address for the user. Unlike
|
||||
the email field, GET will reflect the latest update to this field
|
||||
even if changes have yet to be confirmed.
|
||||
* gender: One of the following values:
|
||||
|
||||
* null
|
||||
|
||||
@@ -554,7 +554,7 @@ def get_enterprise_learner_data(user):
|
||||
|
||||
|
||||
@enterprise_is_enabled(otherwise={})
|
||||
def get_enterprise_customer_for_learner(site, user):
|
||||
def get_enterprise_customer_for_learner(user):
|
||||
"""
|
||||
Return enterprise customer to whom given learner belongs.
|
||||
"""
|
||||
|
||||
@@ -260,6 +260,13 @@ def update_account_settings_context_for_enterprise(context, enterprise_customer)
|
||||
context.update(enterprise_context)
|
||||
|
||||
|
||||
def get_enterprise_readonly_account_fields(user):
|
||||
"""
|
||||
Returns a set of account fields that are read-only for enterprise users.
|
||||
"""
|
||||
return set(settings.ENTERPRISE_READONLY_ACCOUNT_FIELDS) if is_enterprise_learner(user) else set()
|
||||
|
||||
|
||||
def get_enterprise_learner_generic_name(request):
|
||||
"""
|
||||
Get a generic name concatenating the Enterprise Customer name and 'Learner'.
|
||||
|
||||
Reference in New Issue
Block a user