feat: implement 8 OpenSpec proposals for security, reliability, and UX improvements
## Security Enhancements (P0) - Add input validation with max_length and numeric range constraints - Implement WebSocket token authentication via first message - Add path traversal prevention in file storage service ## Permission Enhancements (P0) - Add project member management for cross-department access - Implement is_department_manager flag for workload visibility ## Cycle Detection (P0) - Add DFS-based cycle detection for task dependencies - Add formula field circular reference detection - Display user-friendly cycle path visualization ## Concurrency & Reliability (P1) - Implement optimistic locking with version field (409 Conflict on mismatch) - Add trigger retry mechanism with exponential backoff (1s, 2s, 4s) - Implement cascade restore for soft-deleted tasks ## Rate Limiting (P1) - Add tiered rate limits: standard (60/min), sensitive (20/min), heavy (5/min) - Apply rate limits to tasks, reports, attachments, and comments ## Frontend Improvements (P1) - Add responsive sidebar with hamburger menu for mobile - Improve touch-friendly UI with proper tap target sizes - Complete i18n translations for all components ## Backend Reliability (P2) - Configure database connection pool (size=10, overflow=20) - Add Redis fallback mechanism with message queue - Add blocker check before task deletion ## API Enhancements (P3) - Add standardized response wrapper utility - Add /health/ready and /health/live endpoints - Implement project templates with status/field copying ## Tests Added - test_input_validation.py - Schema and path traversal tests - test_concurrency_reliability.py - Optimistic locking and retry tests - test_backend_reliability.py - Connection pool and Redis tests - test_api_enhancements.py - Health check and template tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
286
backend/tests/test_permission_enhancements.py
Normal file
286
backend/tests/test_permission_enhancements.py
Normal file
@@ -0,0 +1,286 @@
|
||||
"""Tests for permission enhancements.
|
||||
|
||||
Tests for:
|
||||
1. Manager workload access - department managers can view subordinate workloads
|
||||
2. Cross-department project access via project membership
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from app.middleware.auth import check_project_access, check_project_edit_access
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test Helpers
|
||||
# ============================================================================
|
||||
|
||||
def get_mock_user(
|
||||
user_id="test-user-id",
|
||||
is_admin=False,
|
||||
is_department_manager=False,
|
||||
department_id="dept-1",
|
||||
):
|
||||
"""Create a mock user for testing."""
|
||||
user = MagicMock()
|
||||
user.id = user_id
|
||||
user.is_system_admin = is_admin
|
||||
user.is_department_manager = is_department_manager
|
||||
user.department_id = department_id
|
||||
return user
|
||||
|
||||
|
||||
def get_mock_project_member(user_id, role="member"):
|
||||
"""Create a mock project member."""
|
||||
member = MagicMock()
|
||||
member.user_id = user_id
|
||||
member.role = role
|
||||
return member
|
||||
|
||||
|
||||
def get_mock_project(
|
||||
owner_id="owner-id",
|
||||
security_level="department",
|
||||
department_id="dept-1",
|
||||
members=None,
|
||||
):
|
||||
"""Create a mock project for testing."""
|
||||
project = MagicMock()
|
||||
project.id = "project-id"
|
||||
project.owner_id = owner_id
|
||||
project.security_level = security_level
|
||||
project.department_id = department_id
|
||||
project.members = members or []
|
||||
return project
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test Manager Workload Access
|
||||
# ============================================================================
|
||||
|
||||
class TestManagerWorkloadAccess:
|
||||
"""Test that department managers can view subordinate workloads."""
|
||||
|
||||
def test_manager_flag_exists_on_user(self):
|
||||
"""Test that is_department_manager flag exists on mock user."""
|
||||
manager = get_mock_user(is_department_manager=True)
|
||||
assert manager.is_department_manager == True
|
||||
|
||||
regular_user = get_mock_user(is_department_manager=False)
|
||||
assert regular_user.is_department_manager == False
|
||||
|
||||
def test_system_admin_can_view_all_workloads(self):
|
||||
"""Test that system admin can view any user's workload."""
|
||||
from app.api.workload.router import check_workload_access
|
||||
|
||||
admin = get_mock_user(is_admin=True)
|
||||
|
||||
# Should not raise for any target user
|
||||
check_workload_access(admin, target_user_id="any-user-id")
|
||||
check_workload_access(admin, department_id="any-dept")
|
||||
|
||||
def test_manager_can_view_same_department_workload(self):
|
||||
"""Test that manager can view workload of users in their department."""
|
||||
from app.api.workload.router import check_workload_access
|
||||
|
||||
manager = get_mock_user(
|
||||
is_department_manager=True,
|
||||
department_id="dept-1"
|
||||
)
|
||||
|
||||
# Manager can view workload of user in same department
|
||||
check_workload_access(
|
||||
manager,
|
||||
target_user_id="subordinate-user-id",
|
||||
target_user_department_id="dept-1"
|
||||
)
|
||||
|
||||
def test_manager_cannot_view_other_department_workload(self):
|
||||
"""Test that manager cannot view workload of users in other departments."""
|
||||
from app.api.workload.router import check_workload_access
|
||||
from fastapi import HTTPException
|
||||
|
||||
manager = get_mock_user(
|
||||
is_department_manager=True,
|
||||
department_id="dept-1"
|
||||
)
|
||||
|
||||
# Manager cannot view workload of user in different department
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
check_workload_access(
|
||||
manager,
|
||||
target_user_id="other-dept-user-id",
|
||||
target_user_department_id="dept-2"
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
|
||||
def test_regular_user_can_only_view_own_workload(self):
|
||||
"""Test that regular users can only view their own workload."""
|
||||
from app.api.workload.router import check_workload_access
|
||||
from fastapi import HTTPException
|
||||
|
||||
user = get_mock_user(
|
||||
user_id="user-123",
|
||||
is_department_manager=False
|
||||
)
|
||||
|
||||
# User can view their own workload
|
||||
check_workload_access(user, target_user_id="user-123")
|
||||
|
||||
# User cannot view others' workload
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
check_workload_access(user, target_user_id="other-user")
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test Cross-Department Project Access via Membership
|
||||
# ============================================================================
|
||||
|
||||
class TestProjectMemberAccess:
|
||||
"""Test that project members have access regardless of department."""
|
||||
|
||||
def test_project_member_has_access(self):
|
||||
"""Test that project member can access project from different department."""
|
||||
user = get_mock_user(user_id="member-user", department_id="dept-2")
|
||||
|
||||
# Project is in dept-1 but user from dept-2 is a member
|
||||
member = get_mock_project_member(user_id="member-user", role="member")
|
||||
project = get_mock_project(
|
||||
security_level="department",
|
||||
department_id="dept-1",
|
||||
members=[member],
|
||||
)
|
||||
|
||||
assert check_project_access(user, project) == True
|
||||
|
||||
def test_non_member_from_different_dept_denied(self):
|
||||
"""Test that non-member from different department is denied access."""
|
||||
user = get_mock_user(user_id="outsider", department_id="dept-2")
|
||||
|
||||
project = get_mock_project(
|
||||
security_level="department",
|
||||
department_id="dept-1",
|
||||
members=[], # No members
|
||||
)
|
||||
|
||||
assert check_project_access(user, project) == False
|
||||
|
||||
def test_member_access_confidential_project(self):
|
||||
"""Test that members can access confidential projects."""
|
||||
user = get_mock_user(user_id="member-user", department_id="dept-2")
|
||||
|
||||
member = get_mock_project_member(user_id="member-user", role="member")
|
||||
project = get_mock_project(
|
||||
owner_id="owner-id", # User is not owner
|
||||
security_level="confidential",
|
||||
department_id="dept-1",
|
||||
members=[member],
|
||||
)
|
||||
|
||||
# Member should have access even to confidential project
|
||||
assert check_project_access(user, project) == True
|
||||
|
||||
def test_member_with_admin_role_can_edit(self):
|
||||
"""Test that project member with admin role can edit project."""
|
||||
user = get_mock_user(user_id="admin-member", department_id="dept-2")
|
||||
|
||||
member = get_mock_project_member(user_id="admin-member", role="admin")
|
||||
project = get_mock_project(
|
||||
owner_id="owner-id", # User is not owner
|
||||
security_level="department",
|
||||
members=[member],
|
||||
)
|
||||
|
||||
assert check_project_edit_access(user, project) == True
|
||||
|
||||
def test_member_with_member_role_cannot_edit(self):
|
||||
"""Test that project member with member role cannot edit project."""
|
||||
user = get_mock_user(user_id="regular-member", department_id="dept-2")
|
||||
|
||||
member = get_mock_project_member(user_id="regular-member", role="member")
|
||||
project = get_mock_project(
|
||||
owner_id="owner-id", # User is not owner
|
||||
security_level="department",
|
||||
members=[member],
|
||||
)
|
||||
|
||||
assert check_project_edit_access(user, project) == False
|
||||
|
||||
def test_owner_can_still_edit(self):
|
||||
"""Test that project owner can edit regardless of members."""
|
||||
user = get_mock_user(user_id="owner-id")
|
||||
|
||||
project = get_mock_project(
|
||||
owner_id="owner-id",
|
||||
security_level="confidential",
|
||||
members=[],
|
||||
)
|
||||
|
||||
assert check_project_access(user, project) == True
|
||||
assert check_project_edit_access(user, project) == True
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test Filter Accessible Users for Manager
|
||||
# ============================================================================
|
||||
|
||||
class TestFilterAccessibleUsersForManager:
|
||||
"""Test the filter_accessible_users function for managers."""
|
||||
|
||||
def test_admin_can_see_all_users(self):
|
||||
"""Test that admin can see all users."""
|
||||
from app.api.workload.router import filter_accessible_users
|
||||
|
||||
admin = get_mock_user(is_admin=True)
|
||||
|
||||
# Admin with no filter gets None (means all users)
|
||||
result = filter_accessible_users(admin, None, None)
|
||||
assert result is None
|
||||
|
||||
# Admin with specific users gets those users
|
||||
result = filter_accessible_users(admin, ["user1", "user2"], None)
|
||||
assert result == ["user1", "user2"]
|
||||
|
||||
def test_regular_user_sees_only_self(self):
|
||||
"""Test that regular user can only see themselves."""
|
||||
from app.api.workload.router import filter_accessible_users
|
||||
|
||||
user = get_mock_user(user_id="user-123", is_department_manager=False)
|
||||
|
||||
# Regular user with no filter gets only self
|
||||
result = filter_accessible_users(user, None, None)
|
||||
assert result == ["user-123"]
|
||||
|
||||
# Regular user with other users gets only self
|
||||
result = filter_accessible_users(user, ["user1", "user2", "user-123"], None)
|
||||
assert result == ["user-123"]
|
||||
|
||||
|
||||
class TestAccessDeniedForNonManagersAndNonMembers:
|
||||
"""Test that access is properly denied for unauthorized users."""
|
||||
|
||||
def test_non_manager_cannot_view_subordinate_workload(self):
|
||||
"""Test that non-manager cannot view other users' workload."""
|
||||
from app.api.workload.router import check_workload_access
|
||||
from fastapi import HTTPException
|
||||
|
||||
user = get_mock_user(is_department_manager=False)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
check_workload_access(user, target_user_id="other-user")
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
|
||||
def test_non_member_cannot_access_department_project(self):
|
||||
"""Test that non-member from different department cannot access."""
|
||||
user = get_mock_user(department_id="dept-2")
|
||||
|
||||
project = get_mock_project(
|
||||
security_level="department",
|
||||
department_id="dept-1",
|
||||
members=[],
|
||||
)
|
||||
|
||||
assert check_project_access(user, project) == False
|
||||
Reference in New Issue
Block a user