Files
OpenVPN-Monitoring-Simple/APP_PROFILER/routers/profiles.py
T
iclaoudezinandClaude Sonnet 5.5 11c1b6379b Harden auth and API: username change, 2FA fixes, input validation
- Add POST /api/auth/change-username (password + OTP when 2FA is on,
  format/reserved-name checks, uniqueness) and a Change Username modal
  in Account.vue; use the real username for the 2FA provisioning URI.
- Stop creating the built-in admin/password user; the initial admin is
  seeded only from OVPMON_INITIAL_ADMIN_USER/PASSWORD.
- Reject 2FA-pending temporary tokens on all protected routes (Flask
  token_required, Profiler verify_token); only /api/auth/verify-2fa
  accepts them.
- Stop logging the OTP and TOTP secret in enable_2fa.
- Profiler: validate profile username (pattern + realpath checks in
  schema, router, pki and generator) to prevent path traversal.
- Restrict CORS to the panel origin in Profiler and Flask APIs.
- UI: header username no longer sticks to the hardcoded Admin fallback;
  it is synced from /user/me and updated after a rename.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:03:36 +00:00

146 lines
5.1 KiB
Python

import os
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from database import get_db
from utils.auth import verify_token
from models import UserProfile
from schemas import UserProfile as UserProfileSchema, UserProfileCreate
from services import pki, generator
from datetime import datetime
router = APIRouter(dependencies=[Depends(verify_token)])
@router.get("/profiles", response_model=list[UserProfileSchema])
def list_profiles(db: Session = Depends(get_db)):
# 1. Fetch profiles from DB
profiles = db.query(UserProfile).all()
# 2. Get PKI Data (Index mapping: CN -> Expiration Date)
pki_data = pki.get_pki_index_data(db)
now = datetime.utcnow()
updated_profiles = []
for profile in profiles:
# Sync expiration if available in PKI data
if profile.username in pki_data:
exp_date = pki_data[profile.username]
# Update DB if different
if profile.expiration_date != exp_date:
profile.expiration_date = exp_date
db.add(profile)
# Calculate derived fields
# 1. is_expired
is_expired = False
if profile.expiration_date:
if now > profile.expiration_date:
is_expired = True
# 2. is_revoked
# (Assuming status='revoked' in DB is the source of truth)
is_revoked = profile.status == 'revoked'
# 3. days_remaining (computed field)
days_remaining = None
if profile.expiration_date:
delta = profile.expiration_date - now
days_remaining = delta.days
# Update DB fields for persistence if they differ
if profile.is_expired != is_expired:
profile.is_expired = is_expired
db.add(profile)
if profile.is_revoked != is_revoked:
profile.is_revoked = is_revoked
db.add(profile)
# Inject computed fields for response schema
# Since 'days_remaining' is not a DB column, we attach it to the object instance
setattr(profile, 'days_remaining', days_remaining)
updated_profiles.append(profile)
db.commit() # Save any updates
return updated_profiles
@router.post("/profiles", response_model=UserProfileSchema)
def create_profile(
profile_in: UserProfileCreate,
db: Session = Depends(get_db)
):
# Check existing
existing = db.query(UserProfile).filter(UserProfile.username == profile_in.username).first()
if existing:
raise HTTPException(status_code=400, detail="User already exists")
try:
pki.validate_username(profile_in.username)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid username")
# Build PKI
try:
pki.build_client(profile_in.username, db)
except Exception as e:
raise HTTPException(status_code=500, detail=f"PKI Build failed: {str(e)}")
# Generate Config
client_conf_dir = "client-config"
os.makedirs(client_conf_dir, exist_ok=True)
file_path = os.path.join(client_conf_dir, f"{profile_in.username}.ovpn")
base_real = os.path.realpath(client_conf_dir)
if not os.path.realpath(file_path).startswith(base_real + os.sep):
raise HTTPException(status_code=400, detail="Invalid username")
try:
generator.generate_client_config(db, profile_in.username, file_path)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Config Generation failed: {str(e)}")
# Create DB Entry
new_profile = UserProfile(
username=profile_in.username,
status="active",
created_at=datetime.utcnow(),
file_path=file_path
# expired_at would be extracted from cert in a real robust implementation
)
db.add(new_profile)
db.commit()
db.refresh(new_profile)
return new_profile
@router.delete("/profiles/{profile_id}")
def revoke_profile(profile_id: int, db: Session = Depends(get_db)):
profile = db.query(UserProfile).filter(UserProfile.id == profile_id).first()
if not profile:
raise HTTPException(status_code=404, detail="Profile not found")
try:
pki.revoke_client(profile.username, db)
except Exception as e:
# Log but maybe continue to update DB status?
raise HTTPException(status_code=500, detail=f"Revocation failed: {str(e)}")
profile.status = "revoked"
profile.revoked_at = datetime.utcnow()
db.commit()
return {"message": f"Profile {profile.username} revoked"}
@router.get("/profiles/{profile_id}/download")
def download_profile(profile_id: int, db: Session = Depends(get_db)):
profile = db.query(UserProfile).filter(UserProfile.id == profile_id).first()
if not profile:
raise HTTPException(status_code=404, detail="Profile not found")
if not profile.file_path or not os.path.exists(profile.file_path):
raise HTTPException(status_code=404, detail="Config file not found")
return FileResponse(profile.file_path, filename=os.path.basename(profile.file_path))