arbret/backend/tests/test_permissions.py

303 lines
11 KiB
Python
Raw Normal View History

2025-12-18 23:33:32 +01:00
"""
Permission and Role-Based Access Control Tests
These tests verify that:
1. Users can only access endpoints they have permission for
2. Users without proper roles are denied access (403)
3. Unauthenticated users are denied access (401)
4. The permission system cannot be bypassed
"""
2025-12-18 23:33:32 +01:00
import pytest
from models import Permission
# =============================================================================
# Role Assignment Tests
# =============================================================================
2025-12-18 23:33:32 +01:00
class TestRoleAssignment:
"""Test that roles are properly assigned and returned."""
@pytest.mark.asyncio
async def test_regular_user_has_correct_roles(self, client_factory, regular_user):
async with client_factory.create(cookies=regular_user["cookies"]) as client:
response = await client.get("/api/auth/me")
2025-12-18 23:33:32 +01:00
assert response.status_code == 200
data = response.json()
assert "regular" in data["roles"]
assert "admin" not in data["roles"]
@pytest.mark.asyncio
async def test_admin_user_has_correct_roles(self, client_factory, admin_user):
async with client_factory.create(cookies=admin_user["cookies"]) as client:
response = await client.get("/api/auth/me")
2025-12-18 23:33:32 +01:00
assert response.status_code == 200
data = response.json()
assert "admin" in data["roles"]
assert "regular" not in data["roles"]
@pytest.mark.asyncio
async def test_regular_user_has_correct_permissions(
self, client_factory, regular_user
):
2025-12-18 23:33:32 +01:00
async with client_factory.create(cookies=regular_user["cookies"]) as client:
response = await client.get("/api/auth/me")
2025-12-18 23:33:32 +01:00
data = response.json()
permissions = data["permissions"]
# Should have profile and exchange permissions
assert Permission.MANAGE_OWN_PROFILE.value in permissions
assert Permission.CREATE_EXCHANGE.value in permissions
assert Permission.VIEW_OWN_EXCHANGES.value in permissions
2025-12-18 23:33:32 +01:00
# Should NOT have audit permission
assert Permission.VIEW_AUDIT.value not in permissions
@pytest.mark.asyncio
async def test_admin_user_has_correct_permissions(self, client_factory, admin_user):
async with client_factory.create(cookies=admin_user["cookies"]) as client:
response = await client.get("/api/auth/me")
2025-12-18 23:33:32 +01:00
data = response.json()
permissions = data["permissions"]
2025-12-18 23:33:32 +01:00
# Should have audit permission
assert Permission.VIEW_AUDIT.value in permissions
# Should NOT have exchange permissions (those are for regular users)
assert Permission.CREATE_EXCHANGE.value not in permissions
2025-12-18 23:33:32 +01:00
@pytest.mark.asyncio
async def test_user_with_no_roles_has_no_permissions(
self, client_factory, user_no_roles
):
2025-12-18 23:33:32 +01:00
async with client_factory.create(cookies=user_no_roles["cookies"]) as client:
response = await client.get("/api/auth/me")
2025-12-18 23:33:32 +01:00
data = response.json()
assert data["roles"] == []
assert data["permissions"] == []
# =============================================================================
# Audit Endpoint Access Tests
# =============================================================================
2025-12-18 23:33:32 +01:00
class TestAuditAccess:
"""Test access control for audit endpoints."""
@pytest.mark.asyncio
async def test_admin_can_view_price_history(self, client_factory, admin_user):
2025-12-18 23:33:32 +01:00
async with client_factory.create(cookies=admin_user["cookies"]) as client:
response = await client.get("/api/audit/price-history")
2025-12-18 23:33:32 +01:00
assert response.status_code == 200
# Returns a list
assert isinstance(response.json(), list)
2025-12-18 23:33:32 +01:00
@pytest.mark.asyncio
async def test_regular_user_cannot_view_price_history(
self, client_factory, regular_user
):
2025-12-18 23:33:32 +01:00
"""Regular users should be forbidden from audit endpoints."""
async with client_factory.create(cookies=regular_user["cookies"]) as client:
response = await client.get("/api/audit/price-history")
2025-12-18 23:33:32 +01:00
assert response.status_code == 403
assert "permission" in response.json()["detail"].lower()
@pytest.mark.asyncio
async def test_user_without_roles_cannot_view_audit(
self, client_factory, user_no_roles
):
2025-12-18 23:33:32 +01:00
async with client_factory.create(cookies=user_no_roles["cookies"]) as client:
response = await client.get("/api/audit/price-history")
assert response.status_code == 403
@pytest.mark.asyncio
async def test_unauthenticated_cannot_view_price_history(self, client):
response = await client.get("/api/audit/price-history")
assert response.status_code == 401
2025-12-18 23:33:32 +01:00
# =============================================================================
# Offensive Security Tests - Bypass Attempts
# =============================================================================
2025-12-18 23:33:32 +01:00
class TestSecurityBypassAttempts:
"""
Offensive tests that attempt to bypass security controls.
These simulate potential attack vectors.
"""
@pytest.mark.asyncio
async def test_cannot_access_audit_with_forged_role_claim(
self, client_factory, regular_user
):
2025-12-18 23:33:32 +01:00
"""
Attempt to access audit by somehow claiming admin role.
The server should verify roles from DB, not trust client claims.
"""
# Regular user tries to access audit endpoint
async with client_factory.create(cookies=regular_user["cookies"]) as client:
response = await client.get("/api/audit/price-history")
2025-12-18 23:33:32 +01:00
# Should be denied regardless of any manipulation attempts
assert response.status_code == 403
@pytest.mark.asyncio
async def test_cannot_access_with_expired_session(self, client_factory):
2025-12-18 23:33:32 +01:00
"""Test that invalid/expired tokens are rejected."""
fake_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5OTk5IiwiZXhwIjoxfQ.invalid"
2025-12-18 23:33:32 +01:00
async with client_factory.create(cookies={"auth_token": fake_token}) as client:
response = await client.get("/api/profile")
2025-12-18 23:33:32 +01:00
assert response.status_code == 401
@pytest.mark.asyncio
async def test_cannot_access_with_tampered_token(
self, client_factory, regular_user
):
2025-12-18 23:33:32 +01:00
"""Test that tokens signed with wrong key are rejected."""
# Take a valid token and modify it
original_token = regular_user["cookies"].get("auth_token", "")
if original_token:
tampered_token = original_token[:-5] + "XXXXX"
async with client_factory.create(
cookies={"auth_token": tampered_token}
) as client:
response = await client.get("/api/profile")
2025-12-18 23:33:32 +01:00
assert response.status_code == 401
@pytest.mark.asyncio
async def test_cannot_escalate_to_admin_via_registration(self, client_factory):
"""
Test that new registrations cannot claim admin role.
New users should only get 'regular' role by default.
"""
2025-12-20 11:43:32 +01:00
from models import ROLE_REGULAR
from tests.conftest import create_user_with_roles
from tests.helpers import create_invite_for_godfather, unique_email
2025-12-20 11:12:11 +01:00
async with client_factory.get_db_session() as db:
godfather = await create_user_with_roles(
db, unique_email("gf"), "pass123", [ROLE_REGULAR]
)
2025-12-20 11:43:32 +01:00
invite_code = await create_invite_for_godfather(db, godfather.id)
2025-12-18 23:33:32 +01:00
response = await client_factory.post(
"/api/auth/register",
2025-12-20 11:12:11 +01:00
json={
"email": unique_email(),
"password": "password123",
"invite_identifier": invite_code,
},
2025-12-18 23:33:32 +01:00
)
2025-12-18 23:33:32 +01:00
assert response.status_code == 200
data = response.json()
2025-12-18 23:33:32 +01:00
# Should only have regular role, not admin
assert "admin" not in data["roles"]
assert Permission.VIEW_AUDIT.value not in data["permissions"]
2025-12-18 23:33:32 +01:00
# Try to access audit with this new user
async with client_factory.create(cookies=dict(response.cookies)) as client:
audit_response = await client.get("/api/audit/price-history")
2025-12-18 23:33:32 +01:00
assert audit_response.status_code == 403
@pytest.mark.asyncio
async def test_deleted_user_token_is_invalid(self, client_factory):
"""
If a user is deleted, their token should no longer work.
This tests that tokens are validated against current DB state.
"""
from sqlalchemy import delete
2025-12-18 23:33:32 +01:00
from models import User
from tests.helpers import unique_email
2025-12-18 23:33:32 +01:00
email = unique_email("deleted")
2025-12-18 23:33:32 +01:00
# Create and login user
async with client_factory.get_db_session() as db:
from tests.conftest import create_user_with_roles
2025-12-18 23:33:32 +01:00
user = await create_user_with_roles(db, email, "password123", ["regular"])
user_id = user.id
2025-12-18 23:33:32 +01:00
login_response = await client_factory.post(
"/api/auth/login",
json={"email": email, "password": "password123"},
)
cookies = dict(login_response.cookies)
2025-12-18 23:33:32 +01:00
# Delete the user from DB
async with client_factory.get_db_session() as db:
await db.execute(delete(User).where(User.id == user_id))
await db.commit()
2025-12-18 23:33:32 +01:00
# Try to use the old token
async with client_factory.create(cookies=cookies) as client:
response = await client.get("/api/auth/me")
2025-12-18 23:33:32 +01:00
assert response.status_code == 401
@pytest.mark.asyncio
async def test_role_change_reflected_immediately(self, client_factory):
"""
If a user's role is changed, the change should be reflected
in subsequent requests (no stale permission cache).
"""
from sqlalchemy import select
from models import Role, User
from tests.helpers import unique_email
2025-12-18 23:33:32 +01:00
email = unique_email("rolechange")
2025-12-18 23:33:32 +01:00
# Create regular user
async with client_factory.get_db_session() as db:
from tests.conftest import create_user_with_roles
2025-12-18 23:33:32 +01:00
await create_user_with_roles(db, email, "password123", ["regular"])
2025-12-18 23:33:32 +01:00
login_response = await client_factory.post(
"/api/auth/login",
json={"email": email, "password": "password123"},
)
cookies = dict(login_response.cookies)
# Verify can access profile but not audit
2025-12-18 23:33:32 +01:00
async with client_factory.create(cookies=cookies) as client:
assert (await client.get("/api/profile")).status_code == 200
assert (await client.get("/api/audit/price-history")).status_code == 403
2025-12-18 23:33:32 +01:00
# Change user's role from regular to admin
async with client_factory.get_db_session() as db:
result = await db.execute(select(User).where(User.email == email))
user = result.scalar_one()
2025-12-18 23:33:32 +01:00
result = await db.execute(select(Role).where(Role.name == "admin"))
admin_role = result.scalar_one()
user.roles = [admin_role] # Replace roles with admin only
2025-12-18 23:33:32 +01:00
await db.commit()
# Now should have audit access but not profile access (admin doesn't have MANAGE_OWN_PROFILE)
2025-12-18 23:33:32 +01:00
async with client_factory.create(cookies=cookies) as client:
assert (await client.get("/api/audit/price-history")).status_code == 200
assert (await client.get("/api/profile")).status_code == 403