Skip to content

Commit d8a00b0

Browse files
committed
Fix user member sync on django
1 parent 2bf4c48 commit d8a00b0

File tree

2 files changed

+109
-14
lines changed
  • infrastructure/common-images/cloudharness-django/libraries/cloudharness-django/cloudharness_django

2 files changed

+109
-14
lines changed

infrastructure/common-images/cloudharness-django/libraries/cloudharness-django/cloudharness_django/middleware.py

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,82 @@
1515

1616

1717
def _get_user(kc_user_id: str) -> User:
18+
"""
19+
Get or create a Django user for the given Keycloak user ID.
20+
21+
CRITICAL SAFETY GUARANTEE: This function will NEVER return a User without a valid Member.
22+
If we cannot ensure a Member exists, we return None (which triggers anonymous user behavior).
23+
24+
Returns:
25+
User: A Django User with a guaranteed Member relationship, or None for anonymous
26+
"""
1827
user = None
1928
if kc_user_id is None:
2029
return None
21-
# found bearer token get the Django user
30+
2231
try:
32+
# Try to get existing user by member relationship
2333
try:
2434
user = User.objects.get(member__kc_id=kc_user_id)
35+
36+
# SAFETY CHECK: Verify member relationship is intact
37+
try:
38+
_ = user.member # Access to verify it exists
39+
except Member.DoesNotExist:
40+
# Member was deleted between the query and now - return None for safety
41+
log.error("User %s found but Member missing. Returning anonymous.", user.id)
42+
return None
43+
2544
except User.DoesNotExist:
45+
# User doesn't exist - create it via sync_kc_user
2646
user_svc = get_user_service()
2747
kc_user = user_svc.auth_client.get_current_user()
2848
try:
49+
# sync_kc_user is atomic and guarantees Member creation
2950
user = user_svc.sync_kc_user(kc_user)
3051
user_svc.sync_kc_user_groups(kc_user)
52+
53+
# SAFETY CHECK: Final verification that Member exists
54+
try:
55+
_ = user.member
56+
except Member.DoesNotExist:
57+
# This should NEVER happen due to sync_kc_user safety, but be defensive
58+
log.error("sync_kc_user returned user %s without Member! Returning anonymous.", user.id)
59+
return None
60+
3161
except UniqueViolation as e:
32-
# this can happen as a race condition while creating the Member object
62+
# Race condition while creating the Member object
3363
log.warning("UniqueViolation error for kc_id %s. Probably a race condition. %s", kc_user_id, str(e))
34-
return User.objects.get(member__kc_id=kc_user_id) # If it still fails, we are missing something serious
35-
64+
# Try to get the user again
65+
try:
66+
user = User.objects.get(member__kc_id=kc_user_id)
67+
# Verify member exists
68+
_ = user.member
69+
except (User.DoesNotExist, Member.DoesNotExist):
70+
log.error("Failed to retrieve user after UniqueViolation. Returning anonymous.")
71+
return None
72+
3673
except User.MultipleObjectsReturned:
3774
# Race condition, multiple users created for the same kc_id
3875
log.warning("Multiple users found for kc_id %s, cleaning up...", kc_user_id)
3976
user = User.objects.filter(member__kc_id=kc_user_id).order_by('id').first()
4077
User.objects.filter(member__kc_id=kc_user_id).exclude(id=user.id).delete()
78+
79+
# SAFETY CHECK: Verify the kept user has a Member
80+
try:
81+
_ = user.member
82+
except:
83+
log.error("Cleaned up user %s has no Member. Returning anonymous.", user.id)
84+
return None
85+
4186
return user
4287

