llmproxy/backend/admin.py
Oliver Hofmann bf694b79e2 Fix critical/high security and correctness issues from code review
Critical (all fixed):
- bcrypt statt SHA-256 für Passwörter
- API-Keys gehasht in DB, Plaintext nur einmalig zurückgegeben
- DB-Session-Leak behoben (SessionLocal + try/finally, Depends(get_db))
- Admin-Check via is_admin-Spalte statt Hardcoded-Username
- CORS: konfigurierbare Origins via ALLOWED_ORIGINS, kein Wildcard mit Credentials

High (all fixed):
- TOCTOU-Race: check_and_increment_quota mit SELECT FOR UPDATE atomar
- Getrennte Tages-/Monatszähler in Usage + automatische Reset-Logik
- Token-Zählung mit tiktoken (cl100k_base) statt .split()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 21:34:17 +02:00

116 lines
4.0 KiB
Python

import os
from fastapi import FastAPI, Depends, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from database import get_db
import crud, schemas
from models import User, APIKey, Quota
app = FastAPI(title="Ollama Proxy Admin API")
ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://localhost:5173").split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
async def require_admin_auth(request: Request, db: Session = Depends(get_db)):
auth_header = request.headers.get("Authorization", "")
if auth_header.startswith("Bearer "):
api_key = auth_header.replace("Bearer ", "")
elif auth_header.startswith("sk-"):
api_key = auth_header
else:
raise HTTPException(status_code=401, detail="Invalid or missing API key")
db_key = crud.verify_api_key(db, api_key)
if not db_key:
raise HTTPException(status_code=401, detail="Invalid API key")
db_user = db.query(User).filter(User.id == db_key.user_id).first()
if not db_user or not db_user.is_admin:
raise HTTPException(status_code=403, detail="Admin access required")
request.state.user = db_user
@app.get("/api/users", response_model=list[schemas.User])
async def read_users(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
_ = Depends(require_admin_auth)
):
users = db.query(User).offset(skip).limit(limit).all()
return users
@app.post("/api/users", response_model=schemas.User)
async def create_user(
user: schemas.UserCreate,
db: Session = Depends(get_db),
_ = Depends(require_admin_auth)
):
db_user = crud.get_user_by_username(db, username=user.username)
if db_user:
raise HTTPException(status_code=400, detail="Username already registered")
db_user = crud.get_user_by_email(db, email=user.email)
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
return crud.create_user(db=db, username=user.username, email=user.email, password=user.password)
@app.post("/api/api-keys", response_model=schemas.APIKeyCreated)
async def create_api_key(
api_key: schemas.APIKeyCreate,
db: Session = Depends(get_db),
_ = Depends(require_admin_auth)
):
db_user = db.query(User).filter(User.id == api_key.user_id).first()
if not db_user:
raise HTTPException(status_code=404, detail="User not found")
db_key, raw_key = crud.create_api_key(db=db, user_id=api_key.user_id, name=api_key.name)
result = schemas.APIKeyCreated.model_validate(db_key)
result.plaintext_key = raw_key
return result
@app.get("/api/api-keys", response_model=list[schemas.APIKey])
async def read_api_keys(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
_ = Depends(require_admin_auth)
):
api_keys = db.query(APIKey).offset(skip).limit(limit).all()
return api_keys
@app.put("/api/api-keys/{api_key_id}/deactivate")
async def deactivate_api_key(
api_key_id: int,
db: Session = Depends(get_db),
_ = Depends(require_admin_auth)
):
db_key = db.query(APIKey).filter(APIKey.id == api_key_id).first()
if not db_key:
raise HTTPException(status_code=404, detail="API key not found")
db_key.is_active = False
db.commit()
return {"message": "API key deactivated"}
@app.put("/api/quotas/{user_id}", response_model=schemas.Quota)
async def update_quota(
user_id: int,
quota: schemas.QuotaCreate,
db: Session = Depends(get_db),
_ = Depends(require_admin_auth)
):
db_quota = db.query(Quota).filter(Quota.user_id == user_id).first()
if not db_quota:
raise HTTPException(status_code=404, detail="Quota not found")
for key, value in quota.model_dump(exclude_unset=True).items():
setattr(db_quota, key, value)
db.commit()
db.refresh(db_quota)
return db_quota