Skip to content

Commit 797bdc0

Browse files
authored
Merge branch 'master' into pwnage101/ENT-11510
2 parents fea50a5 + b03a0b8 commit 797bdc0

File tree

14 files changed

+172
-59
lines changed

14 files changed

+172
-59
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ venv/
9797
#enterprise/static/enterprise/bundles/
9898

9999
# claude code personalization
100+
CLAUDE.md
100101
.claude/settings.local.json
101102

102103
# Ralph

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ Unreleased
2121
---------------------
2222
* feat: add AccountSettingsEnterpriseReadOnlyFieldsStep pipeline step (ENT-11510)
2323

24+
[6.8.4] - 2026-03-31
25+
--------------------
26+
* fix: hard delete customer admin records from API
27+
2428
[6.8.3] - 2026-03-27
2529
---------------------
2630
* fix: Move settings reads out of AppConfig, into consumers

enterprise/api/utils.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from enterprise.constants import (
1515
BRAZE_ADMIN_INVITE_CAMPAIGN_SETTING,
1616
BRAZE_LEARNER_INVITE_CAMPAIGN_SETTING,
17-
ENTERPRISE_ADMIN_ROLE,
1817
ENTERPRISE_CATALOG_ADMIN_ROLE,
1918
ENTERPRISE_DASHBOARD_ADMIN_ROLE,
2019
ENTERPRISE_REPORTING_CONFIG_ADMIN_ROLE,
@@ -31,7 +30,6 @@
3130
EnterpriseFeatureUserRoleAssignment,
3231
EnterpriseGroup,
3332
PendingEnterpriseCustomerAdminUser,
34-
SystemWideEnterpriseUserRoleAssignment,
3533
)
3634
from enterprise.tasks import send_enterprise_admin_invite_email
3735

