Skip to content

Commit f59e775

Browse files
authored
fix: missing org membership creation for SCIM user update (#42603)
1 parent e0fc7f7 commit f59e775

File tree

2 files changed

+105
-9
lines changed

2 files changed

+105
-9
lines changed

ee/api/scim/test/test_users_api.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,48 @@ def test_put_user(self):
380380
assert scim_user.username == "[email protected]"
381381
assert scim_user.active is True
382382

383+
def test_put_reactivate_user(self):
384+
user = User.objects.create_user(
385+
386+
password=None,
387+
first_name="Put",
388+
last_name="Reactivate",
389+
is_email_verified=True,
390+
)
391+
# Create then remove membership to simulate deactivated user
392+
OrganizationMembership.objects.create(
393+
user=user, organization=self.organization, level=OrganizationMembership.Level.MEMBER
394+
)
395+
SCIMProvisionedUser.objects.create(
396+
user=user,
397+
organization_domain=self.domain,
398+
username="[email protected]",
399+
identity_provider=SCIMProvisionedUser.IdentityProvider.OTHER,
400+
active=True,
401+
)
402+
# Deactivate
403+
OrganizationMembership.objects.filter(user=user, organization=self.organization).delete()
404+
SCIMProvisionedUser.objects.filter(user=user, organization_domain=self.domain).update(active=False)
405+
assert not OrganizationMembership.objects.filter(user=user, organization=self.organization).exists()
406+
407+
put_data = {
408+
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
409+
"userName": "[email protected]",
410+
"name": {"givenName": "Put", "familyName": "Reactivate"},
411+
"emails": [{"value": "[email protected]", "primary": True}],
412+
"active": True,
413+
}
414+
415+
response = self.client.put(
416+
f"/scim/v2/{self.domain.id}/Users/{user.id}", data=put_data, content_type="application/scim+json"
417+
)
418+
419+
assert response.status_code == status.HTTP_200_OK
420+
assert response.json()["active"] is True
421+
assert OrganizationMembership.objects.filter(user=user, organization=self.organization).exists()
422+
scim_user = SCIMProvisionedUser.objects.get(user=user, organization_domain=self.domain)
423+
assert scim_user.active is True
424+
383425
def test_put_user_not_found(self):
384426
put_data = {
385427
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
@@ -644,6 +686,41 @@ def test_patch_add_active_user_with_simple_path(self):
644686
assert response.status_code == status.HTTP_200_OK
645687
assert OrganizationMembership.objects.filter(user=user, organization=self.organization).exists()
646688

689+
def test_patch_replace_reactivate_user(self):
690+
user = User.objects.create_user(
691+
email="[email protected]", password=None, first_name="Test", is_email_verified=True
692+
)
693+
# Create then remove membership to simulate deactivated user
694+
OrganizationMembership.objects.create(
695+
user=user, organization=self.organization, level=OrganizationMembership.Level.MEMBER
696+
)
697+
SCIMProvisionedUser.objects.create(
698+
user=user,
699+
organization_domain=self.domain,
700+
username="[email protected]",
701+
identity_provider=SCIMProvisionedUser.IdentityProvider.OTHER,
702+
active=True,
703+
)
704+
# Deactivate
705+
OrganizationMembership.objects.filter(user=user, organization=self.organization).delete()
706+
SCIMProvisionedUser.objects.filter(user=user, organization_domain=self.domain).update(active=False)
707+
assert not OrganizationMembership.objects.filter(user=user, organization=self.organization).exists()
708+
709+
patch_data = {
710+
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
711+
"Operations": [{"op": "replace", "path": "active", "value": True}],
712+
}
713+
714+
response = self.client.patch(
715+
f"/scim/v2/{self.domain.id}/Users/{user.id}", data=patch_data, content_type="application/scim+json"
716+
)
717+
718+
assert response.status_code == status.HTTP_200_OK
719+
assert response.json()["active"] is True
720+
assert OrganizationMembership.objects.filter(user=user, organization=self.organization).exists()
721+
scim_user = SCIMProvisionedUser.objects.get(user=user, organization_domain=self.domain)
722+
assert scim_user.active is True
723+
647724
def test_patch_add_user_given_name_with_dotted_path(self):
648725
user = User.objects.create_user(
649726
email="[email protected]", password=None, first_name="", last_name="Existing", is_email_verified=True

ee/api/scim/user.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ def user_name(self) -> str:
6464
).first()
6565
return scim_user.username if scim_user else self.obj.email
6666

67+
@property
68+
def identity_provider(self) -> str:
69+
scim_user = SCIMProvisionedUser.objects.filter(
70+
user=self.obj, organization_domain=self._organization_domain
71+
).first()
72+
return scim_user.identity_provider if scim_user else SCIMProvisionedUser.IdentityProvider.OTHER
73+
6774
@property
6875
def active(self) -> bool:
6976
# A user is "active" in SCIM context if they have membership in this org
@@ -226,12 +233,18 @@ def put(self, data: dict) -> None:
226233
defaults={
227234
"username": user_name,
228235
"active": is_active,
229-
"identity_provider": SCIMProvisionedUser.IdentityProvider.OTHER,
236+
"identity_provider": self.identity_provider,
230237
},
231238
)
232239

233-
# Deactivate user if active is false
234-
if not is_active:
240+
if is_active:
241+
# Adding org membership to reactivate the user
242+
OrganizationMembership.objects.get_or_create(
243+
user=self.obj,
244+
organization=self._organization_domain.organization,
245+
defaults={"level": OrganizationMembership.Level.MEMBER},
246+
)
247+
else:
235248
self.deactivate()
236249

237250
def deactivate(self) -> None:
@@ -277,13 +290,19 @@ def handle_replace(self, path: AttrPath, value: Union[str, list, dict], operatio
277290
self.deactivate()
278291
return
279292
else:
293+
OrganizationMembership.objects.get_or_create(
294+
user=self.obj,
295+
organization=self._organization_domain.organization,
296+
defaults={"level": OrganizationMembership.Level.MEMBER},
297+
)
298+
280299
SCIMProvisionedUser.objects.update_or_create(
281300
user=self.obj,
282301
organization_domain=self._organization_domain,
283302
defaults={
284303
"active": True,
285-
"username": self.obj.email,
286-
"identity_provider": SCIMProvisionedUser.IdentityProvider.OTHER,
304+
"username": self.user_name,
305+
"identity_provider": self.identity_provider,
287306
},
288307
)
289308

@@ -315,7 +334,7 @@ def handle_replace(self, path: AttrPath, value: Union[str, list, dict], operatio
315334
defaults={
316335
"username": value,
317336
"active": True,
318-
"identity_provider": SCIMProvisionedUser.IdentityProvider.OTHER,
337+
"identity_provider": self.identity_provider,
319338
},
320339
)
321340

@@ -342,8 +361,8 @@ def handle_add(self, path: AttrPath, value: Union[str, list, dict], operation: d
342361
organization_domain=self._organization_domain,
343362
defaults={
344363
"active": True,
345-
"username": self.obj.email,
346-
"identity_provider": SCIMProvisionedUser.IdentityProvider.OTHER,
364+
"username": self.user_name,
365+
"identity_provider": self.identity_provider,
347366
},
348367
)
349368

@@ -377,7 +396,7 @@ def handle_add(self, path: AttrPath, value: Union[str, list, dict], operation: d
377396
defaults={
378397
"username": value,
379398
"active": True,
380-
"identity_provider": SCIMProvisionedUser.IdentityProvider.OTHER,
399+
"identity_provider": self.identity_provider,
381400
},
382401
)
383402

0 commit comments

Comments
 (0)