Files
saksham 1a4c80958f Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring,
source collection, LLM-based change analysis, enrichment, and account
security (Turnstile, escalating lockout, email verification).
2026-08-05 10:48:20 -04:00

104 lines
3.9 KiB
Python

"""Public unban-request intake + admin resolution. One request per IP per
24h (enforced here), notifying every admin account's own email (Resend if
configured, else SMTP - same provider selection as the rest of security
email, see security_email_service.resolve_provider) and via the existing
admin-only Redis log feed (app/core/logging.py), so it surfaces in the
Settings page's Logging box too.
"""
from __future__ import annotations
import uuid
from datetime import UTC, datetime
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import Settings
from app.core.errors import ConflictError, NotFoundError, RateLimitedError
from app.core.logging import get_logger
from app.models.ip_ban import IpBan
from app.notifications.base import NotificationMessage
from app.repositories.ip_throttle_repository import IpThrottleRepository
from app.repositories.unban_request_repository import UnbanRequestRepository
from app.repositories.user_repository import UserRepository
from app.services.security_email_service import resolve_provider
logger = get_logger(__name__)
UNBAN_REQUEST_COOLDOWN_HOURS = 24
async def submit_unban_request(
db: AsyncSession, settings: Settings, ip_address: str, message: str | None
) -> None:
repo = UnbanRequestRepository(db)
if await repo.within_cooldown(ip_address, UNBAN_REQUEST_COOLDOWN_HOURS):
raise RateLimitedError(
f"Only one unban request is allowed per {UNBAN_REQUEST_COOLDOWN_HOURS} hours."
)
await repo.create(ip_address, message)
await db.commit()
logger.warning("unban_request_received", ip=ip_address, message=message or "")
admin_emails = await UserRepository(db).list_admin_emails()
if not admin_emails:
logger.warning("unban_request_no_admin_to_notify", ip=ip_address)
return
provider = resolve_provider(settings)
for admin_email in admin_emails:
email_message = NotificationMessage(
destination_value=admin_email,
subject=f"Unban request from {ip_address}",
body_text=f"IP: {ip_address}\n\nMessage:\n{message or '(none)'}",
)
await provider.send(email_message)
async def list_ip_bans(db: AsyncSession) -> list[IpBan]:
return await IpThrottleRepository(db).list_bans()
async def unban_ip(db: AsyncSession, ip_address: str) -> bool:
repo = IpThrottleRepository(db)
cleared = await repo.clear_ban_and_state(ip_address)
await db.commit()
return cleared
async def ban_ip(db: AsyncSession, ip_address: str, reason: str = "manual_admin_ban") -> IpBan:
"""Admin-initiated ban, bypassing the usual offense-count escalation
ladder (ip_throttle_service) entirely - a deliberate manual override,
not something the automated abuse-detection path produces."""
repo = IpThrottleRepository(db)
if await repo.get_ban(ip_address) is not None:
raise ConflictError(f"{ip_address} is already banned.")
ban = await repo.create_ban(ip_address, reason, datetime.now(UTC))
await db.commit()
return ban
async def accept_unban_request(db: AsyncSession, request_id: uuid.UUID) -> None:
"""Unbans the requester's IP and clears the request from the pending
queue - a real pardon (see clear_ban_and_state), not just acknowledging
the request was read."""
repo = UnbanRequestRepository(db)
request = await repo.get(request_id)
if request is None:
raise NotFoundError("Unban request not found.")
await IpThrottleRepository(db).clear_ban_and_state(request.ip_address)
await repo.delete(request_id)
await db.commit()
async def reject_unban_request(db: AsyncSession, request_id: uuid.UUID) -> None:
"""Dismisses the request without touching the ban - the IP stays
banned."""
repo = UnbanRequestRepository(db)
if await repo.get(request_id) is None:
raise NotFoundError("Unban request not found.")
await repo.delete(request_id)
await db.commit()