4388
except Exception as e:
4489
log.exception("User sync error, %s", kc_user.email)
4590
return None
91+
4692
return user
93+
4794
except KeycloakGetError:
4895
# KC user not found
4996
return None
@@ -68,9 +115,19 @@ def __call__(self, request):
68115
if not user or user.is_anonymous or getattr(user, "member", None) is None or user.member.kc_id != kc_user.id:
69116
user = _get_user(kc_user)
70117
if user:
71-
# auto login, set the user
72-
request.user = user
73-
request._cached_user = user
118+
# CRITICAL SAFETY CHECK: Never assign user without Member
119+
# This is the final defense to ensure request.user always has a Member
120+
try:
121+
_ = user.member # Verify member exists
122+
# Safe to assign - user has a valid Member
123+
request.user = user
124+
request._cached_user = user
125+
except:
126+
# This should NEVER happen due to _get_user safety checks,
127+
# but if it does, DO NOT assign the user - keep anonymous
128+
log.error("CRITICAL: _get_user returned user %s without Member! Keeping anonymous.", user.id)
129+
logout(request)
130+
# Don't assign user - request will remain anonymous
74131
# elif not request.path.startswith('/admin/'):
75132
# logout(request)
76133

@@ -85,5 +142,23 @@ def authenticate(self, request):
85142
return None
86143
user: User = getattr(request._request, 'user', None)
87144
if user and user.is_authenticated:
88-
return (user, None)
89-
return (_get_user(kc_user), None)
145+
# SAFETY CHECK: Verify authenticated user has Member
146+
try:
147+
_ = user.member
148+
return (user, None)
149+
except Member.DoesNotExist:
150+
log.error("Authenticated user %s has no Member! Falling back to _get_user.", user.id)
151+
# Fall through to _get_user which will handle this safely
152+
153+
# Get or create user with guaranteed Member
154+
user = _get_user(kc_user)
155+
if user:
156+
# FINAL SAFETY CHECK: Ensure Member exists before returning
157+
try:
158+
_ = user.member
159+
return (user, None)
160+
except Member.DoesNotExist:
161+
log.error("CRITICAL: _get_user returned user %s without Member! Returning None.", user.id)
162+
return None
163+
164+
return None

infrastructure/common-images/cloudharness-django/libraries/cloudharness-django/cloudharness_django/services/user.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.contrib.auth.models import User, Group
2+
from django.db import transaction
23

34
from cloudharness_django.models import Team, Member
45
from cloudharness_django.services.auth import AuthService, AuthorizationLevel
@@ -96,28 +97,47 @@ def sync_kc_groups(self, kc_groups=None):
9697
for kc_group in kc_groups:
9798
self.sync_kc_group(kc_group)
9899

100+
@transaction.atomic
99101
def sync_kc_user(self, kc_user: ch_models.User, is_superuser=False, delete=False):
100-
# sync the kc user with the django user using kc_id for reliable lookups
102+
"""
103+
Sync the kc user with the django user using kc_id for reliable lookups.
104+
ALWAYS creates both User and Member atomically - never returns a User without a Member.
105+
"""
101106
user = get_user_by_kc_id(kc_user.id)
102107

103108
if user is None:
104-
# User doesn't exist, create new one
109+
# User doesn't exist, create new one WITH Member atomically
105110
username = kc_user.username or kc_user.email
106111
if not username:
107112
raise ValueError(f"Keycloak user {kc_user.id} has no username or email")
108113

109-
user, _ = User.objects.get_or_create(username=username)
114+
# Create user and member atomically
115+
user, user_created = User.objects.get_or_create(username=username)
116+
117+
# CRITICAL: Ensure Member exists before proceeding
110118
try:
111119
member = Member.objects.get(user=user)
112-
member.kc_id = kc_user.id
113-
member.save()
120+
# Member exists but might have wrong kc_id
121+
if member.kc_id != kc_user.id:
122+
member.kc_id = kc_user.id
123+
member.save()
114124
except Member.DoesNotExist:
115125
# Create the member relationship
116126
Member.objects.create(kc_id=kc_user.id, user=user)
117127

128+
# Update user attributes
118129
user = self._map_kc_user(user, kc_user, is_superuser, delete)
119130
self.sync_kc_user_groups(kc_user)
120131
user.save()
132+
133+
# FINAL SAFETY CHECK: Verify Member exists before returning
134+
# This ensures we never return a user without a Member
135+
try:
136+
_ = user.member # Access member to trigger DoesNotExist if missing
137+
except:
138+
# This should never happen, but if it does, create Member immediately
139+
Member.objects.create(kc_id=kc_user.id, user=user)
140+
121141
return user
122142

123143
def sync_kc_user_groups(self, kc_user: ch_models.User):

0 commit comments

Comments
 (0)