Conversation
- POST: return updated adopted record instead of stale pre-update object - PUT: change from destructive full-sync to additive-only member sync to prevent cross-group deprovisioning in multi-group scenarios - DELETE: remove team member cascade that would wipe all members from mapped teams regardless of membership provenance
- PATCH member remove is now a no-op: without membership provenance tracking, removing would silently revoke access granted by other groups, OIDC, or manual assignment (same safeguard as DELETE/PUT) - Remove removeMappedMemberships helper (no longer used) - Process displayName rename before member ops and re-resolve mappings, preventing stale mapping context when rename + member ops are batched
…rships - PUT handler now re-resolves groupMappings after displayName rename, matching the fix already applied to PATCH - applyMappedMemberships only upgrades roles, never downgrades — prevents a lower-role group sync from overwriting a higher role granted by another group, OIDC, or manual assignment
Introduces provenance tracking for SCIM group memberships. ScimGroupMember tracks which IdP groups each user belongs to. TeamMember.source distinguishes manual from group_mapping assignments.
Single reconcileUserTeamMemberships() function replaces all direct TeamMember manipulation. Computes desired state from group names + mappings, diffs against current source=group_mapping members, and applies adds/updates/removes. Manual assignments are never touched.
…ation POST now creates ScimGroupMember records for each member and calls reconcileUserTeamMemberships instead of directly creating TeamMembers. GET returns actual ScimGroupMember data in the members array.
…ciliation PATCH add/remove members now creates/deletes ScimGroupMembers and reconciles. PUT does full member sync (adds missing, removes absent). DELETE cascades ScimGroupMembers and reconciles affected users. displayName rename reconciles all group members.
In SCIM mode, reconciles all users with ScimGroupMember records when admin updates group mappings. Changes take effect immediately. In OIDC-only mode, changes take effect on next login (no bulk data).
OIDC-only mode: uses token groups directly, removes stale memberships. SCIM+OIDC mode: uses union of ScimGroupMember + token groups. OIDC login never writes to ScimGroupMember (avoids Azure AD token limit). Default team fallback preserved for users with no group matches.
Document two-mode behavior (OIDC-only vs SCIM+OIDC), reconciliation semantics, manual assignment immutability, and corrected SCIM Group lifecycle (remove, full sync, delete cascade).
Group create/adopt and member processing now share one transaction. Previously, the group would commit independently, so if member processing failed the IdP would get a 4xx despite the group being committed — SCIM clients treat 4xx as permanent and won't retry.
Greptile SummaryThis PR replaces the additive-only, event-driven SCIM group membership system with a full state-reconciliation model. It introduces The overall design is solid and the migration is safe (existing rows default to
Confidence Score: 3/5
Sequence DiagramsequenceDiagram
participant IdP as Identity Provider
participant SCIM as SCIM Endpoint
participant SGM as ScimGroupMember
participant Reconcile as reconcileUserTeamMemberships
participant TM as TeamMember
IdP->>SCIM: PATCH /Groups/:id (add/remove/rename)
SCIM->>SGM: upsert / deleteMany ScimGroupMember
SCIM->>SGM: getScimGroupNamesForUser(userId)
SGM-->>SCIM: [groupName1, groupName2, ...]
SCIM->>Reconcile: reconcileUserTeamMemberships(tx, userId, groupNames)
Reconcile->>Reconcile: loadGroupMappings()
Reconcile->>TM: findMany source="group_mapping"
Reconcile->>TM: create / update / delete (diff desired vs current)
Note over Reconcile,TM: Never touches source="manual" records
IdP->>SCIM: OIDC Login (token with groups claim)
SCIM->>SGM: findMany ScimGroupMember (SCIM+OIDC mode)
SCIM->>Reconcile: reconcileUserTeamMemberships(tx, userId, union(scim+token))
Reconcile->>TM: diff and apply changes
|
PATCH remove: handle both RFC 7644 forms — filter notation (members[value eq "userId"]) used by Okta/Azure and array-value form. Without this, single-member removals via filter notation were a no-op. Default team: check if user has any memberships after reconciliation, not whether they have groups. A user with unmatched groups should still get the default team.
|
@greptile re-review.
|
…pping # Conflicts: # docs/public/operations/authentication.md # prisma/schema.prisma # src/app/api/scim/v2/Groups/[id]/route.ts # src/app/api/scim/v2/Groups/route.ts # src/server/services/group-mappings.ts
…ation The withAudit middleware fires after the mutation completes, but by then the user record is already deleted — causing AuditLog_userId_fkey FK violations. Instead, write the audit log manually before the deletion transaction with userId: null, capturing the deleted user's identity in metadata fields.
Wrap scimGroup.delete and user reconciliation in a single transaction so a crash between them can't leave stale group_mapping TeamMembers.
|
@greptile re-review. Finding 1: DELETE endpoint not atomic — Valid, fixed. Wrapped scimGroup.delete and user Finding 2: loadGroupMappings() reads outside transaction — Not a real bug, skipped. Greptile |
…llback Add /i flag to PATCH remove filter regex per RFC 7644 case-insensitive operator requirement. Use upsert for default team assignment to handle concurrent OIDC logins gracefully.
Summary
Replaces the event-driven, additive-only SCIM group membership system with a state-reconciliation model that correctly handles adds, removes, multi-group overlap, group deletion, and role changes.
manual(admin-assigned) fromgroup_mapping(automation-managed)reconcileUserTeamMemberships()computes desired state from group names + mapping config, diffs against current, applies adds/updates/removessource: "manual"TeamMembersWhat was broken before
Design doc
docs/plans/2026-03-08-scim-reconciliation-design.mdTest plan
20260308050000_add_scim_group_member_and_source)