Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
17ca940
feat(surfsense): auto-provision org-space LiteLLM key on first SMB ad…
hunzlahmalik Jun 3, 2026
32f1eeb
perf(surfsense): skip org-space DB lookup when mPass token absent
hunzlahmalik Jun 3, 2026
df08e71
fix(backend): import SECRET from app.users in app.py
AhtishamShahid Jun 3, 2026
6aa6664
feat(web): hide download UI when SSO auth is active
Jun 3, 2026
f97b392
perf(surfsense): skip org provisioning step + commit on non-mPass logins
hunzlahmalik Jun 3, 2026
24380f3
fix(ci): apply ruff-format and biome auto-fixes to clear quality gate
AhtishamShahid Jun 3, 2026
98515e7
fix(web): gate sidebar dropdown download items under SSO auth
Jun 3, 2026
939e7a0
fix(ci): install libreadline-dev before pnpm install in TS quality job
AhtishamShahid Jun 3, 2026
b76e9ce
fix(surfsense): use hard redirect on SSO callback to fix blank displa…
Jun 3, 2026
5719df5
Merge pull request #29 from Pressingly/feat/org-space-litellm-provisi…
UsamaSadiq Jun 4, 2026
21a6794
Merge pull request #30 from Pressingly/chore/ci-check-diagnosis
UsamaSadiq Jun 4, 2026
d025520
Update PR template to standard format
UsamaSadiq Jun 4, 2026
8325216
Merge pull request #33 from Pressingly/usama/standardize-pr-template
UsamaSadiq Jun 4, 2026
07f6d2f
Merge pull request #31 from Pressingly/feat/hide-download-ui-on-sso-auth
UsamaSadiq Jun 5, 2026
65226d7
Merge pull request #32 from Pressingly/fix/sso-display-name-blank-on-…
UsamaSadiq Jun 5, 2026
7c2c7f6
Add corporate_id check to ProxyAuth middleware
UsamaSadiq Jul 7, 2026
2919212
Merge pull request #34 from Pressingly/usama/enforce-corporate-id-auth
UsamaSadiq Jul 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 16 additions & 30 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,27 @@
<!--- Summarize your pull request in a few sentences -->
## Related Ticket
<!-- Link the Plane ticket: https://foss-pm.local.moneta.dev/... -->

## Description
<!--- Clearly describe what has changed in this pull request -->
<!-- What changed and why? Link related issues: FIX #123 -->

## Motivation and Context
<!--- Why is this change required? What problem does it solve? -->
<!--- If this PR relates to an open issue, please link to the issue here: FIX #123 -->
FIX #


## Screenshots
<!-- If applicable, add screenshots or images to demonstrate the changes visually -->

## API Changes
<!-- Document any API changes if applicable -->
- [ ] This PR includes API changes

## Change Type
<!--- Indicate what kind(s) of changes this PR includes: -->
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Performance improvement
- [ ] Improvement / Enhancement
- [ ] Refactoring
- [ ] Performance improvement
- [ ] Documentation
- [ ] Dependency/Build system
- [ ] Breaking change
- [ ] Other (specify):
- [ ] Infrastructure / CI

## Testing Performed
<!--- Briefly describe how you have tested these changes and what verification was performed -->
## Testing
<!-- How did you verify the change? -->
- [ ] Tested locally
- [ ] Manual/QA verification
- [ ] New / updated tests included

## Screenshots
<!-- If applicable, add before/after screenshots -->

## Checklist
<!--- Please confirm the following by marking with an 'x' as appropriate -->
- [ ] Follows project coding standards and conventions
- [ ] Documentation updated as needed
- [ ] Dependencies updated as needed
- [ ] No lint/build errors or new warnings
- [ ] All relevant tests are passing
- [ ] No lint or build errors
- [ ] All existing tests pass
- [ ] Documentation updated (if needed)
4 changes: 4 additions & 0 deletions .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@ jobs:
extension:
- 'surfsense_browser_extension/**'

- name: Install native build dependencies
if: steps.frontend-changes.outputs.web == 'true'
run: sudo apt-get install -y libreadline-dev

- name: Install dependencies for web
if: steps.frontend-changes.outputs.web == 'true'
working-directory: surfsense_web
Expand Down
2 changes: 1 addition & 1 deletion surfsense_backend/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ down_revision: str | None = "<previous revision — fork or upstream>"

1. Find the latest revision in `alembic/versions/` (either upstream integer or
prior `moneta_*` — whichever is more recent).
2. Pick the next available `moneta_NNN`. Current next: `moneta_002`.
2. Pick the next available `moneta_NNN`. Current next: `moneta_003`.
3. Name the file `moneta_NNN_<short_description>.py`.
4. Set `revision = "moneta_NNN"`, `down_revision = "<latest revision found in step 1>"`.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""moneta_002_add_litellm_auto_provisioned_at_to_searchspace

Revision ID: moneta_002
Revises: moneta_001
Create Date: 2026-06-02

Fork-migration convention
-------------------------
Moneta-fork migration (``moneta_NNN`` namespace) — see
``surfsense_backend/CLAUDE.md`` § "Fork migrations". Chains off ``moneta_001``.
Next fork migration should be ``moneta_003``.

Adds ``litellm_auto_provisioned_at`` (nullable timestamp) to the
``searchspaces`` table. This is the one-shot marker for **org-space** Askii
LiteLLM auto-provisioning (``app.services.litellm_provisioning``): when the
``is_owner`` admin of the shared SMB/Organization space logs in, a fresh Askii
key is minted and the four ``SearchSpace.*_id`` LLM FKs are wired onto that
space — exactly once.

