feat: implement 5 QA-driven security and quality proposals

Implemented proposals from comprehensive QA review:

1. extend-csrf-protection
   - Add POST to CSRF protected methods in frontend
   - Global CSRF middleware for all state-changing operations
   - Update tests with CSRF token fixtures

2. tighten-cors-websocket-security
   - Replace wildcard CORS with explicit method/header lists
   - Disable query parameter auth in production (code 4002)
   - Add per-user WebSocket connection limit (max 5, code 4005)

3. shorten-jwt-expiry
   - Reduce JWT expiry from 7 days to 60 minutes
   - Add refresh token support with 7-day expiry
   - Implement token rotation on refresh
   - Frontend auto-refresh when token near expiry (<5 min)

4. fix-frontend-quality
   - Add React.lazy() code splitting for all pages
   - Fix useCallback dependency arrays (Dashboard, Comments)
   - Add localStorage data validation in AuthContext
   - Complete i18n for AttachmentUpload component

5. enhance-backend-validation
   - Add SecurityAuditMiddleware for access denied logging
   - Add ErrorSanitizerMiddleware for production error messages
   - Protect /health/detailed with admin authentication
   - Add input length validation (comment 5000, desc 10000)

All 521 backend tests passing. Frontend builds successfully.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2026-01-12 23:19:05 +08:00
parent df50d5e7f8
commit 35c90fe76b
48 changed files with 2132 additions and 403 deletions

View File

@@ -1,6 +1,7 @@
import pytest
from app.models.user import User
from app.models.department import Department
from app.core.security import generate_csrf_token
class TestUserEndpoints:
@@ -35,7 +36,7 @@ class TestUserEndpoints:
)
assert response.status_code == 404
def test_update_user(self, client, admin_token, db):
def test_update_user(self, client, auth_headers, db):
"""Test updating a user."""
# Create a test user
test_user = User(
@@ -49,7 +50,7 @@ class TestUserEndpoints:
response = client.patch(
"/api/users/test-user-001",
headers={"Authorization": f"Bearer {admin_token}"},
headers=auth_headers,
json={"name": "Updated Name"},
)
assert response.status_code == 200
@@ -84,9 +85,10 @@ class TestUserEndpoints:
mock_redis.setex("session:non-admin-001", 900, token)
# Try to modify system admin - should fail with 403
csrf_token = generate_csrf_token("non-admin-001")
response = client.patch(
"/api/users/00000000-0000-0000-0000-000000000001",
headers={"Authorization": f"Bearer {token}"},
headers={"Authorization": f"Bearer {token}", "X-CSRF-Token": csrf_token},
json={"name": "Hacked Name"},
)
# Engineer role doesn't have users.write permission
@@ -123,16 +125,17 @@ class TestCapacityUpdate:
mock_redis.setex("session:capacity-user-001", 900, token)
# Update own capacity
csrf_token = generate_csrf_token("capacity-user-001")
response = client.put(
"/api/users/capacity-user-001/capacity",
headers={"Authorization": f"Bearer {token}"},
headers={"Authorization": f"Bearer {token}", "X-CSRF-Token": csrf_token},
json={"capacity_hours": 35.5},
)
assert response.status_code == 200
data = response.json()
assert float(data["capacity"]) == 35.5
def test_admin_can_update_other_user_capacity(self, client, admin_token, db):
def test_admin_can_update_other_user_capacity(self, client, auth_headers, db):
"""Test that admin can update another user's capacity."""
# Create a test user
test_user = User(
@@ -148,7 +151,7 @@ class TestCapacityUpdate:
# Admin updates another user's capacity
response = client.put(
"/api/users/capacity-user-002/capacity",
headers={"Authorization": f"Bearer {admin_token}"},
headers=auth_headers,
json={"capacity_hours": 20.0},
)
assert response.status_code == 200
@@ -189,15 +192,16 @@ class TestCapacityUpdate:
mock_redis.setex("session:capacity-user-003", 900, token)
# User1 tries to update user2's capacity - should fail
csrf_token = generate_csrf_token("capacity-user-003")
response = client.put(
"/api/users/capacity-user-004/capacity",
headers={"Authorization": f"Bearer {token}"},
headers={"Authorization": f"Bearer {token}", "X-CSRF-Token": csrf_token},
json={"capacity_hours": 30.0},
)
assert response.status_code == 403
assert "Only admin, manager, or the user themselves" in response.json()["detail"]
def test_update_capacity_invalid_value_negative(self, client, admin_token, db):
def test_update_capacity_invalid_value_negative(self, client, auth_headers, db):
"""Test that negative capacity hours are rejected."""
# Create a test user
test_user = User(
@@ -212,7 +216,7 @@ class TestCapacityUpdate:
response = client.put(
"/api/users/capacity-user-005/capacity",
headers={"Authorization": f"Bearer {admin_token}"},
headers=auth_headers,
json={"capacity_hours": -5.0},
)
# Pydantic validation returns 422 Unprocessable Entity
@@ -221,7 +225,7 @@ class TestCapacityUpdate:
# Check validation error message in Pydantic format
assert any("non-negative" in str(err).lower() for err in error_detail)
def test_update_capacity_invalid_value_too_high(self, client, admin_token, db):
def test_update_capacity_invalid_value_too_high(self, client, auth_headers, db):
"""Test that capacity hours exceeding 168 are rejected."""
# Create a test user
test_user = User(
@@ -236,7 +240,7 @@ class TestCapacityUpdate:
response = client.put(
"/api/users/capacity-user-006/capacity",
headers={"Authorization": f"Bearer {admin_token}"},
headers=auth_headers,
json={"capacity_hours": 200.0},
)
# Pydantic validation returns 422 Unprocessable Entity
@@ -245,11 +249,11 @@ class TestCapacityUpdate:
# Check validation error message in Pydantic format
assert any("168" in str(err) for err in error_detail)
def test_update_capacity_nonexistent_user(self, client, admin_token):
def test_update_capacity_nonexistent_user(self, client, auth_headers):
"""Test updating capacity for a nonexistent user."""
response = client.put(
"/api/users/nonexistent-user-id/capacity",
headers={"Authorization": f"Bearer {admin_token}"},
headers=auth_headers,
json={"capacity_hours": 40.0},
)
assert response.status_code == 404
@@ -303,16 +307,17 @@ class TestCapacityUpdate:
mock_redis.setex("session:manager-cap-001", 900, token)
# Manager updates regular user's capacity
csrf_token = generate_csrf_token("manager-cap-001")
response = client.put(
"/api/users/regular-cap-001/capacity",
headers={"Authorization": f"Bearer {token}"},
headers={"Authorization": f"Bearer {token}", "X-CSRF-Token": csrf_token},
json={"capacity_hours": 30.0},
)
assert response.status_code == 200
data = response.json()
assert float(data["capacity"]) == 30.0
def test_capacity_change_creates_audit_log(self, client, admin_token, db):
def test_capacity_change_creates_audit_log(self, client, auth_headers, db):
"""Test that capacity changes are recorded in audit trail."""
from app.models import AuditLog
@@ -330,7 +335,7 @@ class TestCapacityUpdate:
# Update capacity
response = client.put(
"/api/users/capacity-audit-001/capacity",
headers={"Authorization": f"Bearer {admin_token}"},
headers=auth_headers,
json={"capacity_hours": 35.0},
)
assert response.status_code == 200