feat(auth): add PAT fail-closed bootstrap allowlist

This commit is contained in:
Anish Sarkar 2026-06-20 01:57:37 +05:30
parent 49b5247210
commit 2315b2f344
4 changed files with 14 additions and 18 deletions

View file

@ -56,7 +56,7 @@ from app.routes import router as crud_router
from app.routes.auth_routes import router as auth_router
from app.schemas import UserCreate, UserRead, UserUpdate
from app.session_events import register_session_hooks
from app.users import SECRET, auth_backend, fastapi_users, get_auth_context
from app.users import SECRET, allow_any_principal, auth_backend, fastapi_users
from app.utils.perf import log_system_snapshot
_error_logger = logging.getLogger("surfsense.errors")
@ -1033,7 +1033,7 @@ async def readiness_check():
@app.get("/verify-token")
async def authenticated_route(
auth: AuthContext = Depends(get_auth_context),
auth: AuthContext = Depends(allow_any_principal),
session: AsyncSession = Depends(get_async_session),
):
return {"message": "Token is valid", "method": auth.method}

View file

@ -53,7 +53,7 @@ from app.services.obsidian_plugin_indexer import (
upsert_note,
)
from app.tasks.celery_tasks.obsidian_tasks import index_obsidian_attachment_task
from app.users import get_auth_context
from app.users import allow_any_principal, get_auth_context
from app.utils.rbac import check_search_space_access
logger = logging.getLogger(__name__)
@ -255,7 +255,7 @@ async def _ensure_search_space_access(
@router.get("/health", response_model=HealthResponse)
async def obsidian_health(
_auth: AuthContext = Depends(get_auth_context),
_auth: AuthContext = Depends(allow_any_principal),
) -> HealthResponse:
"""Return the API contract handshake; plugin caches it per onload."""
return HealthResponse(

View file

@ -11,7 +11,6 @@ from app.db import (
SearchSpace,
SearchSpaceMembership,
SearchSpaceRole,
User,
get_async_session,
get_default_roles_config,
)
@ -22,7 +21,7 @@ from app.schemas import (
SearchSpaceUpdate,
SearchSpaceWithStats,
)
from app.users import get_auth_context
from app.users import allow_any_principal, get_auth_context, require_session_context
from app.utils.rbac import check_permission, check_search_space_access
logger = logging.getLogger(__name__)
@ -76,7 +75,7 @@ async def create_default_roles_and_membership(
async def create_search_space(
search_space: SearchSpaceCreate,
session: AsyncSession = Depends(get_async_session),
auth: AuthContext = Depends(get_auth_context),
auth: AuthContext = Depends(require_session_context),
):
user = auth.user
try:
@ -111,7 +110,7 @@ async def read_search_spaces(
limit: int = 200,
owned_only: bool = False,
session: AsyncSession = Depends(get_async_session),
auth: AuthContext = Depends(get_auth_context),
auth: AuthContext = Depends(allow_any_principal),
):
user = auth.user
"""
@ -209,7 +208,6 @@ async def read_search_space(
session: AsyncSession = Depends(get_async_session),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
Get a specific search space by ID.
Requires SETTINGS_VIEW permission or membership.
@ -243,7 +241,6 @@ async def update_search_space(
session: AsyncSession = Depends(get_async_session),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
Update a search space.
Requires SETTINGS_UPDATE permission.
@ -289,7 +286,6 @@ async def update_search_space_api_access(
session: AsyncSession = Depends(get_async_session),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
Toggle programmatic API/PAT access for a search space.
Requires API_ACCESS_MANAGE permission.
@ -373,7 +369,6 @@ async def delete_search_space(
session: AsyncSession = Depends(get_async_session),
auth: AuthContext = Depends(get_auth_context),
):
user = auth.user
"""
Delete a search space.
Requires SETTINGS_DELETE permission (only owners have this by default).

View file

@ -349,15 +349,16 @@ async def get_auth_context(
return AuthContext.session(user)
async def current_active_user(
async def allow_any_principal(
auth: AuthContext = Depends(get_auth_context),
) -> User:
"""Compatibility wrapper for identity-only routes.
) -> AuthContext:
"""Allow either session or PAT principals for bootstrap probes only.
Do not use this for space-scoped authorization or session-grade account
actions. Those should depend on get_auth_context or require_session_context.
Routes using this dependency intentionally have no search-space gate.
Adding a new call site is a security decision and must be covered by
the fail-closed PAT allowlist test.
"""
return auth.user
return auth
async def require_session_context(