Merge dev to main: Security improvements and comprehensive test infrastructure #39
@@ -9,7 +9,7 @@ from slowapi.errors import RateLimitExceeded
|
|||||||
from routers import docker_router, gitea, files, terminal, system, auth, chat_summary, security, passkey, totp, litellm, cc_connect, info_engine, opc_tasks, opc_agents, opc_ws, opc_projects
|
from routers import docker_router, gitea, files, terminal, system, auth, chat_summary, security, passkey, totp, litellm, cc_connect, info_engine, opc_tasks, opc_agents, opc_ws, opc_projects
|
||||||
import auth as auth_module
|
import auth as auth_module
|
||||||
import config
|
import config
|
||||||
from rbac import require_page, _inject_user
|
from rbac import require_page
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
@@ -164,6 +164,11 @@ def _router_reachable():
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Dependency to store user in request.state for rbac dependencies
|
||||||
|
async def _inject_user(request: Request, user = Depends(auth_module.get_current_user)):
|
||||||
|
request.state.user = user
|
||||||
|
return user
|
||||||
|
|
||||||
# Public auth endpoints
|
# Public auth endpoints
|
||||||
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
||||||
app.include_router(passkey.router, prefix="/api/auth", tags=["passkey"])
|
app.include_router(passkey.router, prefix="/api/auth", tags=["passkey"])
|
||||||
@@ -191,11 +196,11 @@ app.include_router(security.router, prefix="/api/security",
|
|||||||
|
|
||||||
# OPC endpoints
|
# OPC endpoints
|
||||||
app.include_router(opc_tasks.router, prefix="/api/opc",
|
app.include_router(opc_tasks.router, prefix="/api/opc",
|
||||||
dependencies=[Depends(require_page("opc"))])
|
dependencies=[Depends(_inject_user), Depends(require_page("opc"))])
|
||||||
app.include_router(opc_agents.router, prefix="/api/opc",
|
app.include_router(opc_agents.router, prefix="/api/opc",
|
||||||
dependencies=[Depends(require_page("opc"))])
|
dependencies=[Depends(_inject_user), Depends(require_page("opc"))])
|
||||||
app.include_router(opc_projects.router, prefix="/api/opc",
|
app.include_router(opc_projects.router, prefix="/api/opc",
|
||||||
dependencies=[Depends(require_page("opc"))])
|
dependencies=[Depends(_inject_user), Depends(require_page("opc"))])
|
||||||
|
|
||||||
# WebSocket endpoint (auth handled inside handler via auth cookie; query token fallback temporary)
|
# WebSocket endpoint (auth handled inside handler via auth cookie; query token fallback temporary)
|
||||||
app.add_api_websocket_route("/ws/terminal", terminal.ws_endpoint)
|
app.add_api_websocket_route("/ws/terminal", terminal.ws_endpoint)
|
||||||
|
|||||||
@@ -115,7 +115,8 @@ def has_page_access(user: User, page: str) -> bool:
|
|||||||
|
|
||||||
def require_page(page: str):
|
def require_page(page: str):
|
||||||
"""FastAPI dependency factory — 403 if user lacks page access."""
|
"""FastAPI dependency factory — 403 if user lacks page access."""
|
||||||
async def checker(user: User = Depends(_inject_user)):
|
async def checker(request: Request):
|
||||||
|
user: User = request.state.user
|
||||||
if not has_page_access(user, page):
|
if not has_page_access(user, page):
|
||||||
raise HTTPException(status_code=403, detail="Access denied")
|
raise HTTPException(status_code=403, detail="Access denied")
|
||||||
return checker
|
return checker
|
||||||
@@ -123,7 +124,8 @@ def require_page(page: str):
|
|||||||
|
|
||||||
def require_admin():
|
def require_admin():
|
||||||
"""FastAPI dependency — 403 if user is not admin."""
|
"""FastAPI dependency — 403 if user is not admin."""
|
||||||
async def checker(user: User = Depends(_inject_user)):
|
async def checker(request: Request):
|
||||||
|
user: User = request.state.user
|
||||||
if user.role != "admin":
|
if user.role != "admin":
|
||||||
raise HTTPException(status_code=403, detail="Admin only")
|
raise HTTPException(status_code=403, detail="Admin only")
|
||||||
return checker
|
return checker
|
||||||
@@ -131,18 +133,8 @@ def require_admin():
|
|||||||
|
|
||||||
def require_write():
|
def require_write():
|
||||||
"""FastAPI dependency — 403 if viewer tries non-GET method."""
|
"""FastAPI dependency — 403 if viewer tries non-GET method."""
|
||||||
async def checker(request: Request, user: User = Depends(_inject_user)):
|
async def checker(request: Request):
|
||||||
|
user: User = request.state.user
|
||||||
if user.role == "viewer" and request.method != "GET":
|
if user.role == "viewer" and request.method != "GET":
|
||||||
raise HTTPException(status_code=403, detail="Read-only access")
|
raise HTTPException(status_code=403, detail="Read-only access")
|
||||||
return checker
|
return checker
|
||||||
|
|
||||||
|
|
||||||
async def _inject_user(request: Request):
|
|
||||||
"""Dependency to store user in request.state for rbac dependencies.
|
|
||||||
|
|
||||||
Import auth module at runtime to avoid circular imports.
|
|
||||||
"""
|
|
||||||
import auth as auth_module
|
|
||||||
user = await auth_module.get_current_user(request)
|
|
||||||
request.state.user = user
|
|
||||||
return user
|
|
||||||
|
|||||||
Reference in New Issue
Block a user