From 654b9fd7363dfc9c5f6c5a95a7947eb54173a0c4 Mon Sep 17 00:00:00 2001 From: jawad-khan Date: Tue, 12 May 2026 14:38:27 +0500 Subject: [PATCH 1/4] feat: Add user to default workspace on signin --- .../app/services/smb_auto_join.py | 103 ++++++++++++++++++ .../scripts/provision-surfsense.py | 0 2 files changed, 103 insertions(+) create mode 100644 surfsense_backend/app/services/smb_auto_join.py create mode 100644 surfsense_backend/scripts/provision-surfsense.py diff --git a/surfsense_backend/app/services/smb_auto_join.py b/surfsense_backend/app/services/smb_auto_join.py new file mode 100644 index 0000000000..1d7941b4c7 --- /dev/null +++ b/surfsense_backend/app/services/smb_auto_join.py @@ -0,0 +1,103 @@ +"""Ensure SSO users are members of the shared SMB SearchSpace (SMB_NAME).""" + +from __future__ import annotations + +import logging +import uuid + +from sqlalchemy import select +from sqlalchemy.exc import IntegrityError + +from app.config import config +from app.db import ( + SearchSpace, + SearchSpaceMembership, + SearchSpaceRole, + async_session_maker, +) + +logger = logging.getLogger(__name__) + + +async def auto_join_smb_search_space(user_id: uuid.UUID) -> None: + """ + If ``SMB_NAME`` is set and a matching SearchSpace exists, insert membership + for ``user_id`` using the default invite role (Editor) when absent. + + Idempotent. Safe when auth uses Bearer JWT only (no ForwardAuth headers): + ``ProxyAuthMiddleware`` skips resolution for those requests, so this runs + from ``current_active_user`` as well. + """ + smb = (getattr(config, "SMB_NAME", None) or "").strip() + if not smb: + return + + async with async_session_maker() as session: + not_deleting = ~SearchSpace.name.startswith("[DELETING] ") + space_result = await session.execute( + select(SearchSpace) + .where(SearchSpace.name == smb, not_deleting) + .order_by(SearchSpace.id.asc()) + .limit(1) + ) + space = space_result.scalars().first() + if space is None: + logger.debug( + "SMB auto-join: no search space named %r — skipping", + smb, + ) + return + + existing_member = await session.execute( + select(SearchSpaceMembership.id).where( + SearchSpaceMembership.user_id == user_id, + SearchSpaceMembership.search_space_id == space.id, + ) + ) + if existing_member.scalars().first() is not None: + return + + role_result = await session.execute( + select(SearchSpaceRole).where( + SearchSpaceRole.search_space_id == space.id, + SearchSpaceRole.is_default == True, # noqa: E712 + ) + ) + role = role_result.scalars().first() + if role is None: + role_result = await session.execute( + select(SearchSpaceRole).where( + SearchSpaceRole.search_space_id == space.id, + SearchSpaceRole.name == "Editor", + ) + ) + role = role_result.scalars().first() + if role is None: + logger.warning( + "SMB auto-join: no default or Editor role for search space %s — skipping", + space.id, + ) + return + + membership = SearchSpaceMembership( + user_id=user_id, + search_space_id=space.id, + role_id=role.id, + is_owner=False, + ) + session.add(membership) + try: + await session.commit() + logger.info( + "SMB auto-join: joined user %s to search space %s (role=%s)", + user_id, + space.id, + role.name, + ) + except IntegrityError: + await session.rollback() + logger.debug( + "SMB auto-join: race for user %s / space %s — already joined", + user_id, + space.id, + ) diff --git a/surfsense_backend/scripts/provision-surfsense.py b/surfsense_backend/scripts/provision-surfsense.py new file mode 100644 index 0000000000..e69de29bb2 From ad7c980b693d645bfb0b7b17cc3da3fa9da0a2ee Mon Sep 17 00:00:00 2001 From: jawad-khan Date: Tue, 12 May 2026 14:40:25 +0500 Subject: [PATCH 2/4] fix: deleted unwanted file --- surfsense_backend/scripts/provision-surfsense.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 surfsense_backend/scripts/provision-surfsense.py diff --git a/surfsense_backend/scripts/provision-surfsense.py b/surfsense_backend/scripts/provision-surfsense.py deleted file mode 100644 index e69de29bb2..0000000000 From 75bcf377943ac1a3887072fe8cfd52d41f5ebc01 Mon Sep 17 00:00:00 2001 From: jawad-khan Date: Thu, 14 May 2026 11:14:02 +0500 Subject: [PATCH 3/4] fix: fixed sso workspace --- surfsense_backend/app/config/__init__.py | 7 +++++ .../app/middleware/proxy_auth.py | 3 +++ .../app/services/smb_auto_join.py | 26 ++++++++++++------- surfsense_backend/app/users.py | 17 ++++++++++++ 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/surfsense_backend/app/config/__init__.py b/surfsense_backend/app/config/__init__.py index 4eda2741d0..ca7d7d47d5 100644 --- a/surfsense_backend/app/config/__init__.py +++ b/surfsense_backend/app/config/__init__.py @@ -320,6 +320,13 @@ def is_cloud(cls) -> bool: # claim) instead of a full email address. DEFAULT_EMAIL_DOMAIN = os.getenv("DEFAULT_EMAIL_DOMAIN", "askii.ai") + # Portal hostname prefix (e.g. fossil for fossil.local...). + SMB_NAME = (os.getenv("SMB_NAME") or "").strip() + # Default shared search space name (SSO auto-join). Defaults to SMB_NAME. + SMB_DEFAULT_WORKSPACE_NAME = ( + os.getenv("SMB_DEFAULT_WORKSPACE_NAME") or os.getenv("SMB_NAME") or "" + ).strip() + # Google OAuth GOOGLE_OAUTH_CLIENT_ID = os.getenv("GOOGLE_OAUTH_CLIENT_ID") GOOGLE_OAUTH_CLIENT_SECRET = os.getenv("GOOGLE_OAUTH_CLIENT_SECRET") diff --git a/surfsense_backend/app/middleware/proxy_auth.py b/surfsense_backend/app/middleware/proxy_auth.py index 3b466eb294..e74f4ebd10 100644 --- a/surfsense_backend/app/middleware/proxy_auth.py +++ b/surfsense_backend/app/middleware/proxy_auth.py @@ -209,6 +209,9 @@ async def _resolve_user(self, email: str, request: Request) -> User | None: email, ) + # SMB shared-space join runs in users.current_active_user so Bearer JWT + # requests (no X-Auth-Request-Email on upstream) still enroll like Plane. + return user except Exception: diff --git a/surfsense_backend/app/services/smb_auto_join.py b/surfsense_backend/app/services/smb_auto_join.py index 1d7941b4c7..7cefdcff31 100644 --- a/surfsense_backend/app/services/smb_auto_join.py +++ b/surfsense_backend/app/services/smb_auto_join.py @@ -1,4 +1,4 @@ -"""Ensure SSO users are members of the shared SMB SearchSpace (SMB_NAME).""" +"""Ensure SSO users are members of the shared SMB SearchSpace (SMB_DEFAULT_WORKSPACE_NAME).""" from __future__ import annotations @@ -21,22 +21,28 @@ async def auto_join_smb_search_space(user_id: uuid.UUID) -> None: """ - If ``SMB_NAME`` is set and a matching SearchSpace exists, insert membership - for ``user_id`` using the default invite role (Editor) when absent. + Same resolution as Plane ``_auto_join_workspace``: match the search space whose + name equals ``SMB_DEFAULT_WORKSPACE_NAME`` or ``SMB_NAME``. If none exists, + do nothing (no fallback to another space). - Idempotent. Safe when auth uses Bearer JWT only (no ForwardAuth headers): - ``ProxyAuthMiddleware`` skips resolution for those requests, so this runs - from ``current_active_user`` as well. + Uses the default invite role (Editor) when absent. Idempotent. + Safe when auth uses Bearer JWT only: runs from ``current_active_user`` as well. """ - smb = (getattr(config, "SMB_NAME", None) or "").strip() - if not smb: + smb_slug = ( + getattr(config, "SMB_DEFAULT_WORKSPACE_NAME", None) + or getattr(config, "SMB_NAME", "") + or "" + ) + if isinstance(smb_slug, str): + smb_slug = smb_slug.strip() + if not smb_slug: return async with async_session_maker() as session: not_deleting = ~SearchSpace.name.startswith("[DELETING] ") space_result = await session.execute( select(SearchSpace) - .where(SearchSpace.name == smb, not_deleting) + .where(SearchSpace.name == smb_slug, not_deleting) .order_by(SearchSpace.id.asc()) .limit(1) ) @@ -44,7 +50,7 @@ async def auto_join_smb_search_space(user_id: uuid.UUID) -> None: if space is None: logger.debug( "SMB auto-join: no search space named %r — skipping", - smb, + smb_slug, ) return diff --git a/surfsense_backend/app/users.py b/surfsense_backend/app/users.py index e35cb99911..8de25d6c95 100644 --- a/surfsense_backend/app/users.py +++ b/surfsense_backend/app/users.py @@ -16,6 +16,7 @@ from sqlalchemy import update from app.config import config +from app.services.smb_auto_join import auto_join_smb_search_space from app.db import ( Prompt, SearchSpace, @@ -312,11 +313,27 @@ async def current_active_user( mPass proxy auth is active). Falls back to JWT Bearer token validation so existing email/password and Google OAuth flows continue to work when proxy auth is disabled. + + SMB shared SearchSpace membership (Plane parity) is enforced here — not only in + ProxyAuthMiddleware — because after proxy-login the browser usually sends Bearer + JWT without X-Auth-Request-Email, so middleware alone would never run auto-join. """ proxy_user = getattr(request.state, "proxy_user", None) if proxy_user is not None: + try: + await auto_join_smb_search_space(proxy_user.id) + except Exception: + logger.exception( + "SMB auto-join failed for proxy session user %s", proxy_user.id + ) return proxy_user if jwt_user is not None: + try: + await auto_join_smb_search_space(jwt_user.id) + except Exception: + logger.exception( + "SMB auto-join failed for JWT user %s", jwt_user.id + ) return jwt_user raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, From 648056dfc13af58359fe2161f1e95b00d3d9eb23 Mon Sep 17 00:00:00 2001 From: Usama Sadiq Date: Thu, 14 May 2026 16:37:34 +0500 Subject: [PATCH 4/4] fix: apply suggestions from code review Co-authored-by: Usama Sadiq --- surfsense_backend/app/middleware/proxy_auth.py | 3 --- surfsense_backend/app/services/smb_auto_join.py | 2 +- surfsense_backend/app/users.py | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/surfsense_backend/app/middleware/proxy_auth.py b/surfsense_backend/app/middleware/proxy_auth.py index e74f4ebd10..3b466eb294 100644 --- a/surfsense_backend/app/middleware/proxy_auth.py +++ b/surfsense_backend/app/middleware/proxy_auth.py @@ -209,9 +209,6 @@ async def _resolve_user(self, email: str, request: Request) -> User | None: email, ) - # SMB shared-space join runs in users.current_active_user so Bearer JWT - # requests (no X-Auth-Request-Email on upstream) still enroll like Plane. - return user except Exception: diff --git a/surfsense_backend/app/services/smb_auto_join.py b/surfsense_backend/app/services/smb_auto_join.py index 7cefdcff31..a2ae182f5d 100644 --- a/surfsense_backend/app/services/smb_auto_join.py +++ b/surfsense_backend/app/services/smb_auto_join.py @@ -21,7 +21,7 @@ async def auto_join_smb_search_space(user_id: uuid.UUID) -> None: """ - Same resolution as Plane ``_auto_join_workspace``: match the search space whose + ``_auto_join_workspace``: match the search space whose name equals ``SMB_DEFAULT_WORKSPACE_NAME`` or ``SMB_NAME``. If none exists, do nothing (no fallback to another space). diff --git a/surfsense_backend/app/users.py b/surfsense_backend/app/users.py index 8de25d6c95..0fc8b6f722 100644 --- a/surfsense_backend/app/users.py +++ b/surfsense_backend/app/users.py @@ -314,7 +314,7 @@ async def current_active_user( so existing email/password and Google OAuth flows continue to work when proxy auth is disabled. - SMB shared SearchSpace membership (Plane parity) is enforced here — not only in + SMB shared SearchSpace membership is enforced here — not only in ProxyAuthMiddleware — because after proxy-login the browser usually sends Bearer JWT without X-Auth-Request-Email, so middleware alone would never run auto-join. """