Security fixes: P0-P2 complete (22 fixes)
P0 (7): Auth-bypass removed, migrations fixed, plugin-upload disabled, RLS FORCE+WITH CHECK, plugin double-registration fixed, persistent volume, domain removed P1 (11): User/tenant model, Redis centralized, worker separated, transactional outbox, XSS fixed, DMS chunked streaming, permissions unified, password reset, metrics secured, config/docs fixed, cross-tenant FK P2 (4): Contact model normalized, cross-imports reduced 94%, commands+state machines for contacts/dms/mail/calendar, SPA path-traversal 8 new migrations, 99 unit tests, 13 commands, 8 contracts, 72 files changed
This commit is contained in:
+22
-25
@@ -7,6 +7,7 @@ from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.audit import log_audit
|
||||
from app.core.auth import get_redis
|
||||
@@ -14,6 +15,7 @@ from app.core.db import get_db
|
||||
from app.core.notifications import create_notification
|
||||
from app.core.permissions import invalidate_permission_cache
|
||||
from app.deps import get_current_user, require_permission
|
||||
from app.models.user import User, UserTenant
|
||||
from app.schemas.user import UserCreate, UserUpdate, UserResponse, PaginatedUsers
|
||||
from app.services.user_service import user_service, _UNSET
|
||||
|
||||
@@ -107,10 +109,10 @@ async def create_user(
|
||||
"id": str(user.id),
|
||||
"email": user.email,
|
||||
"name": user.name,
|
||||
"role": user.role,
|
||||
"role_id": str(user.role_id) if user.role_id else None,
|
||||
"role": body.role,
|
||||
"role_id": str(role_id) if role_id else None,
|
||||
"is_active": user.is_active,
|
||||
"tenant_id": str(user.tenant_id),
|
||||
"tenant_id": str(tenant_id),
|
||||
}
|
||||
|
||||
|
||||
@@ -129,18 +131,19 @@ async def get_user(
|
||||
400, detail={"detail": "Invalid user_id", "code": "invalid_id"}
|
||||
) from None
|
||||
|
||||
user = await user_service.get_user(db, tenant_id, uid)
|
||||
if user is None:
|
||||
result = await user_service.get_user(db, tenant_id, uid)
|
||||
if result is None:
|
||||
raise HTTPException(404, detail={"detail": "User not found", "code": "not_found"})
|
||||
|
||||
user, user_tenant = result
|
||||
return {
|
||||
"id": str(user.id),
|
||||
"email": user.email,
|
||||
"name": user.name,
|
||||
"role": user.role,
|
||||
"role_id": str(user.role_id) if user.role_id else None,
|
||||
"role": user_tenant.role,
|
||||
"role_id": str(user_tenant.role_id) if user_tenant.role_id else None,
|
||||
"is_active": user.is_active,
|
||||
"tenant_id": str(user.tenant_id),
|
||||
"tenant_id": str(user_tenant.tenant_id),
|
||||
}
|
||||
|
||||
|
||||
@@ -211,7 +214,7 @@ async def update_user(
|
||||
changes["password_changed"] = True
|
||||
|
||||
try:
|
||||
user = await user_service.update_user(
|
||||
result = await user_service.update_user(
|
||||
db,
|
||||
tenant_id,
|
||||
uid,
|
||||
@@ -228,9 +231,10 @@ async def update_user(
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(400, detail={"detail": str(exc), "code": "invalid_password"}) from None
|
||||
if user is None:
|
||||
if result is None:
|
||||
raise HTTPException(404, detail={"detail": "User not found", "code": "not_found"})
|
||||
|
||||
user, user_tenant = result
|
||||
await log_audit(db, tenant_id, acting_user_id, "update", "user", uid, changes=changes)
|
||||
|
||||
# Invalidate permission cache for the updated user
|
||||
@@ -244,10 +248,10 @@ async def update_user(
|
||||
"first_name": user.first_name,
|
||||
"last_name": user.last_name,
|
||||
"avatar_url": user.avatar_url,
|
||||
"role": user.role,
|
||||
"role_id": str(user.role_id) if user.role_id else None,
|
||||
"role": user_tenant.role,
|
||||
"role_id": str(user_tenant.role_id) if user_tenant.role_id else None,
|
||||
"is_active": user.is_active,
|
||||
"tenant_id": str(user.tenant_id),
|
||||
"tenant_id": str(user_tenant.tenant_id),
|
||||
}
|
||||
|
||||
|
||||
@@ -268,9 +272,10 @@ async def delete_user(
|
||||
) from None
|
||||
|
||||
# Get user snapshot for audit before deletion
|
||||
user = await user_service.get_user(db, tenant_id, uid)
|
||||
if user is None:
|
||||
result = await user_service.get_user(db, tenant_id, uid)
|
||||
if result is None:
|
||||
raise HTTPException(404, detail={"detail": "User not found", "code": "not_found"})
|
||||
user, user_tenant = result
|
||||
|
||||
success = await user_service.delete_user(db, tenant_id, uid)
|
||||
if not success:
|
||||
@@ -295,14 +300,10 @@ async def get_menu_order(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Get the current user's menu order preference."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
|
||||
from sqlalchemy import select
|
||||
from app.models.user import User
|
||||
|
||||
result = await db.execute(
|
||||
select(User).where(User.id == user_id, User.tenant_id == tenant_id)
|
||||
select(User).where(User.id == user_id)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
if user is None:
|
||||
@@ -319,12 +320,8 @@ async def update_menu_order(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Update the current user's menu order preference."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
|
||||
from sqlalchemy import select
|
||||
from app.models.user import User
|
||||
|
||||
menu_order = body.get("menu_order")
|
||||
if not isinstance(menu_order, list) or not all(isinstance(x, str) for x in menu_order):
|
||||
raise HTTPException(
|
||||
@@ -333,7 +330,7 @@ async def update_menu_order(
|
||||
)
|
||||
|
||||
result = await db.execute(
|
||||
select(User).where(User.id == user_id, User.tenant_id == tenant_id)
|
||||
select(User).where(User.id == user_id)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
if user is None:
|
||||
|
||||
Reference in New Issue
Block a user