fix(auth): move SMB auto-join from current_active_user to on_after_register#21
Merged
Merged
Conversation
…gister
`auto_join_smb_search_space` was called from `current_active_user`,
FastAPI's per-request auth dependency. That dependency runs on every
authenticated API request — both proxy-auth and JWT paths.
Combined with `DELETE /searchspaces/{id}/members/{membership_id}`
(`rbac_routes.py:705`) which hard-deletes the membership row with no
tombstone (the SearchSpaceMembership model has no `removed_at` /
`is_blocked` / `deleted_at` column), this made admin removal a no-op:
1. Owner Alice calls `DELETE /api/v1/searchspaces/42/members/77` to
evict Editor Bob → row deleted, API returns 200.
2. Bob's next request (`GET /api/v1/searchspaces/42/documents`) hits
`current_active_user` → triggers `auto_join_smb_search_space(bob.id)`
→ no membership row found → INSERT (Editor again).
3. Bob is back inside the workspace, can re-read every document, mint
invite links, etc. He cannot be removed at all while his SSO sign-in
still works at the IdP.
Editor permissions include documents:create/read/update,
chats:create/read/update, members:invite, connectors:create/update —
significant write surface to silently restore.
Fix: move the call to `on_after_register` so it fires exactly once per
user, at user creation. Both registration paths land here:
- Standard FastAPI Users register flow
- ProxyAuthMiddleware (already invokes `on_after_register` after
creating a user via header-trust — see proxy_auth.py:179-209)
Trade-off documented in the comment: users who registered BEFORE the
SMB workspace existed are not retroactively auto-joined. Operators
can backfill with a one-time SQL INSERT against
`search_space_memberships`. The previous design intentionally caught
that case via per-request enforcement; the un-revokability cost was
not worth the convenience.
Refs: awais786/sso-rules surfsense-security.md §"Finding 1"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
16 tasks
There was a problem hiding this comment.
Pull request overview
Moves the auto_join_smb_search_space call out of the per-request current_active_user dependency and into on_after_register, so it executes exactly once at user creation. Previously, every authenticated request from a removed user re-INSERTed their SMB membership row, because the DELETE .../members/{membership_id} endpoint hard-deletes without a tombstone — making admin removals effectively a no-op.
Changes:
- Add a single
auto_join_smb_search_space(user.id)call at the end ofUserManager.on_after_register, wrapped in try/except withlogger.exception. - Remove the two per-request
auto_join_smb_search_spaceinvocations (proxy_user and jwt_user branches) fromcurrent_active_user. - Update docstrings to document the new placement and rationale.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
CI's ruff-check moved `from app.services.smb_auto_join import auto_join_smb_search_space` from between `app.config` and `app.db` to between `app.prompts.system_defaults` and `app.utils.refresh_tokens` — correct alphabetical position within the `app.*` block. Auto-fix from `ruff check --fix surfsense_backend/app/users.py`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UsamaSadiq
approved these changes
May 18, 2026
aznszn
pushed a commit
that referenced
this pull request
May 19, 2026
…gister (#21) * fix(auth): move SMB auto-join from current_active_user to on_after_register `auto_join_smb_search_space` was called from `current_active_user`, FastAPI's per-request auth dependency. That dependency runs on every authenticated API request — both proxy-auth and JWT paths. Combined with `DELETE /searchspaces/{id}/members/{membership_id}` (`rbac_routes.py:705`) which hard-deletes the membership row with no tombstone (the SearchSpaceMembership model has no `removed_at` / `is_blocked` / `deleted_at` column), this made admin removal a no-op: 1. Owner Alice calls `DELETE /api/v1/searchspaces/42/members/77` to evict Editor Bob → row deleted, API returns 200. 2. Bob's next request (`GET /api/v1/searchspaces/42/documents`) hits `current_active_user` → triggers `auto_join_smb_search_space(bob.id)` → no membership row found → INSERT (Editor again). 3. Bob is back inside the workspace, can re-read every document, mint invite links, etc. He cannot be removed at all while his SSO sign-in still works at the IdP. Editor permissions include documents:create/read/update, chats:create/read/update, members:invite, connectors:create/update — significant write surface to silently restore. Fix: move the call to `on_after_register` so it fires exactly once per user, at user creation. Both registration paths land here: - Standard FastAPI Users register flow - ProxyAuthMiddleware (already invokes `on_after_register` after creating a user via header-trust — see proxy_auth.py:179-209) Trade-off documented in the comment: users who registered BEFORE the SMB workspace existed are not retroactively auto-joined. Operators can backfill with a one-time SQL INSERT against `search_space_memberships`. The previous design intentionally caught that case via per-request enforcement; the un-revokability cost was not worth the convenience. Refs: awais786/sso-rules surfsense-security.md §"Finding 1" Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: fix ruff import ordering CI's ruff-check moved `from app.services.smb_auto_join import auto_join_smb_search_space` from between `app.config` and `app.db` to between `app.prompts.system_defaults` and `app.utils.refresh_tokens` — correct alphabetical position within the `app.*` block. Auto-fix from `ruff check --fix surfsense_backend/app/users.py`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
aznszn
pushed a commit
that referenced
this pull request
May 19, 2026
…gister (#21) * fix(auth): move SMB auto-join from current_active_user to on_after_register `auto_join_smb_search_space` was called from `current_active_user`, FastAPI's per-request auth dependency. That dependency runs on every authenticated API request — both proxy-auth and JWT paths. Combined with `DELETE /searchspaces/{id}/members/{membership_id}` (`rbac_routes.py:705`) which hard-deletes the membership row with no tombstone (the SearchSpaceMembership model has no `removed_at` / `is_blocked` / `deleted_at` column), this made admin removal a no-op: 1. Owner Alice calls `DELETE /api/v1/searchspaces/42/members/77` to evict Editor Bob → row deleted, API returns 200. 2. Bob's next request (`GET /api/v1/searchspaces/42/documents`) hits `current_active_user` → triggers `auto_join_smb_search_space(bob.id)` → no membership row found → INSERT (Editor again). 3. Bob is back inside the workspace, can re-read every document, mint invite links, etc. He cannot be removed at all while his SSO sign-in still works at the IdP. Editor permissions include documents:create/read/update, chats:create/read/update, members:invite, connectors:create/update — significant write surface to silently restore. Fix: move the call to `on_after_register` so it fires exactly once per user, at user creation. Both registration paths land here: - Standard FastAPI Users register flow - ProxyAuthMiddleware (already invokes `on_after_register` after creating a user via header-trust — see proxy_auth.py:179-209) Trade-off documented in the comment: users who registered BEFORE the SMB workspace existed are not retroactively auto-joined. Operators can backfill with a one-time SQL INSERT against `search_space_memberships`. The previous design intentionally caught that case via per-request enforcement; the un-revokability cost was not worth the convenience. Refs: awais786/sso-rules surfsense-security.md §"Finding 1" Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: fix ruff import ordering CI's ruff-check moved `from app.services.smb_auto_join import auto_join_smb_search_space` from between `app.config` and `app.db` to between `app.prompts.system_defaults` and `app.utils.refresh_tokens` — correct alphabetical position within the `app.*` block. Auto-fix from `ruff check --fix surfsense_backend/app/users.py`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
auto_join_smb_search_spacewas being called fromcurrent_active_user, FastAPI's per-request auth dependency. Combined with the hard-deleteDELETE /searchspaces/{id}/members/{membership_id}endpoint (no tombstone column onSearchSpaceMembership), this made admin removal a no-op: the very next API request from a removed user re-INSERTed their membership row.This PR moves the call to `on_after_register` so it fires exactly once per user, at creation time. Removed users stay removed.
Reproduction (against current main)
Fix
`users.py`:
Both registration paths land in `on_after_register`:
So new users from either flow are still auto-joined exactly once. Existing users at fix time who don't have an SMB membership row aren't backfilled — operators can do a one-time SQL INSERT against `search_space_memberships` for those.
Why not a tombstone column
A tombstone column on `SearchSpaceMembership` (`removed_at` or `is_blocked`) was the alternative — preserves the original "auto-join existing users when SMB workspace appears" UX while making removals durable. Larger change (schema migration + DELETE refactor + auto-join skip-tombstoned-users logic). Saved for a follow-up if the one-shot trade-off proves limiting.
Test plan
Refs
External writeup: awais786/sso-rules `surfsense-security.md` §Finding 1 (PR pending)
🤖 Generated with Claude Code