Security hardening release addressing CodeQL and Dependabot alerts: - Fix stack trace exposure in error responses - Add SSRF protection with DNS resolution checking - Implement proper URL hostname validation (replaces substring matching) - Add centralized path sanitization to prevent path traversal - Fix ReDoS vulnerability in email validation regex - Improve HTML sanitization in validation utilities - Fix capability wildcard matching in auth utilities - Update glob dependency to address CVE - Add CodeQL suppression comments for verified false positives 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
84 lines
2.5 KiB
Python
84 lines
2.5 KiB
Python
"""
|
|
Public API endpoints (no authentication required)
|
|
|
|
Handles public-facing endpoints like tenant info for branding.
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
import structlog
|
|
|
|
from app.core.database import get_db
|
|
from app.models.tenant import Tenant
|
|
|
|
logger = structlog.get_logger()
|
|
router = APIRouter(tags=["public"])
|
|
|
|
|
|
# Pydantic models
|
|
class TenantInfoResponse(BaseModel):
|
|
name: str
|
|
domain: str
|
|
|
|
|
|
# API endpoints
|
|
@router.get("/tenant-info", response_model=TenantInfoResponse)
|
|
async def get_tenant_info(
|
|
tenant_domain: str,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Get public tenant information for branding (no authentication required)
|
|
|
|
Used by tenant login page to display tenant name.
|
|
Fails fast if tenant name is not configured (no fallbacks).
|
|
|
|
Args:
|
|
tenant_domain: Tenant domain identifier (e.g., "test-company")
|
|
|
|
Returns:
|
|
Tenant name and domain
|
|
|
|
Raises:
|
|
HTTP 404: Tenant not found
|
|
HTTP 500: Tenant name not configured
|
|
"""
|
|
try:
|
|
# Query tenant by domain
|
|
stmt = select(Tenant).where(Tenant.domain == tenant_domain)
|
|
result = await db.execute(stmt)
|
|
tenant = result.scalar_one_or_none()
|
|
|
|
# Check if tenant exists
|
|
if not tenant:
|
|
logger.warning("Tenant not found", domain=tenant_domain)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Tenant not found: {tenant_domain}"
|
|
)
|
|
|
|
# Validate tenant name exists (fail fast - no fallback)
|
|
if not tenant.name or not tenant.name.strip():
|
|
logger.error("Tenant name not configured", tenant_id=tenant.id, domain=tenant_domain)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Tenant configuration error: tenant name not set"
|
|
)
|
|
|
|
logger.info("Tenant info retrieved", domain=tenant_domain, name=tenant.name)
|
|
|
|
return TenantInfoResponse(
|
|
name=tenant.name,
|
|
domain=tenant.domain
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Error retrieving tenant info", domain=tenant_domain, error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to retrieve tenant information"
|
|
)
|