Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3172,3 +3172,15 @@ def _should_send_certificate_events(settings):
SSL_AUTH_DN_FORMAT_STRING = (
"/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}"
)

# .. setting_name: OPEN_EDX_FILTERS_CONFIG
# .. setting_default: {}
# .. setting_description: Configuration dict for openedx-filters pipeline steps.
# Keys are filter type strings; values are dicts with 'fail_silently' (bool) and
# 'pipeline' (list of dotted-path strings to PipelineStep subclasses).
OPEN_EDX_FILTERS_CONFIG = {
"org.openedx.learning.account.settings.read_only_fields.requested.v1": {
"fail_silently": True,
"pipeline": ["enterprise.filters.accounts.AccountSettingsReadOnlyFieldsStep"],
},
}
11 changes: 8 additions & 3 deletions openedx/core/djangoapps/user_api/accounts/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@

import datetime
import re
from zoneinfo import ZoneInfo

from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import ValidationError, validate_email
from django.utils.translation import gettext as _
from django.utils.translation import override as override_language
from eventtracking import tracker
from zoneinfo import ZoneInfo
from openedx_filters.learning.filters import AccountSettingsReadOnlyFieldsRequested

from common.djangoapps.student import views as student_views
from common.djangoapps.student.models import (
Expand All @@ -38,7 +39,6 @@
from openedx.core.djangoapps.user_authn.utils import check_pwned_password
from openedx.core.djangoapps.user_authn.views.registration_form import validate_name, validate_username
from openedx.core.lib.api.view_utils import add_serializer_errors
from openedx.features.enterprise_support.utils import get_enterprise_readonly_account_fields
from openedx.features.name_affirmation_api.utils import is_name_affirmation_installed

from .serializers import AccountLegacyProfileSerializer, AccountUserSerializer, UserReadOnlySerializer, _visible_fields
Expand Down Expand Up @@ -193,11 +193,16 @@ def update_account_settings(requesting_user, update, username=None):

def _validate_read_only_fields(user, data, field_errors):
# Check for fields that are not editable. Marking them read-only causes them to be ignored, but we wish to 400.
plugin_readonly_fields = AccountSettingsReadOnlyFieldsRequested.run_filter(
readonly_fields=set(),
user=user,
) or set()

read_only_fields = set(data.keys()).intersection(
# Remove email since it is handled separately below when checking for changing_email.
(set(AccountUserSerializer.get_read_only_fields()) - {"email"}) |
set(AccountLegacyProfileSerializer.get_read_only_fields() or set()) |
get_enterprise_readonly_account_fields(user)
plugin_readonly_fields
)

for read_only_field in read_only_fields:
Expand Down
92 changes: 19 additions & 73 deletions openedx/core/djangoapps/user_api/accounts/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"""

import datetime
import itertools
import unicodedata
from unittest.mock import Mock, patch
from zoneinfo import ZoneInfo

import ddt
import pytest
Expand All @@ -17,8 +17,6 @@
from django.test import TestCase
from django.test.client import RequestFactory
from django.urls import reverse
from zoneinfo import ZoneInfo
from social_django.models import UserSocialAuth

from common.djangoapps.student.models import (
AccountRecovery,
Expand Down Expand Up @@ -104,10 +102,12 @@ def setUp(self):
self.staff_user = UserFactory(is_staff=True, password=self.password)
self.reset_tracker()

enterprise_patcher = patch('openedx.features.enterprise_support.api.enterprise_customer_for_request')
enterprise_learner_patcher = enterprise_patcher.start()
enterprise_learner_patcher.return_value = {}
self.addCleanup(enterprise_learner_patcher.stop)
filter_patcher = patch(
'openedx.core.djangoapps.user_api.accounts.api.AccountSettingsReadOnlyFieldsRequested.run_filter',
return_value=set(),
)
filter_patcher.start()
self.addCleanup(filter_patcher.stop)

def test_get_username_provided(self):
"""Test the difference in behavior when a username is supplied to get_account_settings."""
Expand Down Expand Up @@ -248,73 +248,19 @@ def test_update_success_for_enterprise(self):
account_settings = get_account_settings(self.default_request)[0]
assert level_of_education == account_settings['level_of_education']

@patch('openedx.features.enterprise_support.api.enterprise_customer_for_request')
@patch('openedx.features.enterprise_support.utils.third_party_auth.provider.Registry.get')
@ddt.data(
*itertools.product(
# field_name_value values
(("email", "new_email@example.com"), ("name", "new name"), ("country", "IN")),
# is_enterprise_user
(True, False),
# is_synch_learner_profile_data
(True, False),
# has `UserSocialAuth` record
(True, False),
)
@patch(
'openedx.core.djangoapps.user_api.accounts.api.AccountSettingsReadOnlyFieldsRequested.run_filter',
return_value={'country'},
)
@ddt.unpack
def test_update_validation_error_for_enterprise(
self,
field_name_value,
is_enterprise_user,
is_synch_learner_profile_data,
has_user_social_auth_record,
mock_auth_provider,
mock_customer,
):
idp_backend_name = 'tpa-saml'
mock_customer.return_value = {}
if is_enterprise_user:
mock_customer.return_value.update({
'uuid': 'real-ent-uuid',
'name': 'Dummy Enterprise',
'identity_provider': 'saml-ubc',
'identity_providers': [
{
"provider_id": "saml-ubc",
}
],
})
mock_auth_provider.return_value.sync_learner_profile_data = is_synch_learner_profile_data
mock_auth_provider.return_value.backend_name = idp_backend_name

update_data = {field_name_value[0]: field_name_value[1]}

user_fullname_editable = False
if has_user_social_auth_record:
UserSocialAuth.objects.create(
provider=idp_backend_name,
user=self.user
)
else:
UserSocialAuth.objects.all().delete()
# user's fullname is editable if no `UserSocialAuth` record exists
user_fullname_editable = field_name_value[0] == 'name'

# prevent actual email change requests
with patch('openedx.core.djangoapps.user_api.accounts.api.student_views.do_email_change_request'):
# expect field un-editability only when all of the following conditions are met
if is_enterprise_user and is_synch_learner_profile_data and not user_fullname_editable:
with pytest.raises(AccountValidationError) as validation_error:
update_account_settings(self.user, update_data)
field_errors = validation_error.value.field_errors
assert 'This field is not editable via this API' == \
field_errors[field_name_value[0]]['developer_message']
else:
update_account_settings(self.user, update_data)
account_settings = get_account_settings(self.default_request)[0]
if field_name_value[0] != "email":
assert field_name_value[1] == account_settings[field_name_value[0]]
def test_readonly_field_from_filter_is_rejected(self, mock_run_filter): # pylint: disable=unused-argument
"""
When AccountSettingsReadOnlyFieldsRequested.run_filter returns a field as read-only,
update_account_settings should raise AccountValidationError for that field.
"""
with pytest.raises(AccountValidationError) as exc_info:
update_account_settings(self.user, {"country": "IN"})
field_errors = exc_info.value.field_errors
assert 'This field is not editable via this API' == field_errors['country']['developer_message']

def test_update_error_validating(self):
"""Test that AccountValidationError is thrown if incorrect values are supplied."""
Expand Down
Loading