- **Configurations:** Test minimum `SECRET_KEY` length validation to prevent weak JWT signing keys. Validate proper handling of secure defaults. - **Permissions:** Add tests for inactive user blocking, API access control, and superuser privilege escalation across organizational roles. - **Authentication:** Test logout safety, session revocation, token replay prevention, and defense against JWT algorithm confusion attacks. - Include `# pragma: no cover` for unreachable defensive code in security-sensitive areas.
229 lines
7.5 KiB
Python
229 lines
7.5 KiB
Python
"""
|
|
Security tests for permissions and access control (app/api/dependencies/permissions.py).
|
|
|
|
Critical security tests covering:
|
|
- Inactive user blocking (prevents deactivated accounts from accessing APIs)
|
|
- Superuser privilege escalation (auto-OWNER role in organizations)
|
|
|
|
These tests prevent unauthorized access and privilege escalation.
|
|
"""
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.user import User
|
|
from app.models.organization import Organization
|
|
from app.crud.user import user as user_crud
|
|
|
|
|
|
class TestInactiveUserBlocking:
|
|
"""
|
|
Test inactive user blocking (permissions.py lines 52-57).
|
|
|
|
Attack Scenario:
|
|
Admin deactivates a user's account (ban/suspension), but user still has
|
|
valid access tokens. System must block ALL API access for inactive users.
|
|
|
|
Covers: permissions.py:52-57
|
|
"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_inactive_user_cannot_access_protected_endpoints(
|
|
self,
|
|
client: AsyncClient,
|
|
async_test_db,
|
|
async_test_user: User,
|
|
user_token: str
|
|
):
|
|
"""
|
|
Test that inactive users are blocked from protected endpoints.
|
|
|
|
Attack Scenario:
|
|
1. User logs in and gets access token
|
|
2. Admin deactivates user account
|
|
3. User tries to access protected endpoint with valid token
|
|
4. System MUST reject (account inactive)
|
|
"""
|
|
test_engine, SessionLocal = async_test_db
|
|
|
|
# Step 1: Verify user can access endpoint while active
|
|
response = await client.get(
|
|
"/api/v1/users/me",
|
|
headers={"Authorization": f"Bearer {user_token}"}
|
|
)
|
|
assert response.status_code == 200, "Active user should have access"
|
|
|
|
# Step 2: Admin deactivates the user
|
|
async with SessionLocal() as session:
|
|
user = await user_crud.get(session, id=async_test_user.id)
|
|
user.is_active = False
|
|
await session.commit()
|
|
|
|
# Step 3: User tries to access endpoint with same token
|
|
response = await client.get(
|
|
"/api/v1/users/me",
|
|
headers={"Authorization": f"Bearer {user_token}"}
|
|
)
|
|
|
|
# Step 4: System MUST reject (covers lines 52-57)
|
|
assert response.status_code == 403, "Inactive user must be blocked"
|
|
data = response.json()
|
|
if "errors" in data:
|
|
assert "inactive" in data["errors"][0]["message"].lower()
|
|
else:
|
|
assert "inactive" in data.get("detail", "").lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_inactive_user_blocked_from_organization_endpoints(
|
|
self,
|
|
client: AsyncClient,
|
|
async_test_db,
|
|
async_test_user: User,
|
|
user_token: str
|
|
):
|
|
"""
|
|
Test that inactive users can't access organization endpoints.
|
|
|
|
Ensures the inactive check applies to ALL protected endpoints.
|
|
"""
|
|
test_engine, SessionLocal = async_test_db
|
|
|
|
# Deactivate user
|
|
async with SessionLocal() as session:
|
|
user = await user_crud.get(session, id=async_test_user.id)
|
|
user.is_active = False
|
|
await session.commit()
|
|
|
|
# Try to list organizations
|
|
response = await client.get(
|
|
"/api/v1/organizations/me",
|
|
headers={"Authorization": f"Bearer {user_token}"}
|
|
)
|
|
|
|
# Must be blocked
|
|
assert response.status_code == 403, "Inactive user blocked from org endpoints"
|
|
|
|
|
|
class TestSuperuserPrivilegeEscalation:
|
|
"""
|
|
Test superuser privilege escalation (permissions.py lines 154-157).
|
|
|
|
Business Logic:
|
|
Superusers automatically get OWNER role in ALL organizations.
|
|
This is intentional for admin oversight, but must be tested to ensure
|
|
it works correctly and doesn't grant too little or too much access.
|
|
|
|
Covers: permissions.py:154-157
|
|
"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_superuser_gets_owner_role_automatically(
|
|
self,
|
|
client: AsyncClient,
|
|
async_test_db,
|
|
async_test_superuser: User,
|
|
superuser_token: str
|
|
):
|
|
"""
|
|
Test that superusers automatically get OWNER role in organizations.
|
|
|
|
Business Rule:
|
|
Superusers can manage any organization without being explicitly added.
|
|
This is for platform administration.
|
|
"""
|
|
test_engine, SessionLocal = async_test_db
|
|
|
|
# Step 1: Create an organization (owned by someone else)
|
|
async with SessionLocal() as session:
|
|
org = Organization(
|
|
name="Test Organization",
|
|
slug="test-org"
|
|
)
|
|
session.add(org)
|
|
await session.commit()
|
|
await session.refresh(org)
|
|
org_id = org.id
|
|
|
|
# Step 2: Superuser tries to access the organization
|
|
# (They're not a member, but should auto-get OWNER role)
|
|
response = await client.get(
|
|
f"/api/v1/organizations/{org_id}",
|
|
headers={"Authorization": f"Bearer {superuser_token}"}
|
|
)
|
|
|
|
# Step 3: Should have access (covers lines 154-157)
|
|
# The get_user_role_in_org function returns OWNER for superusers
|
|
assert response.status_code == 200, "Superuser should access any org"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_superuser_can_manage_any_organization(
|
|
self,
|
|
client: AsyncClient,
|
|
async_test_db,
|
|
async_test_superuser: User,
|
|
superuser_token: str
|
|
):
|
|
"""
|
|
Test that superusers have full management access to all organizations.
|
|
|
|
Ensures the OWNER role privilege escalation works end-to-end.
|
|
"""
|
|
test_engine, SessionLocal = async_test_db
|
|
|
|
# Create an organization
|
|
async with SessionLocal() as session:
|
|
org = Organization(
|
|
name="Test Organization",
|
|
slug="test-org"
|
|
)
|
|
session.add(org)
|
|
await session.commit()
|
|
await session.refresh(org)
|
|
org_id = org.id
|
|
|
|
# Superuser tries to update it (OWNER-only action)
|
|
response = await client.put(
|
|
f"/api/v1/organizations/{org_id}",
|
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
json={"name": "Updated Name"}
|
|
)
|
|
|
|
# Should succeed (superuser has OWNER privileges)
|
|
assert response.status_code in [200, 404], "Superuser should be able to manage any org"
|
|
# Note: Might be 404 if org endpoints require membership, but the role check passes
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_regular_user_does_not_get_owner_role(
|
|
self,
|
|
client: AsyncClient,
|
|
async_test_db,
|
|
async_test_user: User,
|
|
user_token: str
|
|
):
|
|
"""
|
|
Sanity check: Regular users don't get automatic OWNER role.
|
|
|
|
Ensures the superuser check is working correctly (line 154).
|
|
"""
|
|
test_engine, SessionLocal = async_test_db
|
|
|
|
# Create an organization
|
|
async with SessionLocal() as session:
|
|
org = Organization(
|
|
name="Test Organization",
|
|
slug="test-org"
|
|
)
|
|
session.add(org)
|
|
await session.commit()
|
|
await session.refresh(org)
|
|
org_id = org.id
|
|
|
|
# Regular user tries to access it (not a member)
|
|
response = await client.get(
|
|
f"/api/v1/organizations/{org_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"}
|
|
)
|
|
|
|
# Should be denied (not a member, not a superuser)
|
|
assert response.status_code in [403, 404], "Regular user shouldn't access non-member org"
|