- NULL → org space has never been auto-provisioned → eligible.
- non-NULL → provisioned once at this time → permanently ineligible,
regardless of whether the four config rows still exist.

It is the ``SearchSpace``-scoped analogue of the user-scoped
``user.litellm_auto_provisioned_at`` marker added in ``moneta_001``.

No backfill: org-space auto-provisioning did not exist before this migration,
so there are no already-provisioned org spaces to stamp.
"""

from __future__ import annotations

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

revision: str = "moneta_002"
down_revision: str | None = "moneta_001"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
conn = op.get_bind()
existing_columns = [
col["name"] for col in sa.inspect(conn).get_columns("searchspaces")
]

if "litellm_auto_provisioned_at" not in existing_columns:
op.add_column(
"searchspaces",
sa.Column(
"litellm_auto_provisioned_at",
sa.TIMESTAMP(timezone=True),
nullable=True,
),
)


def downgrade() -> None:
conn = op.get_bind()
existing_columns = [
col["name"] for col in sa.inspect(conn).get_columns("searchspaces")
]

if "litellm_auto_provisioned_at" in existing_columns:
op.drop_column("searchspaces", "litellm_auto_provisioned_at")
4 changes: 4 additions & 0 deletions surfsense_backend/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from app.schemas import UserCreate, UserRead, UserUpdate
from app.tasks.surfsense_docs_indexer import seed_surfsense_docs
from app.users import (
SECRET,
auth_backend,
current_active_user,
fastapi_users,
Expand Down Expand Up @@ -819,6 +820,7 @@ async def dispatch(
tags=["auth"],
)


# Register /users/me BEFORE fastapi_users.get_users_router so our routes take
# precedence (FastAPI first-match wins). fastapi-users' internal /users/me only
# validates JWT — it does not check request.state.proxy_user set by the proxy
Expand All @@ -842,6 +844,8 @@ async def update_current_user_me(
# JWT-loaded one. user_manager.get() returns the session-attached instance.
db_user = await user_manager.get(user.id)
return await user_manager.update(user_update, db_user, safe=True, request=request)


app.include_router(
fastapi_users.get_users_router(UserRead, UserUpdate),
prefix="/users",
Expand Down
4 changes: 4 additions & 0 deletions surfsense_backend/app/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,10 @@ def is_cloud(cls) -> bool:
os.getenv("SMB_DEFAULT_WORKSPACE_NAME") or os.getenv("SMB_NAME") or ""
).strip()

# Corporate tenant ID for cross-tenant isolation. When set, the proxy-auth
# middleware rejects access tokens whose custom:corporate_id does not match.
SMB_CORPORATE_ID = (os.getenv("SMB_CORPORATE_ID") or "").strip()

# Google OAuth
GOOGLE_OAUTH_CLIENT_ID = os.getenv("GOOGLE_OAUTH_CLIENT_ID")
GOOGLE_OAUTH_CLIENT_SECRET = os.getenv("GOOGLE_OAUTH_CLIENT_SECRET")
Expand Down
6 changes: 6 additions & 0 deletions surfsense_backend/app/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,12 @@ class SearchSpace(BaseModel, TimestampMixin):
Integer, nullable=True, default=0
) # For vision/screenshot analysis, defaults to Auto mode

# Org-space one-shot LiteLLM auto-provision marker (Moneta fork).
# NULL → eligible; non-NULL → provisioned once, permanently ineligible.
# SearchSpace-scoped analogue of User.litellm_auto_provisioned_at; stamped
# when the is_owner admin of the shared SMB/Organization space logs in.
litellm_auto_provisioned_at = Column(TIMESTAMP(timezone=True), nullable=True)

ai_file_sort_enabled = Column(
Boolean, nullable=False, default=False, server_default="false"
)
Expand Down
29 changes: 28 additions & 1 deletion surfsense_backend/app/middleware/proxy_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
import secrets
import unicodedata

import jwt
from fastapi_users.db import SQLAlchemyUserDatabase
from fastapi_users.password import PasswordHelper # singleton below
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request
from starlette.responses import Response
from starlette.responses import JSONResponse, Response

from app.config import config
from app.db import User, async_session_maker
Expand All @@ -33,6 +34,29 @@ def _coerce_bypass_paths(setting) -> list[str]:
return list(setting)


def _check_corporate_id(request) -> bool:
"""Verify that the caller's access token belongs to this deployment's tenant.

When ``SMB_CORPORATE_ID`` is configured, only corporate tokens whose
``custom:corporate_id`` claim matches the expected value are allowed.
Individual (non-corporate) tokens are rejected. When the setting is
empty the check is skipped entirely for backward compatibility.
"""
expected = getattr(config, "SMB_CORPORATE_ID", "")
if not expected:
return True
access_token = request.headers.get("x-auth-request-access-token")
if not access_token:
return False
try:
claims = jwt.decode(access_token, options={"verify_signature": False})
except Exception:
return False
if claims.get("custom:is_corporate") != "true":
return False
return claims.get("custom:corporate_id") == expected


def _is_bypass_path(path: str, bypass_paths: list[str]) -> bool:
# Match exact path OR a true subpath (e.g. /health/ready) but NOT a path that
# merely starts with the same characters (e.g. /healthz must NOT bypass /health).
Expand Down Expand Up @@ -98,6 +122,9 @@ async def dispatch(
)
return await call_next(request)

if not _check_corporate_id(request):
return JSONResponse(status_code=403, content={"error": "access_denied"})

user = await self._resolve_user(_normalise_email(raw_email), request)

# Respect deactivated accounts — mPass authentication does not
Expand Down
Loading
Loading