Skip to content

Conversation

@a-lider
Copy link
Contributor

@a-lider a-lider commented Nov 27, 2025

Problem

Okta integration tests were failing because SCIM expects userName to preserve case, but we were returning the user's email which gets lowercased.

Changes

  • Added SCIMProvisionedUser model to store userName, is_active, and identity_provider
    • This also enables future restrictions on editing active SCIM-managed users.

Note: the active field in SCIMProvisionedModel is needed so that in the future we can display users in Members who were deactivated (their org membership was removed) but the user account itself was not deleted.

  • Updated all SCIM responses to return userName from scim_provisioned_user instead of the user.email, preserving the original case
  • Return 409 on subsequent SCIM POST user request

How did you test this code?

Tests + Runscope: https://www.runscope.com/radar/lx4oulj4u3qz/e9d81345-46fd-414a-b833-34241e91d956/history/ed52c993-0a64-4299-b5d4-d2cfbdd736d7

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 files reviewed, 7 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 28 to 29
SCIM_USER_ATTR_MAP = {
("userName", None, None): "email",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: userName now maps to SCIMProvisionedUser.username instead of User.email, but this filter mapping still points to email. This breaks case-sensitive filtering. When Okta filters by userName eq "[email protected]", it searches User.email (lowercased) instead of SCIMProvisionedUser.username (preserves case).

Prompt To Fix With AI
This is a comment left during a code review.
Path: ee/api/scim/views.py
Line: 28:29

Comment:
**logic:** `userName` now maps to `SCIMProvisionedUser.username` instead of `User.email`, but this filter mapping still points to `email`. This breaks case-sensitive filtering. When Okta filters by `userName eq "[email protected]"`, it searches `User.email` (lowercased) instead of `SCIMProvisionedUser.username` (preserves case).

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Legit comment, will fix it

Comment on lines 148 to 156
# Check if user is already SCIM-provisioned for this org domain
email = PostHogSCIMUser._extract_email_from_value(request.data.get("emails", []))
if email:
user = User.objects.filter(email__iexact=email).first()
if user and SCIMProvisionedUser.objects.filter(user=user, organization_domain=organization_domain).exists():
return Response(
{"schemas": [constants.SchemaURI.ERROR], "status": 409, "detail": "User already exists"},
status=status.HTTP_409_CONFLICT,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Race condition: the 409 check happens outside the transaction in from_dict(). Two concurrent POST requests with the same email could both pass this check, then one would succeed and the other would hit a database constraint error instead of returning 409.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ee/api/scim/views.py
Line: 148:156

Comment:
**logic:** Race condition: the 409 check happens outside the transaction in `from_dict()`. Two concurrent POST requests with the same email could both pass this check, then one would succeed and the other would hit a database constraint error instead of returning 409.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the check if user is already provisioned into transaction

Comment on lines 8 to 66
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("posthog", "0923_add_quick_filters"),
("ee", "0031_agentartifact"),
]

operations = [
migrations.CreateModel(
name="SCIMProvisionedUser",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("updated_at", models.DateTimeField(auto_now=True, null=True)),
(
"identity_provider",
models.CharField(
choices=[
("okta", "Okta"),
("entra_id", "Microsoft Entra ID"),
("google", "Google Workspace"),
("onelogin", "OneLogin"),
("other", "Other"),
],
max_length=50,
),
),
("username", models.CharField(max_length=255)),
("active", models.BooleanField(default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"organization_domain",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="scim_provisioned_users",
to="posthog.organizationdomain",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="scim_provisions",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"indexes": [
models.Index(fields=["organization_domain", "username"], name="ee_scimprov_organiz_6d83ff_idx")
],
},
),
migrations.AddConstraint(
model_name="scimprovisioneduser",
constraint=models.UniqueConstraint(
fields=("user", "organization_domain"), name="unique_user_organization_domain"
),
),
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Missing data migration for existing SCIM-provisioned users. Users created before this PR won't have SCIMProvisionedUser records, so their userName will fallback to lowercased email and they can't be filtered by case-sensitive userName.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ee/migrations/0032_scimprovisioneduser_and_more.py
Line: 8:66

Comment:
**logic:** Missing data migration for existing SCIM-provisioned users. Users created before this PR won't have `SCIMProvisionedUser` records, so their `userName` will fallback to lowercased email and they can't be filtered by case-sensitive userName.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have any customers on SCIM yet, so this is not critical

@github-actions
Copy link
Contributor

github-actions bot commented Nov 27, 2025

🔍 Migration Risk Analysis

We've analyzed your migrations for potential risks.

Summary: 1 Safe | 0 Needs Review | 0 Blocked

✅ Safe

No contention risk, backwards compatible

ee.0032_scimprovisioneduser_and_more
  └─ #1 ✅ CreateModel
     Creating new table is safe
     model: SCIMProvisionedUser
  │
  └──> ℹ️  INFO:
       ℹ️  Skipped operations on newly created tables (empty tables
       don't cause lock contention).

Last updated: 2025-11-28 19:31 UTC (ebc83a1)

@a-lider a-lider merged commit 5049183 into master Nov 28, 2025
176 checks passed
@a-lider a-lider deleted the alex/fix/scim/provisioned-users-data branch November 28, 2025 19:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants