Fix authentication to support both header and cookie auth
All API endpoints now use require_auth or get_current_user which handle both Authorization header (for CLI) and cookies (for browser). Previously many endpoints only checked cookies via get_user_from_cookie. Changed files: - runs.py: list_runs, run_detail, run_plan, run_artifacts, plan_node_detail, ui_discard_run - recipes.py: list_recipes, get_recipe, ui_discard_recipe - storage.py: list_storage, add_storage_form, delete_storage, test_storage, storage_type_page - cache.py: get_cached, list_media, get_metadata_form, update_metadata_htmx Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -47,8 +47,7 @@ async def get_cached(
|
|||||||
cache_service: CacheService = Depends(get_cache_service),
|
cache_service: CacheService = Depends(get_cache_service),
|
||||||
):
|
):
|
||||||
"""Get cached content by hash. Content negotiation: HTML for browsers, JSON for APIs."""
|
"""Get cached content by hash. Content negotiation: HTML for browsers, JSON for APIs."""
|
||||||
auth_service = AuthService(get_redis_client())
|
ctx = await get_current_user(request)
|
||||||
ctx = auth_service.get_user_from_cookie(request)
|
|
||||||
|
|
||||||
# Pass actor_id to get friendly name and user-specific metadata
|
# Pass actor_id to get friendly name and user-specific metadata
|
||||||
actor_id = ctx.actor_id if ctx else None
|
actor_id = ctx.actor_id if ctx else None
|
||||||
@@ -255,17 +254,9 @@ async def list_media(
|
|||||||
limit: int = 24,
|
limit: int = 24,
|
||||||
media_type: Optional[str] = None,
|
media_type: Optional[str] = None,
|
||||||
cache_service: CacheService = Depends(get_cache_service),
|
cache_service: CacheService = Depends(get_cache_service),
|
||||||
|
ctx: UserContext = Depends(require_auth),
|
||||||
):
|
):
|
||||||
"""List all media in cache."""
|
"""List all media in cache."""
|
||||||
auth_service = AuthService(get_redis_client())
|
|
||||||
ctx = auth_service.get_user_from_cookie(request)
|
|
||||||
|
|
||||||
if not ctx:
|
|
||||||
if wants_json(request):
|
|
||||||
raise HTTPException(401, "Authentication required")
|
|
||||||
from fastapi.responses import RedirectResponse
|
|
||||||
return RedirectResponse(url="/auth", status_code=302)
|
|
||||||
|
|
||||||
items = await cache_service.list_media(
|
items = await cache_service.list_media(
|
||||||
actor_id=ctx.actor_id,
|
actor_id=ctx.actor_id,
|
||||||
username=ctx.username,
|
username=ctx.username,
|
||||||
@@ -301,9 +292,7 @@ async def get_metadata_form(
|
|||||||
cache_service: CacheService = Depends(get_cache_service),
|
cache_service: CacheService = Depends(get_cache_service),
|
||||||
):
|
):
|
||||||
"""Get metadata editing form (HTMX)."""
|
"""Get metadata editing form (HTMX)."""
|
||||||
auth_service = AuthService(get_redis_client())
|
ctx = await get_current_user(request)
|
||||||
ctx = auth_service.get_user_from_cookie(request)
|
|
||||||
|
|
||||||
if not ctx:
|
if not ctx:
|
||||||
return HTMLResponse('<div class="text-red-400">Login required</div>')
|
return HTMLResponse('<div class="text-red-400">Login required</div>')
|
||||||
|
|
||||||
@@ -341,9 +330,7 @@ async def update_metadata_htmx(
|
|||||||
cache_service: CacheService = Depends(get_cache_service),
|
cache_service: CacheService = Depends(get_cache_service),
|
||||||
):
|
):
|
||||||
"""Update metadata (HTMX form handler)."""
|
"""Update metadata (HTMX form handler)."""
|
||||||
auth_service = AuthService(get_redis_client())
|
ctx = await get_current_user(request)
|
||||||
ctx = auth_service.get_user_from_cookie(request)
|
|
||||||
|
|
||||||
if not ctx:
|
if not ctx:
|
||||||
return HTMLResponse('<div class="text-red-400">Login required</div>')
|
return HTMLResponse('<div class="text-red-400">Login required</div>')
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from artdag_common import render
|
|||||||
from artdag_common.middleware import wants_html, wants_json
|
from artdag_common.middleware import wants_html, wants_json
|
||||||
from artdag_common.middleware.auth import UserContext
|
from artdag_common.middleware.auth import UserContext
|
||||||
|
|
||||||
from ..dependencies import require_auth, get_templates, get_redis_client, get_cache_manager
|
from ..dependencies import require_auth, get_current_user, get_templates, get_redis_client, get_cache_manager
|
||||||
from ..services.auth_service import AuthService
|
from ..services.auth_service import AuthService
|
||||||
from ..services.recipe_service import RecipeService
|
from ..services.recipe_service import RecipeService
|
||||||
from ..types import (
|
from ..types import (
|
||||||
@@ -365,17 +365,9 @@ async def list_recipes(
|
|||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
recipe_service: RecipeService = Depends(get_recipe_service),
|
recipe_service: RecipeService = Depends(get_recipe_service),
|
||||||
|
ctx: UserContext = Depends(require_auth),
|
||||||
):
|
):
|
||||||
"""List available recipes."""
|
"""List available recipes."""
|
||||||
auth_service = AuthService(get_redis_client())
|
|
||||||
ctx = auth_service.get_user_from_cookie(request)
|
|
||||||
|
|
||||||
if not ctx:
|
|
||||||
if wants_json(request):
|
|
||||||
raise HTTPException(401, "Authentication required")
|
|
||||||
from fastapi.responses import RedirectResponse
|
|
||||||
return RedirectResponse(url="/auth", status_code=302)
|
|
||||||
|
|
||||||
recipes = await recipe_service.list_recipes(ctx.actor_id, offset=offset, limit=limit)
|
recipes = await recipe_service.list_recipes(ctx.actor_id, offset=offset, limit=limit)
|
||||||
has_more = len(recipes) >= limit
|
has_more = len(recipes) >= limit
|
||||||
|
|
||||||
@@ -402,17 +394,9 @@ async def get_recipe(
|
|||||||
recipe_id: str,
|
recipe_id: str,
|
||||||
request: Request,
|
request: Request,
|
||||||
recipe_service: RecipeService = Depends(get_recipe_service),
|
recipe_service: RecipeService = Depends(get_recipe_service),
|
||||||
|
ctx: UserContext = Depends(require_auth),
|
||||||
):
|
):
|
||||||
"""Get recipe details."""
|
"""Get recipe details."""
|
||||||
auth_service = AuthService(get_redis_client())
|
|
||||||
ctx = auth_service.get_user_from_cookie(request)
|
|
||||||
|
|
||||||
if not ctx:
|
|
||||||
if wants_json(request):
|
|
||||||
raise HTTPException(401, "Authentication required")
|
|
||||||
from fastapi.responses import RedirectResponse
|
|
||||||
return RedirectResponse(url="/auth", status_code=302)
|
|
||||||
|
|
||||||
recipe = await recipe_service.get_recipe(recipe_id)
|
recipe = await recipe_service.get_recipe(recipe_id)
|
||||||
if not recipe:
|
if not recipe:
|
||||||
raise HTTPException(404, "Recipe not found")
|
raise HTTPException(404, "Recipe not found")
|
||||||
@@ -640,9 +624,7 @@ async def ui_discard_recipe(
|
|||||||
recipe_service: RecipeService = Depends(get_recipe_service),
|
recipe_service: RecipeService = Depends(get_recipe_service),
|
||||||
):
|
):
|
||||||
"""HTMX handler: discard a recipe."""
|
"""HTMX handler: discard a recipe."""
|
||||||
auth_service = AuthService(get_redis_client())
|
ctx = await get_current_user(request)
|
||||||
ctx = auth_service.get_user_from_cookie(request)
|
|
||||||
|
|
||||||
if not ctx:
|
if not ctx:
|
||||||
return HTMLResponse('<div class="text-red-400">Login required</div>', status_code=401)
|
return HTMLResponse('<div class="text-red-400">Login required</div>', status_code=401)
|
||||||
|
|
||||||
|
|||||||
@@ -370,18 +370,9 @@ async def list_runs(
|
|||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
run_service: RunService = Depends(get_run_service),
|
run_service: RunService = Depends(get_run_service),
|
||||||
|
ctx: UserContext = Depends(require_auth),
|
||||||
):
|
):
|
||||||
"""List all runs for the current user."""
|
"""List all runs for the current user."""
|
||||||
from ..services.auth_service import AuthService
|
|
||||||
|
|
||||||
auth_service = AuthService(get_redis_client())
|
|
||||||
ctx = auth_service.get_user_from_cookie(request)
|
|
||||||
|
|
||||||
if not ctx:
|
|
||||||
if wants_json(request):
|
|
||||||
raise HTTPException(401, "Authentication required")
|
|
||||||
from fastapi.responses import RedirectResponse
|
|
||||||
return RedirectResponse(url="/auth", status_code=302)
|
|
||||||
|
|
||||||
runs = await run_service.list_runs(ctx.actor_id, offset=offset, limit=limit)
|
runs = await run_service.list_runs(ctx.actor_id, offset=offset, limit=limit)
|
||||||
has_more = len(runs) >= limit
|
has_more = len(runs) >= limit
|
||||||
@@ -449,19 +440,9 @@ async def run_detail(
|
|||||||
run_id: str,
|
run_id: str,
|
||||||
request: Request,
|
request: Request,
|
||||||
run_service: RunService = Depends(get_run_service),
|
run_service: RunService = Depends(get_run_service),
|
||||||
|
ctx: UserContext = Depends(require_auth),
|
||||||
):
|
):
|
||||||
"""Run detail page with tabs for plan/analysis/artifacts."""
|
"""Run detail page with tabs for plan/analysis/artifacts."""
|
||||||
from ..services.auth_service import AuthService
|
|
||||||
|
|
||||||
auth_service = AuthService(get_redis_client())
|
|
||||||
ctx = auth_service.get_user_from_cookie(request)
|
|
||||||
|
|
||||||
if not ctx:
|
|
||||||
if wants_json(request):
|
|
||||||
raise HTTPException(401, "Authentication required")
|
|
||||||
from fastapi.responses import RedirectResponse
|
|
||||||
return RedirectResponse(url="/auth", status_code=302)
|
|
||||||
|
|
||||||
run = await run_service.get_run(run_id)
|
run = await run_service.get_run(run_id)
|
||||||
if not run:
|
if not run:
|
||||||
raise HTTPException(404, f"Run {run_id} not found")
|
raise HTTPException(404, f"Run {run_id} not found")
|
||||||
@@ -532,16 +513,9 @@ async def run_plan(
|
|||||||
run_id: str,
|
run_id: str,
|
||||||
request: Request,
|
request: Request,
|
||||||
run_service: RunService = Depends(get_run_service),
|
run_service: RunService = Depends(get_run_service),
|
||||||
|
ctx: UserContext = Depends(require_auth),
|
||||||
):
|
):
|
||||||
"""Plan visualization as interactive DAG."""
|
"""Plan visualization as interactive DAG."""
|
||||||
from ..services.auth_service import AuthService
|
|
||||||
|
|
||||||
auth_service = AuthService(get_redis_client())
|
|
||||||
ctx = auth_service.get_user_from_cookie(request)
|
|
||||||
|
|
||||||
if not ctx:
|
|
||||||
raise HTTPException(401, "Authentication required")
|
|
||||||
|
|
||||||
plan = await run_service.get_run_plan(run_id)
|
plan = await run_service.get_run_plan(run_id)
|
||||||
if not plan:
|
if not plan:
|
||||||
raise HTTPException(404, "Plan not found for this run")
|
raise HTTPException(404, "Plan not found for this run")
|
||||||
@@ -597,16 +571,9 @@ async def run_artifacts(
|
|||||||
run_id: str,
|
run_id: str,
|
||||||
request: Request,
|
request: Request,
|
||||||
run_service: RunService = Depends(get_run_service),
|
run_service: RunService = Depends(get_run_service),
|
||||||
|
ctx: UserContext = Depends(require_auth),
|
||||||
):
|
):
|
||||||
"""Get artifacts list for a run."""
|
"""Get artifacts list for a run."""
|
||||||
from ..services.auth_service import AuthService
|
|
||||||
|
|
||||||
auth_service = AuthService(get_redis_client())
|
|
||||||
ctx = auth_service.get_user_from_cookie(request)
|
|
||||||
|
|
||||||
if not ctx:
|
|
||||||
raise HTTPException(401, "Authentication required")
|
|
||||||
|
|
||||||
artifacts = await run_service.get_run_artifacts(run_id)
|
artifacts = await run_service.get_run_artifacts(run_id)
|
||||||
|
|
||||||
if wants_json(request):
|
if wants_json(request):
|
||||||
@@ -629,12 +596,9 @@ async def plan_node_detail(
|
|||||||
run_service: RunService = Depends(get_run_service),
|
run_service: RunService = Depends(get_run_service),
|
||||||
):
|
):
|
||||||
"""HTMX partial: Get plan node detail by cache_id."""
|
"""HTMX partial: Get plan node detail by cache_id."""
|
||||||
from ..services.auth_service import AuthService
|
|
||||||
from artdag_common import render_fragment
|
from artdag_common import render_fragment
|
||||||
|
|
||||||
auth_service = AuthService(get_redis_client())
|
ctx = await get_current_user(request)
|
||||||
ctx = auth_service.get_user_from_cookie(request)
|
|
||||||
|
|
||||||
if not ctx:
|
if not ctx:
|
||||||
return HTMLResponse('<p class="text-red-400">Login required</p>', status_code=401)
|
return HTMLResponse('<p class="text-red-400">Login required</p>', status_code=401)
|
||||||
|
|
||||||
@@ -732,11 +696,7 @@ async def ui_discard_run(
|
|||||||
run_service: RunService = Depends(get_run_service),
|
run_service: RunService = Depends(get_run_service),
|
||||||
):
|
):
|
||||||
"""HTMX handler: discard a run."""
|
"""HTMX handler: discard a run."""
|
||||||
from ..services.auth_service import AuthService
|
ctx = await get_current_user(request)
|
||||||
|
|
||||||
auth_service = AuthService(get_redis_client())
|
|
||||||
ctx = auth_service.get_user_from_cookie(request)
|
|
||||||
|
|
||||||
if not ctx:
|
if not ctx:
|
||||||
return HTMLResponse(
|
return HTMLResponse(
|
||||||
'<div class="text-red-400">Login required</div>',
|
'<div class="text-red-400">Login required</div>',
|
||||||
|
|||||||
@@ -47,19 +47,9 @@ class UpdateStorageRequest(BaseModel):
|
|||||||
async def list_storage(
|
async def list_storage(
|
||||||
request: Request,
|
request: Request,
|
||||||
storage_service: StorageService = Depends(get_storage_service),
|
storage_service: StorageService = Depends(get_storage_service),
|
||||||
|
ctx: UserContext = Depends(require_auth),
|
||||||
):
|
):
|
||||||
"""List user's storage providers. HTML for browsers, JSON for API."""
|
"""List user's storage providers. HTML for browsers, JSON for API."""
|
||||||
from ..services.auth_service import AuthService
|
|
||||||
from ..dependencies import get_redis_client
|
|
||||||
|
|
||||||
auth_service = AuthService(get_redis_client())
|
|
||||||
ctx = auth_service.get_user_from_cookie(request)
|
|
||||||
|
|
||||||
if not ctx:
|
|
||||||
if wants_json(request):
|
|
||||||
raise HTTPException(401, "Authentication required")
|
|
||||||
return RedirectResponse(url="/auth", status_code=302)
|
|
||||||
|
|
||||||
storages = await storage_service.list_storages(ctx.actor_id)
|
storages = await storage_service.list_storages(ctx.actor_id)
|
||||||
|
|
||||||
if wants_json(request):
|
if wants_json(request):
|
||||||
@@ -120,12 +110,7 @@ async def add_storage_form(
|
|||||||
storage_service: StorageService = Depends(get_storage_service),
|
storage_service: StorageService = Depends(get_storage_service),
|
||||||
):
|
):
|
||||||
"""Add a storage provider via HTML form."""
|
"""Add a storage provider via HTML form."""
|
||||||
from ..services.auth_service import AuthService
|
ctx = await get_current_user(request)
|
||||||
from ..dependencies import get_redis_client
|
|
||||||
|
|
||||||
auth_service = AuthService(get_redis_client())
|
|
||||||
ctx = auth_service.get_user_from_cookie(request)
|
|
||||||
|
|
||||||
if not ctx:
|
if not ctx:
|
||||||
return HTMLResponse('<div class="text-red-400">Not authenticated</div>', status_code=401)
|
return HTMLResponse('<div class="text-red-400">Not authenticated</div>', status_code=401)
|
||||||
|
|
||||||
@@ -208,17 +193,9 @@ async def delete_storage(
|
|||||||
storage_id: int,
|
storage_id: int,
|
||||||
request: Request,
|
request: Request,
|
||||||
storage_service: StorageService = Depends(get_storage_service),
|
storage_service: StorageService = Depends(get_storage_service),
|
||||||
|
ctx: UserContext = Depends(require_auth),
|
||||||
):
|
):
|
||||||
"""Remove a storage provider."""
|
"""Remove a storage provider."""
|
||||||
from ..services.auth_service import AuthService
|
|
||||||
from ..dependencies import get_redis_client
|
|
||||||
|
|
||||||
auth_service = AuthService(get_redis_client())
|
|
||||||
ctx = auth_service.get_user_from_cookie(request)
|
|
||||||
|
|
||||||
if not ctx:
|
|
||||||
raise HTTPException(401, "Not authenticated")
|
|
||||||
|
|
||||||
success, error = await storage_service.delete_storage(storage_id, ctx.actor_id)
|
success, error = await storage_service.delete_storage(storage_id, ctx.actor_id)
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
@@ -237,12 +214,7 @@ async def test_storage(
|
|||||||
storage_service: StorageService = Depends(get_storage_service),
|
storage_service: StorageService = Depends(get_storage_service),
|
||||||
):
|
):
|
||||||
"""Test storage provider connectivity."""
|
"""Test storage provider connectivity."""
|
||||||
from ..services.auth_service import AuthService
|
ctx = await get_current_user(request)
|
||||||
from ..dependencies import get_redis_client
|
|
||||||
|
|
||||||
auth_service = AuthService(get_redis_client())
|
|
||||||
ctx = auth_service.get_user_from_cookie(request)
|
|
||||||
|
|
||||||
if not ctx:
|
if not ctx:
|
||||||
if wants_html(request):
|
if wants_html(request):
|
||||||
return HTMLResponse('<span class="text-red-400">Not authenticated</span>', status_code=401)
|
return HTMLResponse('<span class="text-red-400">Not authenticated</span>', status_code=401)
|
||||||
@@ -262,19 +234,9 @@ async def storage_type_page(
|
|||||||
provider_type: str,
|
provider_type: str,
|
||||||
request: Request,
|
request: Request,
|
||||||
storage_service: StorageService = Depends(get_storage_service),
|
storage_service: StorageService = Depends(get_storage_service),
|
||||||
|
ctx: UserContext = Depends(require_auth),
|
||||||
):
|
):
|
||||||
"""Page for managing storage configs of a specific type."""
|
"""Page for managing storage configs of a specific type."""
|
||||||
from ..services.auth_service import AuthService
|
|
||||||
from ..dependencies import get_redis_client
|
|
||||||
|
|
||||||
auth_service = AuthService(get_redis_client())
|
|
||||||
ctx = auth_service.get_user_from_cookie(request)
|
|
||||||
|
|
||||||
if not ctx:
|
|
||||||
if wants_json(request):
|
|
||||||
raise HTTPException(401, "Authentication required")
|
|
||||||
return RedirectResponse(url="/auth", status_code=302)
|
|
||||||
|
|
||||||
if provider_type not in STORAGE_PROVIDERS_INFO:
|
if provider_type not in STORAGE_PROVIDERS_INFO:
|
||||||
raise HTTPException(404, "Invalid provider type")
|
raise HTTPException(404, "Invalid provider type")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user