Skip to content

Commit 5049183

Browse files
authored
fix: preserve SCIM userName for Okta integration (#42253)
1 parent d98379a commit 5049183

File tree

8 files changed

+336
-24
lines changed

8 files changed

+336
-24
lines changed

ee/api/scim/test/test_users_api.py

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from ee.api.scim.auth import generate_scim_token
88
from ee.api.test.base import APILicensedTest
99
from ee.models.rbac.role import RoleMembership
10+
from ee.models.scim_provisioned_user import SCIMProvisionedUser
1011

1112

1213
class TestSCIMUsersAPI(APILicensedTest):
@@ -56,13 +57,27 @@ def test_users_list_filter_exact_match(self):
5657
OrganizationMembership.objects.create(
5758
user=user_a, organization=self.organization, level=OrganizationMembership.Level.MEMBER
5859
)
60+
SCIMProvisionedUser.objects.create(
61+
user=user_a,
62+
organization_domain=self.domain,
63+
username="[email protected]",
64+
identity_provider=SCIMProvisionedUser.IdentityProvider.OTHER,
65+
active=True,
66+
)
5967

6068
user_b = User.objects.create_user(
6169
email="[email protected]", password=None, first_name="Alex", last_name="Other", is_email_verified=True
6270
)
6371
OrganizationMembership.objects.create(
6472
user=user_b, organization=self.organization, level=OrganizationMembership.Level.MEMBER
6573
)
74+
SCIMProvisionedUser.objects.create(
75+
user=user_b,
76+
organization_domain=self.domain,
77+
username="[email protected]",
78+
identity_provider=SCIMProvisionedUser.IdentityProvider.OTHER,
79+
active=True,
80+
)
6681

6782
# Exact match should return only [email protected]
6883
response = self.client.get(
@@ -129,9 +144,9 @@ def test_users_list_filter_unrecognized_returns_empty_list(self):
129144
def test_create_user(self):
130145
user_data = {
131146
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
132-
"userName": "newuser@example.com",
147+
"userName": "Newuser@example.com",
133148
"name": {"givenName": "New", "familyName": "User"},
134-
"emails": [{"value": "newuser@example.com", "primary": True}],
149+
"emails": [{"value": "Newuser@example.com", "primary": True}],
135150
"active": True,
136151
}
137152

@@ -141,11 +156,11 @@ def test_create_user(self):
141156

142157
assert response.status_code == status.HTTP_201_CREATED
143158
data = response.json()
144-
assert data["userName"] == "newuser@example.com"
159+
assert data["userName"] == "Newuser@example.com"
145160
assert data["name"]["givenName"] == "New"
146161
assert data["name"]["familyName"] == "User"
147162

148-
# Verify user was created
163+
# Verify user was created with lowercase email
149164
user = User.objects.get(email="[email protected]")
150165
assert user.first_name == "New"
151166
assert user.last_name == "User"
@@ -155,6 +170,12 @@ def test_create_user(self):
155170
membership = OrganizationMembership.objects.get(user=user, organization=self.organization)
156171
assert membership.level == OrganizationMembership.Level.MEMBER
157172

173+
# Verify SCIM provisioned user record was created
174+
scim_user = SCIMProvisionedUser.objects.get(user=user, organization_domain=self.domain)
175+
assert scim_user.username == "[email protected]"
176+
assert scim_user.active is True
177+
assert scim_user.identity_provider == SCIMProvisionedUser.IdentityProvider.OTHER
178+
158179
def test_existing_user_is_added_to_org(self):
159180
# Create user in different org
160181
other_org = Organization.objects.create(name="Other Org")
@@ -184,9 +205,11 @@ def test_existing_user_is_added_to_org(self):
184205
assert OrganizationMembership.objects.filter(user=existing_user, organization=self.organization).exists()
185206
assert OrganizationMembership.objects.filter(user=existing_user, organization=other_org).exists()
186207

187-
def test_repeated_post_does_not_create_duplicate_user(self):
188-
# In case the IdP failed to match user by id, it can send POST request to create a new user.
189-
# The user should be merged with existing one by email, not create a duplicate.
208+
# Verify SCIM provisioned user record was created for this domain
209+
scim_user = SCIMProvisionedUser.objects.get(user=existing_user, organization_domain=self.domain)
210+
assert scim_user.active is True
211+
212+
def test_repeated_post_returns_409_for_already_provisioned_user(self):
190213
user_data_first = {
191214
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
192215
"userName": "[email protected]",
@@ -202,7 +225,7 @@ def test_repeated_post_does_not_create_duplicate_user(self):
202225
assert response.status_code == status.HTTP_201_CREATED
203226
first_user = User.objects.get(email="[email protected]")
204227

205-
# IdP sends POST request again with same email
228+
# IdP sends POST request again with same email - should fail with 409
206229
user_data_second = {
207230
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
208231
"userName": "[email protected]",
@@ -215,14 +238,14 @@ def test_repeated_post_does_not_create_duplicate_user(self):
215238
f"/scim/v2/{self.domain.id}/Users", data=user_data_second, content_type="application/scim+json"
216239
)
217240

218-
assert response.status_code == status.HTTP_201_CREATED
241+
assert response.status_code == status.HTTP_409_CONFLICT
219242

220243
# Should NOT create duplicate user
221244
assert User.objects.filter(email="[email protected]").count() == 1
222245

223-
# User should be updated with new data from second POST
246+
# User should NOT be updated (still has first POST data)
224247
first_user.refresh_from_db()
225-
assert first_user.first_name == "Second"
248+
assert first_user.first_name == "First"
226249
assert first_user.last_name == "Time"
227250

228251
# User should have only one membership
@@ -261,6 +284,14 @@ def test_deactivate_user(self):
261284
OrganizationMembership.objects.create(
262285
user=user, organization=self.organization, level=OrganizationMembership.Level.MEMBER
263286
)
287+
# Create SCIM provisioned user record
288+
SCIMProvisionedUser.objects.create(
289+
user=user,
290+
organization_domain=self.domain,
291+
username="[email protected]",
292+
identity_provider=SCIMProvisionedUser.IdentityProvider.OTHER,
293+
active=True,
294+
)
264295

265296
patch_data = {
266297
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
@@ -280,13 +311,25 @@ def test_deactivate_user(self):
280311
user.refresh_from_db()
281312
assert user.is_active is True # User is still active globally
282313

314+
# Verify SCIM provisioned user record still exists but is marked inactive
315+
scim_user = SCIMProvisionedUser.objects.get(user=user, organization_domain=self.domain)
316+
assert scim_user.active is False
317+
283318
def test_delete_user(self):
284319
user = User.objects.create_user(
285320
email="[email protected]", password=None, first_name="Delete", is_email_verified=True
286321
)
287322
OrganizationMembership.objects.create(
288323
user=user, organization=self.organization, level=OrganizationMembership.Level.MEMBER
289324
)
325+
# Create SCIM provisioned user record
326+
SCIMProvisionedUser.objects.create(
327+
user=user,
328+
organization_domain=self.domain,
329+
username="[email protected]",
330+
identity_provider=SCIMProvisionedUser.IdentityProvider.OTHER,
331+
active=True,
332+
)
290333

291334
response = self.client.delete(f"/scim/v2/{self.domain.id}/Users/{user.id}")
292335

@@ -295,13 +338,24 @@ def test_delete_user(self):
295338
# Verify membership was removed
296339
assert not OrganizationMembership.objects.filter(user=user, organization=self.organization).exists()
297340

341+
# Verify SCIM provisioned user record was deleted
342+
assert not SCIMProvisionedUser.objects.filter(user=user, organization_domain=self.domain).exists()
343+
298344
def test_put_user(self):
299345
user = User.objects.create_user(
300346
email="[email protected]", password=None, first_name="Old", last_name="Name", is_email_verified=True
301347
)
302348
OrganizationMembership.objects.create(
303349
user=user, organization=self.organization, level=OrganizationMembership.Level.MEMBER
304350
)
351+
# Create SCIM provisioned user record
352+
SCIMProvisionedUser.objects.create(
353+
user=user,
354+
organization_domain=self.domain,
355+
username="[email protected]",
356+
identity_provider=SCIMProvisionedUser.IdentityProvider.OTHER,
357+
active=True,
358+
)
305359

306360
put_data = {
307361
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
@@ -321,6 +375,11 @@ def test_put_user(self):
321375
assert user.last_name == "User"
322376
assert user.email == "[email protected]"
323377

378+
# Verify SCIM provisioned user was updated
379+
scim_user = SCIMProvisionedUser.objects.get(user=user, organization_domain=self.domain)
380+
assert scim_user.username == "[email protected]"
381+
assert scim_user.active is True
382+
324383
def test_put_user_not_found(self):
325384
put_data = {
326385
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],

0 commit comments

Comments
 (0)