Files
2026-07-13 13:12:00 +08:00

156 lines
6.2 KiB
Python

"""Admin route for the server-wide session-sharing policy.
``GET /v1/sharing`` reports two independent settings and whether each is
editable here: the sharing *mode* (the tri-state tier + tier list) and whether
*public* (anyone-with-the-link) access may be granted. ``PUT /v1/sharing``
sets either or both (admin only), persisting an override file
(``<data_dir>/sharing_mode`` / ``<data_dir>/public_sharing``) that the grant
gate and ``GET /v1/info`` read per request.
Editing a setting is only possible when the server resolves it from its file
(the OSS default — ``create_app(sharing_mode=None, public_sharing=None)``). A
deployment that injects its own resolver — a static value or a callable such as
a Databricks SAFE flag — reports that setting as not editable and rejects
writes to it, since its policy is authoritative elsewhere.
"""
from __future__ import annotations
import asyncio
from typing import Any
from fastapi import APIRouter, Request
from pydantic import BaseModel
from omnigent.errors import ErrorCode, OmnigentError
from omnigent.server.auth import AuthProvider, SharingMode
from omnigent.server.routes._auth_helpers import get_user_id
from omnigent.server.sharing_settings import (
write_public_sharing_override,
write_sharing_mode_override,
)
from omnigent.stores.permission_store import PermissionStore
# The tiers offered to admins, most-permissive first (matches the UI order).
_TIERS: tuple[SharingMode, ...] = (
SharingMode.ON,
SharingMode.READ_ONLY,
SharingMode.RESTRICTED_READ_ONLY,
SharingMode.OFF,
)
class SetSharingRequest(BaseModel):
"""Body for ``PUT /v1/sharing``.
Both fields are optional so an admin can update either setting
independently; at least one must be present.
"""
sharing_mode: str | None = None
public_sharing: bool | None = None
def _state_response(request: Request) -> dict[str, Any]:
"""Shape the sharing-settings payload from live ``app.state`` — shared by
GET and PUT so both reflect any override just written."""
state = request.app.state
mode: SharingMode = state.sharing_mode()
return {
"object": "sharing",
"sharing_mode": mode.value,
"editable": bool(getattr(state, "sharing_mode_writable", False)),
"options": [tier.value for tier in _TIERS],
"public_sharing_enabled": bool(state.public_sharing()),
"public_sharing_editable": bool(getattr(state, "public_sharing_writable", False)),
}
async def _require_admin(
request: Request,
auth_provider: AuthProvider | None,
permission_store: PermissionStore | None,
) -> None:
"""Verify the caller is an admin, mirroring the default-policies gate.
Single-user mode (no permission store) skips the check. Multi-user mode
raises 401 if unauthenticated or 403 if the user is not an admin.
"""
if permission_store is None:
return
user_id = get_user_id(request, auth_provider)
if user_id is None:
raise OmnigentError("Authentication required", code=ErrorCode.UNAUTHORIZED)
is_admin = await asyncio.to_thread(permission_store.is_admin, user_id)
if not is_admin:
raise OmnigentError(
"Admin privileges required to manage sharing settings",
code=ErrorCode.FORBIDDEN,
)
def create_sharing_router(
auth_provider: AuthProvider | None = None,
permission_store: PermissionStore | None = None,
) -> APIRouter:
"""Build the admin sharing router (mounted under ``/v1``)."""
router = APIRouter()
@router.get("/sharing")
async def get_sharing(request: Request) -> dict[str, Any]:
"""Report both settings, whether each is editable here, and the tiers."""
await _require_admin(request, auth_provider, permission_store)
return _state_response(request)
@router.put("/sharing")
async def set_sharing(request: Request, body: SetSharingRequest) -> dict[str, Any]:
"""Set the sharing mode and/or public-access setting (admin only).
Updates only the fields present in the body; requires at least one.
Rejects an unknown mode value with 400 (no fail-open coercion — an admin
setting a value should learn about a typo). Rejects a write to a setting
the deployment manages itself (not file-backed) with 403.
"""
await _require_admin(request, auth_provider, permission_store)
state = request.app.state
if body.sharing_mode is None and body.public_sharing is None:
raise OmnigentError(
"No sharing settings to update.",
code=ErrorCode.INVALID_INPUT,
)
# Validate AND authorize both fields before writing either, so a request
# updating both never persists one and then rejects the other (a partial
# apply — reachable only when a deployment makes exactly one setting
# file-backed and the other a managed callable).
mode: SharingMode | None = None
if body.sharing_mode is not None:
if not getattr(state, "sharing_mode_writable", False):
raise OmnigentError(
"Sharing mode is managed by this deployment and cannot be changed here.",
code=ErrorCode.FORBIDDEN,
)
try:
mode = SharingMode(body.sharing_mode.strip().lower())
except ValueError as exc:
raise OmnigentError(
f"Unknown sharing mode {body.sharing_mode!r}. Expected one of: "
+ ", ".join(tier.value for tier in _TIERS)
+ ".",
code=ErrorCode.INVALID_INPUT,
) from exc
if body.public_sharing is not None and not getattr(
state, "public_sharing_writable", False
):
raise OmnigentError(
"Public access is managed by this deployment and cannot be changed here.",
code=ErrorCode.FORBIDDEN,
)
# All checks passed — apply the writes.
if mode is not None:
await asyncio.to_thread(write_sharing_mode_override, mode)
if body.public_sharing is not None:
await asyncio.to_thread(write_public_sharing_override, body.public_sharing)
return _state_response(request)
return router