Commit e880c08
authored
feat: SCIM reconciliation redesign (#64)
* feat: add ScimGroup model for SCIM protocol compliance
* feat: add shared group mapping helpers, remove resolveScimRole
* feat: rewrite SCIM Groups GET/POST to use ScimGroup model instead of Team
* feat: rewrite SCIM Groups PATCH/PUT/DELETE to use mapping table
* feat: rename OIDC-specific UI labels to generic IdP Group Mappings
* docs: update authentication docs for unified group mapping
* fix: address Greptile findings on SCIM group mutation handlers
- 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
* fix: PATCH remove no-op and stale mapping context on rename
- 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
* fix: PUT stale mapping context and role downgrade in applyMappedMemberships
- 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
* feat: add ScimGroupMember model and TeamMember.source field
Introduces provenance tracking for SCIM group memberships.
ScimGroupMember tracks which IdP groups each user belongs to.
TeamMember.source distinguishes manual from group_mapping assignments.
* feat: rewrite group-mappings.ts as reconciliation module
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.
* feat: rewrite SCIM Groups POST/GET to use ScimGroupMember + reconciliation
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.
* feat: rewrite SCIM Groups [id] endpoints with ScimGroupMember + reconciliation
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.
* feat: trigger bulk reconciliation when group mappings are saved
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).
* feat: update OIDC login to use reconciliation for group sync
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.
* docs: update auth and SCIM docs for reconciliation model
Document two-mode behavior (OIDC-only vs SCIM+OIDC), reconciliation
semantics, manual assignment immutability, and corrected SCIM Group
lifecycle (remove, full sync, delete cascade).
* fix: wrap SCIM Groups POST in single transaction
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.
* fix: handle SCIM filter-notation removes and fix default team fallback
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.
* fix: write audit log before user deletion to avoid FK constraint violation
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.
* fix: make SCIM group DELETE atomic with reconciliation
Wrap scimGroup.delete and user reconciliation in a single transaction
so a crash between them can't leave stale group_mapping TeamMembers.
* fix: case-insensitive SCIM filter matching and upsert default team fallback
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.1 parent 57d68a9 commit e880c08
10 files changed
Lines changed: 498 additions & 200 deletions
File tree
- docs/public/operations
- prisma
- migrations/20260308050000_add_scim_group_member_and_source
- src
- app/api/scim/v2/Groups
- [id]
- server
- routers
- services
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
77 | 77 | | |
78 | 78 | | |
79 | 79 | | |
80 | | - | |
| 80 | + | |
81 | 81 | | |
82 | | - | |
83 | | - | |
| 82 | + | |
84 | 83 | | |
85 | | - | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
86 | 100 | | |
87 | 101 | | |
88 | 102 | | |
| |||
110 | 124 | | |
111 | 125 | | |
112 | 126 | | |
113 | | - | |
114 | | - | |
115 | 127 | | |
116 | 128 | | |
117 | 129 | | |
118 | | - | |
| 130 | + | |
119 | 131 | | |
120 | 132 | | |
121 | 133 | | |
122 | 134 | | |
123 | | - | |
| 135 | + | |
124 | 136 | | |
125 | 137 | | |
126 | 138 | | |
| |||
240 | 252 | | |
241 | 253 | | |
242 | 254 | | |
243 | | - | |
244 | | - | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
| 16 | + | |
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| |||
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
56 | | - | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
57 | 72 | | |
58 | 73 | | |
59 | 74 | | |
60 | | - | |
61 | | - | |
62 | | - | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
63 | 79 | | |
64 | 80 | | |
65 | 81 | | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
66 | 86 | | |
67 | 87 | | |
68 | 88 | | |
| |||
118 | 138 | | |
119 | 139 | | |
120 | 140 | | |
121 | | - | |
| 141 | + | |
| 142 | + | |
122 | 143 | | |
123 | | - | |
124 | | - | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
125 | 147 | | |
126 | 148 | | |
127 | 149 | | |
| |||
157 | 179 | | |
158 | 180 | | |
159 | 181 | | |
160 | | - | |
| 182 | + | |
161 | 183 | | |
162 | 184 | | |
163 | 185 | | |
| |||
172 | 194 | | |
173 | 195 | | |
174 | 196 | | |
175 | | - | |
| 197 | + | |
Lines changed: 21 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | | - | |
18 | | - | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
19 | 20 | | |
20 | 21 | | |
21 | 22 | | |
| |||
54 | 55 | | |
55 | 56 | | |
56 | 57 | | |
57 | | - | |
58 | | - | |
59 | | - | |
60 | | - | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
61 | 74 | | |
62 | 75 | | |
63 | 76 | | |
64 | 77 | | |
65 | 78 | | |
66 | 79 | | |
67 | 80 | | |
| 81 | + | |
68 | 82 | | |
69 | 83 | | |
70 | 84 | | |
| |||
0 commit comments