@@ -45,13 +43,12 @@ def get_existing_admin_emails(enterprise_customer: EnterpriseCustomer) -> Set[st
4543
Only includes admins who have:
4644
1. An EnterpriseCustomerAdmin record
4745
2. An active EnterpriseCustomerUser (active=True)
48-
3. An active admin role assignment in SystemWideEnterpriseUserRoleAssignment
4946
5047
Args:
5148
enterprise_customer: The enterprise customer instance.
5249
5350
Returns:
54-
Set of lowercased email addresses of active admins with valid role assignments.
51+
Set of lowercased email addresses of active admins.
5552
5653
Raises:
5754
DatabaseError: If database query fails.
@@ -62,20 +59,12 @@ def get_existing_admin_emails(enterprise_customer: EnterpriseCustomer) -> Set[st
6259
True
6360
"""
6461
try:
65-
# Get user IDs with active admin role assignments
66-
users_with_admin_role = set(
67-
SystemWideEnterpriseUserRoleAssignment.objects.filter(
68-
enterprise_customer=enterprise_customer,
69-
role__name=ENTERPRISE_ADMIN_ROLE,
70-
).values_list('user_id', flat=True)
71-
)
72-
73-
# Return emails of admins who have active ECU AND active role assignment
62+
# Return emails of admins who have active ECU
63+
# Roles are not considered in customer admin lookup
7464
return set(
7565
EnterpriseCustomerAdmin.objects.filter(
7666
enterprise_customer_user__enterprise_customer=enterprise_customer,
77-
enterprise_customer_user__active=True,
78-
enterprise_customer_user__user_id__in=users_with_admin_role,
67+
enterprise_customer_user__active=True
7968
)
8069
.annotate(email_l=Lower(F("enterprise_customer_user__user_fk__email")))
8170
.values_list("email_l", flat=True)

enterprise/api/v1/views/enterprise_customer_admin.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,14 @@ def _delete_active_admin(self, enterprise_customer_uuid, admin_record_id):
490490
enterprise_customer.uuid
491491
)
492492

493+
# Hard-delete the EnterpriseCustomerAdmin record
494+
admin_record.delete()
495+
logger.info(
496+
"Hard deleted EnterpriseCustomerAdmin for EnterpriseCustomerUser id=%s in enterprise %s",
497+
enterprise_customer_user.id,
498+
enterprise_customer.uuid
499+
)
500+
493501
# Check if user has other roles for this enterprise
494502
# No locking needed - we're only checking existence, and ECU is already locked
495503
has_other_roles_in_enterprise = models.SystemWideEnterpriseUserRoleAssignment.objects.filter(

enterprise/filters/accounts.py

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ def run_filter(self, readonly_fields, user): # pylint: disable=arguments-differ
3232
"""
3333
Add enterprise SSO-managed fields to the read-only fields set.
3434
35+
The original code migrated from openedx-platform can be distilled into 3 logical branches:
36+
37+
1. If NO identify provider (IdP) has sync enabled → no readonly fields added.
38+
2. If one or more IdPs have sync enabled, AND user has social auth → append ALL readonly fields.
39+
3. If one or more IdPs have sync enabled, AND user has NO social auth → append readonly fields MINUS 'name'.
40+
41+
Each return statement below is marked with the corresponding branch number.
42+
3543
Arguments:
3644
readonly_fields (set): current set of read-only account field names.
3745
user (User): the Django User whose account settings are being updated.
@@ -46,35 +54,52 @@ def run_filter(self, readonly_fields, user): # pylint: disable=arguments-differ
4654
.first()
4755
)
4856
if not enterprise_customer_user:
57+
# Logical branch #1 (early exit)
4958
return {"readonly_fields": readonly_fields, "user": user}
5059

5160
enterprise_customer = enterprise_customer_user.enterprise_customer
5261

53-
idp_qs = EnterpriseCustomerIdentityProvider.objects.filter(
54-
enterprise_customer=enterprise_customer
62+
idp_records = list(
63+
EnterpriseCustomerIdentityProvider.objects
64+
.filter(enterprise_customer=enterprise_customer)
5565
)
5666

57-
# look for default provider in case of multiple, or default to first
58-
idp_record = idp_qs.filter(default_provider=True).first() or idp_qs.first()
59-
if not idp_record:
60-
return {"readonly_fields": readonly_fields, "user": user}
67+
# Track whether any IdP for the customer is configured to sync learner profile data. If none are, then we can
68+
# safely allow all fields to be editable since they won't get overwritten by the sync process
69+
sync_learner_profile_data = False
6170

62-
identity_provider = third_party_auth.provider.Registry.get(
63-
provider_id=idp_record.provider_id
64-
)
71+
# Accumulate a list of all identity providers for the customer. If the learner does NOT have any social auth
72+
# account configured with these backends, then we can safely allow them to edit the 'name' field (full name)
73+
provider_backend_names = []
74+
75+
for idp in idp_records:
76+
identity_provider = third_party_auth.provider.Registry.get(
77+
provider_id=idp.provider_id
78+
)
79+
if identity_provider and getattr(identity_provider, 'sync_learner_profile_data', False):
80+
sync_learner_profile_data = True
81+
82+
backend_name = getattr(identity_provider, 'backend_name', None)
83+
if backend_name:
84+
provider_backend_names.append(backend_name)
6585

66-
if not identity_provider or not getattr(identity_provider, 'sync_learner_profile_data', False):
86+
# If none of the IdPs for the customer are configured to sync, allow the fields to be editable
87+
if not sync_learner_profile_data:
88+
# Logical branch #1
6789
return {"readonly_fields": readonly_fields, "user": user}
6890

91+
# Determine if the learner has social auth configured.
92+
has_social_auth = False
93+
if provider_backend_names:
94+
has_social_auth = UserSocialAuth.objects.filter(
95+
provider__in=provider_backend_names, user=user
96+
).exists()
97+
6998
enterprise_readonly = set(getattr(settings, 'ENTERPRISE_READONLY_ACCOUNT_FIELDS', []))
7099

71-
if 'name' in enterprise_readonly:
72-
backend_name = getattr(identity_provider, 'backend_name', None)
73-
has_social_auth = (
74-
backend_name
75-
and UserSocialAuth.objects.filter(provider=backend_name, user=user).exists()
76-
)
77-
if not has_social_auth:
78-
enterprise_readonly = enterprise_readonly - {'name'}
100+
# If the learner does NOT have social auth configured, then at least allow them to edit their name.
101+
if not has_social_auth:
102+
enterprise_readonly = enterprise_readonly - {'name'}
79103

104+
# Logical branch #2 and #3
80105
return {"readonly_fields": readonly_fields | enterprise_readonly, "user": user}

requirements/base.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ requests
4242
rules
4343
slumber
4444
snowflake-connector-python
45+
social-auth-app-django
4546
stevedore
4647
testfixtures
4748
unicodecsv

requirements/dev.in

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ isort # to standardize order of imports
1111
pip-tools # Requirements file management
1212
pycodestyle # PEP 8 compliance validation
1313
pydocstyle # PEP 257 compliance validation
14-
social-auth-app-django # adding explicit requirement to prevent defensive imports
1514
testfixtures # Mock objects for unit tests and doc tests
1615
tox # virtualenv management for tests
1716
twine==1.11.0 # Utility for PyPI package uploads

requirements/dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# This file is autogenerated by pip-compile with Python 3.12
33
# by the following command:
44
#
5-
# pip-compile --output-file=requirements/dev.txt requirements/dev.in
5+
# make upgrade
66
#
77
accessible-pygments==0.0.5
88
# via

requirements/doc.in

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,5 @@ edx-braze-client
99
pytest
1010
factory-boy
1111
readme_renderer # Validates README.rst for usage on PyPI
12-
social-auth-app-django # adding explicit requirement to prevent defensive imports
1312
Sphinx # Documentation builder
1413
sphinx-book-theme # Common theme for all Open edX projects

requirements/doc.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# This file is autogenerated by pip-compile with Python 3.12
33
# by the following command:
44
#
5-
# pip-compile --output-file=requirements/doc.txt requirements/doc.in
5+
# make upgrade
66
#
77
accessible-pygments==0.0.5
88
# via pydata-sphinx-theme

0 commit comments

Comments
 (0)