Fix security vulnerabilities. Add logging
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user