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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user