Harden auth and persist report schedules

This commit is contained in:
dwindown
2026-06-06 19:40:32 +07:00
parent aaf64264f7
commit fd7989f673
18 changed files with 748 additions and 105 deletions

View File

@@ -15,7 +15,13 @@ from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.core.auth import issue_access_token
from app.core.auth import (
AuthContext,
ensure_website_scope_matches,
get_auth_context,
issue_access_token,
require_website_auth,
)
from app.models.user import User
from app.models.website import Website
from app.schemas.wordpress import (
@@ -44,6 +50,16 @@ logger = logging.getLogger(__name__)
router = APIRouter(prefix="/wordpress", tags=["wordpress"])
def _api_role_from_wordpress_roles(roles: list[str]) -> str:
"""Map WordPress roles to API roles used by route authorization."""
normalized_roles = {str(role).strip().lower() for role in roles}
if normalized_roles & {"super_admin", "system_admin"}:
return "system_admin"
if normalized_roles & {"administrator", "admin"}:
return "admin"
return "student"
def get_website_id_from_header(
x_website_id: Optional[str] = Header(None, alias="X-Website-ID"),
) -> int:
@@ -132,7 +148,7 @@ async def sync_users_endpoint(
Raises:
HTTPException: If website not found, token invalid, or API error
"""
enforce_rate_limit(
await enforce_rate_limit(
request,
scope="wordpress.sync_users",
max_requests=20,
@@ -230,7 +246,7 @@ async def verify_session_endpoint(
Raises:
HTTPException: If website not found or API error
"""
enforce_rate_limit(
await enforce_rate_limit(
http_request,
scope="wordpress.verify_session",
max_requests=60,
@@ -273,7 +289,7 @@ async def verify_session_endpoint(
},
access_token=issue_access_token(
website_id=request.website_id,
role="student",
role=_api_role_from_wordpress_roles(wp_user_info.roles),
wp_user_id=request.wp_user_id,
expires_in_seconds=3600 * 24,
),
@@ -310,6 +326,7 @@ async def verify_session_endpoint(
async def get_website_users(
website_id: int,
db: AsyncSession = Depends(get_db),
auth: AuthContext = Depends(get_auth_context),
page: int = 1,
page_size: int = 50,
) -> UserListResponse:
@@ -328,6 +345,9 @@ async def get_website_users(
Raises:
HTTPException: If website not found
"""
auth_website_id = require_website_auth(auth, allowed_roles={"admin", "system_admin"})
ensure_website_scope_matches(auth_website_id, website_id)
# Validate website exists
await get_valid_website(website_id, db)
@@ -374,6 +394,7 @@ async def get_user_endpoint(
website_id: int,
wp_user_id: str,
db: AsyncSession = Depends(get_db),
auth: AuthContext = Depends(get_auth_context),
) -> WordPressUserResponse:
"""
Get a specific user by WordPress user ID.
@@ -389,6 +410,16 @@ async def get_user_endpoint(
Raises:
HTTPException: If website or user not found
"""
auth_website_id = require_website_auth(
auth, allowed_roles={"student", "admin", "system_admin"}
)
ensure_website_scope_matches(auth_website_id, website_id)
if auth.role == "student" and auth.wp_user_id != wp_user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User does not belong to this authenticated user",
)
# Validate website exists
await get_valid_website(website_id, db)