Fix security vulnerabilities. Add logging

This commit is contained in:
Ettore
2026-05-09 17:52:59 +02:00
parent d803e2d7f6
commit 69e4f594de
14 changed files with 226 additions and 72 deletions

View File

@@ -29,6 +29,8 @@ async def create_admin(
raise HTTPException(422, "Username cannot be empty")
if req.role not in ("admin", "manager"):
raise HTTPException(422, "role must be 'admin' or 'manager'")
if len(req.password) < 12:
raise HTTPException(422, "Password must be at least 12 characters")
if db.query(AdminUser).filter_by(username=username).first():
raise HTTPException(409, "Username already exists")
user = AdminUser(username=username, password_hash=hash_password(req.password), role=req.role)
@@ -64,6 +66,8 @@ async def change_password(
):
if not req.new_password:
raise HTTPException(422, "Password cannot be empty")
if len(req.new_password) < 12:
raise HTTPException(422, "Password must be at least 12 characters")
user: Optional[AdminUser] = db.query(AdminUser).filter_by(username=username).first()
if not user:
raise HTTPException(404, "Admin not found")

View File

@@ -1,4 +1,5 @@
from datetime import datetime, timedelta
import logging
from datetime import datetime, timedelta, timezone
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status
@@ -9,18 +10,21 @@ from core.database import AdminUser, Keypass, get_db
from core.schemas import AdminLoginRequest, KeypassLoginRequest, TokenResponse
router = APIRouter(prefix="/api/auth", tags=["auth"])
logger = logging.getLogger(__name__)
@router.post("/admin", response_model=TokenResponse)
async def admin_login(req: AdminLoginRequest, db: Session = Depends(get_db)):
user: Optional[AdminUser] = db.query(AdminUser).filter_by(username=req.username).first()
if not user or not verify_password(req.password, user.password_hash):
logger.warning("Failed admin login attempt for username=%r", req.username)
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid credentials")
logger.info("Admin login: username=%r role=%r", user.username, user.role)
token = create_token({
"sub": user.username,
"role": "admin",
"scope": user.role, # 'admin' | 'manager'
"exp": datetime.utcnow() + timedelta(hours=24),
"exp": datetime.now(timezone.utc) + timedelta(hours=24),
})
return TokenResponse(token=token)
@@ -34,9 +38,9 @@ async def keypass_login(req: KeypassLoginRequest, db: Session = Depends(get_db))
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid keypass")
if kp.revoked:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Keypass has been revoked")
if kp.expires_at is not None and kp.expires_at < datetime.utcnow():
if kp.expires_at is not None and kp.expires_at < datetime.now(timezone.utc):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Keypass has expired")
exp = kp.expires_at if kp.expires_at else datetime(2099, 12, 31, 23, 59, 59)
exp = kp.expires_at if kp.expires_at else datetime(2099, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
token = create_token({
"sub": str(kp.id),
"role": "keypass",

View File

@@ -1,5 +1,6 @@
import json
from datetime import datetime
import logging
from datetime import datetime, timezone
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request
@@ -12,6 +13,7 @@ from core.schemas import GateCreate, GatePublicResponse, GateResponse
from services.gates import call_open_gate
router = APIRouter(tags=["gates"])
logger = logging.getLogger(__name__)
# ── Admin: gate CRUD ──────────────────────────────────────────────────────────
@@ -85,7 +87,7 @@ async def admin_open_gate(
if not cred_db:
raise HTTPException(503, "AVConnect credentials not configured")
ip = request.headers.get("X-Forwarded-For", request.client.host if request.client else None)
ip = request.client.host if request.client else None
ua = request.headers.get("User-Agent")
mock = bool(cred_db.mock_avconnect)
@@ -98,7 +100,7 @@ async def admin_open_gate(
)
db.add(GateAccessLog(
timestamp=datetime.utcnow(),
timestamp=datetime.now(timezone.utc),
keypass_id=0,
keypass_code=f"[{caller['sub']}]",
gate_id=gate_db.id,
@@ -114,8 +116,10 @@ async def admin_open_gate(
db.commit()
if not success:
logger.error("Gate open failed: gate_id=%d caller=%r error=%r", gate_db.id, caller["sub"], error_msg)
raise HTTPException(502, error_msg or "Gate operation failed")
logger.info("Gate opened by admin: gate_id=%d gate=%r caller=%r ip=%r", gate_db.id, gate_db.name, caller["sub"], ip)
return {"success": True, "gate": gate_db.name}
@@ -149,7 +153,7 @@ async def open_gate(
if not cred_db:
raise HTTPException(503, "AVConnect credentials not configured")
ip = request.headers.get("X-Forwarded-For", request.client.host if request.client else None)
ip = request.client.host if request.client else None
ua = request.headers.get("User-Agent")
allowed = json.loads(_kp.allowed_gates) if _kp.allowed_gates else None
@@ -166,7 +170,7 @@ async def open_gate(
)
db.add(GateAccessLog(
timestamp=datetime.utcnow(),
timestamp=datetime.now(timezone.utc),
keypass_id=_kp.id,
keypass_code=_kp.code,
gate_id=gate_db.id,
@@ -177,11 +181,9 @@ async def open_gate(
error=error_msg,
))
if new_sid and new_sid != cred_db.session_id:
cred_db.session_id = new_sid
db.commit()
if not success:
logger.error("Gate open failed: gate_id=%d keypass_id=%d error=%r", gate_db.id, _kp.id, error_msg)
raise HTTPException(502, error_msg or "Gate operation failed")
logger.info("Gate opened by keypass: gate_id=%d gate=%r keypass_id=%d ip=%r", gate_db.id, gate_db.name, _kp.id, ip)
return {"success": True, "gate": gate_db.name}

View File

@@ -1,7 +1,7 @@
import json
import secrets
import string
from datetime import datetime
from datetime import datetime, timezone
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
@@ -38,7 +38,7 @@ async def create_keypass(
kp = Keypass(
code=code,
description=req.description,
created_at=datetime.utcnow(),
created_at=datetime.now(timezone.utc),
expires_at=req.expires_at,
revoked=False,
allowed_gates=json.dumps(req.gate_ids) if req.gate_ids else None,
@@ -80,10 +80,10 @@ async def revoke_keypass(
kp: Optional[Keypass] = db.query(Keypass).filter(Keypass.id == kp_id).first()
if not kp:
raise HTTPException(404, "Keypass not found")
if kp.expires_at is not None and kp.expires_at < datetime.utcnow():
if kp.expires_at is not None and kp.expires_at < datetime.now(timezone.utc):
raise HTTPException(409, "Expired keypasses cannot be revoked")
if kp.revoked:
raise HTTPException(409, "Keypass is already revoked")
kp.revoked = True
kp.revoked_at = datetime.utcnow()
kp.revoked_at = datetime.now(timezone.utc)
db.commit()