Extract real client IP from proxy headers instead of Docker bridge IP
Nginx already forwards X-Forwarded-For and X-Real-IP, but the backend read request.client.host directly — always returning 172.18.0.x. Added get_client_ip() helper to audit service; updated all 13 call sites. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f8c2df9328
commit
21aa670a39
@ -48,7 +48,7 @@ from app.schemas.admin import (
|
||||
UserListItem,
|
||||
UserListResponse,
|
||||
)
|
||||
from app.services.audit import log_audit_event
|
||||
from app.services.audit import get_client_ip, log_audit_event
|
||||
from app.services.auth import hash_password
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -228,7 +228,7 @@ async def create_user(
|
||||
actor_id=actor.id,
|
||||
target_id=new_user.id,
|
||||
detail={"username": new_user.username, "role": new_user.role},
|
||||
ip=request.client.host if request.client else None,
|
||||
ip=get_client_ip(request),
|
||||
)
|
||||
|
||||
try:
|
||||
@ -290,7 +290,7 @@ async def update_user_role(
|
||||
actor_id=actor.id,
|
||||
target_id=user_id,
|
||||
detail={"old_role": old_role, "new_role": data.role, "sessions_revoked": revoked},
|
||||
ip=request.client.host if request.client else None,
|
||||
ip=get_client_ip(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
@ -332,7 +332,7 @@ async def reset_user_password(
|
||||
actor_id=actor.id,
|
||||
target_id=user_id,
|
||||
detail={"sessions_revoked": revoked},
|
||||
ip=request.client.host if request.client else None,
|
||||
ip=get_client_ip(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
@ -385,7 +385,7 @@ async def disable_user_mfa(
|
||||
actor_id=actor.id,
|
||||
target_id=user_id,
|
||||
detail={"sessions_revoked": revoked},
|
||||
ip=request.client.host if request.client else None,
|
||||
ip=get_client_ip(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
@ -420,7 +420,7 @@ async def toggle_mfa_enforce(
|
||||
actor_id=actor.id,
|
||||
target_id=user_id,
|
||||
detail={"enforce": data.enforce},
|
||||
ip=request.client.host if request.client else None,
|
||||
ip=get_client_ip(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
@ -462,7 +462,7 @@ async def toggle_user_active(
|
||||
actor_id=actor.id,
|
||||
target_id=user_id,
|
||||
detail={"sessions_revoked": revoked},
|
||||
ip=request.client.host if request.client else None,
|
||||
ip=get_client_ip(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
@ -494,7 +494,7 @@ async def revoke_user_sessions(
|
||||
actor_id=actor.id,
|
||||
target_id=user_id,
|
||||
detail={"sessions_revoked": revoked},
|
||||
ip=request.client.host if request.client else None,
|
||||
ip=get_client_ip(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
@ -545,7 +545,7 @@ async def delete_user(
|
||||
actor_id=actor.id,
|
||||
target_id=user_id,
|
||||
detail={"user_id": user_id, "username": deleted_username},
|
||||
ip=request.client.host if request.client else None,
|
||||
ip=get_client_ip(request),
|
||||
)
|
||||
# Flush audit + session revocation within the same transaction
|
||||
await db.flush()
|
||||
@ -652,7 +652,7 @@ async def update_system_config(
|
||||
action="admin.config_updated",
|
||||
actor_id=actor.id,
|
||||
detail=changes,
|
||||
ip=request.client.host if request.client else None,
|
||||
ip=get_client_ip(request),
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
@ -43,7 +43,7 @@ from app.services.auth import (
|
||||
create_mfa_token,
|
||||
create_mfa_enforce_token,
|
||||
)
|
||||
from app.services.audit import log_audit_event
|
||||
from app.services.audit import get_client_ip, log_audit_event
|
||||
from app.config import settings as app_settings
|
||||
|
||||
router = APIRouter()
|
||||
@ -275,7 +275,7 @@ async def setup(
|
||||
|
||||
await _create_user_defaults(db, new_user.id)
|
||||
|
||||
ip = request.client.host if request.client else "unknown"
|
||||
ip = get_client_ip(request)
|
||||
user_agent = request.headers.get("user-agent")
|
||||
_, token = await _create_db_session(db, new_user, ip, user_agent)
|
||||
_set_session_cookie(response, token)
|
||||
@ -307,7 +307,7 @@ async def login(
|
||||
HTTP 403 — account disabled (is_active=False)
|
||||
HTTP 423 — account locked
|
||||
"""
|
||||
client_ip = request.client.host if request.client else "unknown"
|
||||
client_ip = get_client_ip(request)
|
||||
|
||||
result = await db.execute(select(User).where(User.username == data.username))
|
||||
user = result.scalar_one_or_none()
|
||||
@ -438,7 +438,7 @@ async def register(
|
||||
|
||||
await _create_user_defaults(db, new_user.id)
|
||||
|
||||
ip = request.client.host if request.client else "unknown"
|
||||
ip = get_client_ip(request)
|
||||
user_agent = request.headers.get("user-agent")
|
||||
|
||||
await log_audit_event(
|
||||
|
||||
@ -35,6 +35,7 @@ from app.models.session import UserSession
|
||||
from app.models.totp_usage import TOTPUsage
|
||||
from app.models.backup_code import BackupCode
|
||||
from app.routers.auth import get_current_user, _set_session_cookie
|
||||
from app.services.audit import get_client_ip
|
||||
from app.services.auth import (
|
||||
verify_password_with_upgrade,
|
||||
hash_password,
|
||||
@ -164,7 +165,7 @@ async def _create_full_session(
|
||||
"""Create a UserSession row and return the signed cookie token."""
|
||||
session_id = uuid.uuid4().hex
|
||||
expires_at = datetime.now() + timedelta(days=app_settings.SESSION_MAX_AGE_DAYS)
|
||||
ip = request.client.host if request.client else None
|
||||
ip = get_client_ip(request)
|
||||
user_agent = request.headers.get("user-agent")
|
||||
|
||||
db_session = UserSession(
|
||||
|
||||
@ -1,8 +1,21 @@
|
||||
import json
|
||||
from fastapi import Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.models.audit_log import AuditLog
|
||||
|
||||
|
||||
def get_client_ip(request: Request) -> str:
|
||||
"""Extract the real client IP from proxy headers, falling back to direct connection."""
|
||||
forwarded_for = request.headers.get("x-forwarded-for")
|
||||
if forwarded_for:
|
||||
# X-Forwarded-For can be a comma-separated list; first entry is the original client
|
||||
return forwarded_for.split(",")[0].strip()
|
||||
real_ip = request.headers.get("x-real-ip")
|
||||
if real_ip:
|
||||
return real_ip.strip()
|
||||
return request.client.host if request.client else "unknown"
|
||||
|
||||
|
||||
async def log_audit_event(
|
||||
db: AsyncSession,
|
||||
action: str,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user