infrasynth-backend-kit/tests/test_security/test_authorization.py
jcv-dev 551b42eab5 feat: production-hardening pass across the kit
Close the gaps between the documented contract (API-STANDARD, TENANCY,
ENTITLEMENTS) and the implementation, and remove committed build artifacts.

Security:
- verify + process inbound webhooks (HMAC/handler verify, size limit,
  timestamp tolerance, idempotency via InboundEvent.external_id)
- real 2FA login flow (pre-auth challenge; tokens only after verify/recovery)
- wire HybridPermission into security/audit views; add API-key rotate and
  users/<id>/permissions|roles endpoints
- tenant-scoped throttling on by default; webhook replay protection
- verify MercadoPago webhook signatures
- login brute-force guard, configurable password policy, real ALTCHA PoW

Correctness:
- apply verified billing webhooks idempotently (subscription/entitlement/
  invoice/PaymentTransaction); scheduled payment lifecycle jobs
- capture audit update diffs automatically; add audit retention purge
- working notification retries, per-channel rate limits, log retention
- pluggable virus scanner, upload-size limit, pipeline toggle
- feature rollout %/environment targeting; settings-driven registrations
- workflow guards (instance cap, route depth, self-assignment, clone on re-entry)
- wire every previously-dead INFRASYNTH_* setting; drop truly dead ones

Delivery:
- README + CHANGELOG; CI format check + coverage gate
- keep test media out of the tree; untrack .coverage, __pycache__,
  egg-info, docs/ and invoice artifacts
2026-09-24 10:41:21 -05:00

335 lines
14 KiB
Python

from datetime import timedelta
import pytest
from django.utils import timezone
from rest_framework import status
from infrasynth.security.models import Grant, Revoke, Role
from infrasynth.security.permissions import HybridPermission, require_permission
from infrasynth.security.services import AuthorizationService
@pytest.fixture
def authz():
return AuthorizationService()
@pytest.fixture
def role(db):
return Role.objects.create(name="Editor", slug="editor", permissions=["content.edit", "content.view"])
class TestPermissionResolutionChain:
def test_unauthenticated_denied(self, authz, db):
anon = type("Anon", (), {"is_authenticated": False})()
assert authz.has_permission(anon, "content.edit") is False
def test_no_permission_default_denied(self, authz, user):
assert authz.has_permission(user, "content.edit") is False
def test_superuser_always_allowed(self, authz, admin_user):
assert authz.has_permission(admin_user, "anything.at.all") is True
def test_role_permission_granted(self, authz, user, role):
role.users.add(user)
assert authz.has_permission(user, "content.edit") is True
def test_direct_grant_granted(self, authz, user):
Grant.objects.create(user=user, codename="billing.view")
assert authz.has_permission(user, "billing.view") is True
def test_expired_grant_denied(self, authz, user):
Grant.objects.create(
user=user,
codename="billing.view",
expires_at=timezone.now() - timedelta(days=1),
)
assert authz.has_permission(user, "billing.view") is False
def test_future_grant_allowed(self, authz, user):
Grant.objects.create(
user=user,
codename="billing.view",
expires_at=timezone.now() + timedelta(days=1),
)
assert authz.has_permission(user, "billing.view") is True
def test_revoke_beats_grant(self, authz, user):
Grant.objects.create(user=user, codename="content.edit")
Revoke.objects.create(user=user, codename="content.edit")
assert authz.has_permission(user, "content.edit") is False
def test_revoke_beats_role(self, authz, user, role):
role.users.add(user)
Revoke.objects.create(user=user, codename="content.edit")
assert authz.has_permission(user, "content.edit") is False
def test_revoke_only_affects_target_permission(self, authz, user, role):
role.users.add(user)
Revoke.objects.create(user=user, codename="content.edit")
assert authz.has_permission(user, "content.view") is True
def test_union_of_multiple_roles(self, authz, user):
Role.objects.create(slug="r1", name="R1", permissions=["perm.a"]).users.add(user)
Role.objects.create(slug="r2", name="R2", permissions=["perm.b"]).users.add(user)
assert authz.has_permission(user, "perm.a") is True
assert authz.has_permission(user, "perm.b") is True
def test_superuser_revoke_ignored(self, authz, admin_user):
Revoke.objects.create(user=admin_user, codename="anything")
assert authz.has_permission(admin_user, "anything") is True
class TestEffectivePermissions:
def test_superuser_returns_wildcard(self, authz, admin_user):
assert authz.get_effective_permissions(admin_user) == {"*"}
def test_unauthenticated_empty(self, authz, db):
anon = type("Anon", (), {"is_authenticated": False})()
assert authz.get_effective_permissions(anon) == set()
def test_merges_grant_and_role(self, authz, user, role):
role.permissions = ["role.perm"]
role.save()
role.users.add(user)
Grant.objects.create(user=user, codename="grant.perm")
perms = authz.get_effective_permissions(user)
assert perms == {"role.perm", "grant.perm"}
def test_revoked_permissions_removed(self, authz, user, role):
role.users.add(user)
Revoke.objects.create(user=user, codename="content.edit")
perms = authz.get_effective_permissions(user)
assert "content.edit" not in perms
assert "content.view" in perms
def test_empty_role_permissions(self, authz, user):
Role.objects.create(slug="empty", name="Empty", permissions=[]).users.add(user)
assert authz.get_effective_permissions(user) == set()
class TestAggregateChecks:
def test_has_all_permissions(self, authz, user, role):
role.permissions = ["perm.a", "perm.b"]
role.save()
role.users.add(user)
assert authz.has_all_permissions(user, ["perm.a", "perm.b"]) is True
assert authz.has_all_permissions(user, ["perm.a", "perm.c"]) is False
def test_has_any_permission(self, authz, user, role):
role.permissions = ["perm.a"]
role.save()
role.users.add(user)
assert authz.has_any_permission(user, ["perm.a", "perm.b"]) is True
assert authz.has_any_permission(user, ["perm.b", "perm.c"]) is False
class TestSystemUserPermissions:
@pytest.fixture
def system_user(self):
from infrasynth.security.auth.api_keys import SystemUser
return SystemUser(scopes=["read:users", "write:billing"])
def test_scope_permission_granted(self, authz, system_user):
assert authz.has_permission(system_user, "read:users") is True
assert authz.has_permission(system_user, "write:billing") is True
def test_unknown_scope_denied(self, authz, system_user):
assert authz.has_permission(system_user, "delete:users") is False
def test_effective_permissions_are_scopes(self, authz, system_user):
assert authz.get_effective_permissions(system_user) == {
"read:users",
"write:billing",
}
def test_empty_scopes_deny_all(self, authz, db):
from infrasynth.security.auth.api_keys import SystemUser
sys_user = SystemUser()
assert authz.has_permission(sys_user, "anything") is False
assert authz.get_effective_permissions(sys_user) == set()
class _PermissionView:
def __init__(self, required_permissions=None):
self.required_permissions = required_permissions
class TestHybridPermission:
def test_no_required_permissions_allows(self, user):
request = type("R", (), {"user": user})()
perm = HybridPermission()
assert perm.has_permission(request, _PermissionView(None)) is True
def test_any_permission_sufficient(self, user):
Grant.objects.create(user=user, codename="one.of.these")
request = type("R", (), {"user": user})()
perm = HybridPermission()
view = _PermissionView(["one.of.these", "other.perm"])
assert perm.has_permission(request, view) is True
def test_no_permission_denied_for_member(self, member_user):
request = type("R", (), {"user": member_user})()
perm = HybridPermission()
view = _PermissionView(["missing.perm"])
assert perm.has_permission(request, view) is False
def test_owner_is_allowed(self, user):
request = type("R", (), {"user": user})()
perm = HybridPermission()
assert perm.has_permission(request, _PermissionView(["missing.perm"])) is True
def test_unauthenticated_denied(self, db):
anon = type("R", (), {"user": type("U", (), {"is_authenticated": False})()})()
perm = HybridPermission()
assert perm.has_permission(anon, _PermissionView(None)) is False
class TestRequirePermission:
def test_all_permissions_required(self, user):
Grant.objects.create(user=user, codename="perm.a")
Grant.objects.create(user=user, codename="perm.b")
request = type("R", (), {"user": user})()
perm_class = require_permission("perm.a", "perm.b")
assert perm_class().has_permission(request, _PermissionView()) is True
def test_missing_any_denied(self, member_user):
Grant.objects.create(user=member_user, codename="perm.a")
request = type("R", (), {"user": member_user})()
perm_class = require_permission("perm.a", "perm.b")
assert perm_class().has_permission(request, _PermissionView()) is False
class TestRoleViewSet:
def test_list_roles(self, authenticated_client, role):
resp = authenticated_client.get("/api/v1/auth/roles/")
assert resp.status_code == status.HTTP_200_OK
assert resp.json()["count"] == 1
def test_create_role(self, authenticated_client):
resp = authenticated_client.post(
"/api/v1/auth/roles/",
{"name": "Viewer", "slug": "viewer", "permissions": ["content.view"]},
format="json",
)
assert resp.status_code == status.HTTP_201_CREATED
assert Role.objects.filter(slug="viewer").exists()
def test_retrieve_role(self, authenticated_client, role):
resp = authenticated_client.get(f"/api/v1/auth/roles/{role.slug}/")
assert resp.status_code == status.HTTP_200_OK
assert resp.json()["permissions"] == ["content.edit", "content.view"]
def test_update_role(self, authenticated_client, role):
resp = authenticated_client.put(
f"/api/v1/auth/roles/{role.slug}/",
{"name": "Editor2", "slug": "editor", "permissions": ["content.edit"]},
format="json",
)
assert resp.status_code == status.HTTP_200_OK
role.refresh_from_db()
assert role.name == "Editor2"
def test_delete_role(self, authenticated_client, role):
resp = authenticated_client.delete(f"/api/v1/auth/roles/{role.slug}/")
assert resp.status_code == status.HTTP_204_NO_CONTENT
assert not Role.objects.filter(pk=role.pk).exists()
def test_system_role_cannot_be_deleted(self, authenticated_client, db):
system_role = Role.objects.create(slug="sys", name="Sys", permissions=[], is_system=True)
resp = authenticated_client.delete(f"/api/v1/auth/roles/{system_role.slug}/")
assert resp.status_code in (
status.HTTP_400_BAD_REQUEST,
status.HTTP_403_FORBIDDEN,
)
assert Role.objects.filter(pk=system_role.pk).exists()
def test_requires_auth(self, api_client):
resp = api_client.get("/api/v1/auth/roles/")
assert resp.status_code == status.HTTP_401_UNAUTHORIZED
class TestPermissionEnforcement:
def test_owner_allowed(self, authenticated_client):
assert authenticated_client.get("/api/v1/auth/roles/").status_code == status.HTTP_200_OK
def test_member_denied_without_permission(self, member_client):
assert member_client.get("/api/v1/auth/roles/").status_code == status.HTTP_403_FORBIDDEN
def test_member_allowed_with_grant(self, member_client, member_user):
Grant.objects.create(user=member_user, codename="security.manage_roles")
assert member_client.get("/api/v1/auth/roles/").status_code == status.HTTP_200_OK
def test_member_denied_for_unrelated_permission(self, member_client, member_user):
Grant.objects.create(user=member_user, codename="content.view")
assert member_client.get("/api/v1/auth/roles/").status_code == status.HTTP_403_FORBIDDEN
def test_user_permissions_endpoint(self, authenticated_client, user):
Grant.objects.create(user=user, codename="billing.view")
resp = authenticated_client.get(f"/api/v1/auth/users/{user.pk}/permissions/")
assert resp.status_code == status.HTTP_200_OK
assert "billing.view" in resp.json()["permissions"]
def test_user_roles_set_endpoint(self, authenticated_client, user):
role = Role.objects.create(name="Viewer", slug="viewer", permissions=["content.view"])
resp = authenticated_client.put(
f"/api/v1/auth/users/{user.pk}/roles/",
{"roles": [role.slug]},
format="json",
)
assert resp.status_code == status.HTTP_200_OK
assert role.slug in resp.json()["roles"]
assert role.users.filter(pk=user.pk).exists()
class TestGrantViewSet:
def test_create_grant(self, authenticated_client, user):
resp = authenticated_client.post(
"/api/v1/auth/grants/",
{"user": user.pk, "codename": "billing.view", "reason": "temporary"},
format="json",
)
assert resp.status_code == status.HTTP_201_CREATED
grant = Grant.objects.get(codename="billing.view")
assert grant.granted_by == user
def test_list_grants(self, authenticated_client, user):
Grant.objects.create(user=user, codename="a.perm")
resp = authenticated_client.get("/api/v1/auth/grants/")
assert resp.status_code == status.HTTP_200_OK
assert resp.json()["count"] == 1
def test_delete_grant(self, authenticated_client, user):
grant = Grant.objects.create(user=user, codename="a.perm")
resp = authenticated_client.delete(f"/api/v1/auth/grants/{grant.pk}/")
assert resp.status_code == status.HTTP_204_NO_CONTENT
assert not Grant.objects.filter(pk=grant.pk).exists()
def test_grant_requires_auth(self, api_client):
resp = api_client.get("/api/v1/auth/grants/")
assert resp.status_code == status.HTTP_401_UNAUTHORIZED
class TestRevokeViewSet:
def test_create_revoke(self, authenticated_client, user):
resp = authenticated_client.post(
"/api/v1/auth/revokes/",
{"user": user.pk, "codename": "billing.view", "reason": "audit"},
format="json",
)
assert resp.status_code == status.HTTP_201_CREATED
assert Revoke.objects.filter(codename="billing.view").exists()
def test_list_revokes(self, authenticated_client, user):
Revoke.objects.create(user=user, codename="a.perm")
resp = authenticated_client.get("/api/v1/auth/revokes/")
assert resp.status_code == status.HTTP_200_OK
assert resp.json()["count"] == 1
def test_delete_revoke(self, authenticated_client, user):
revoke = Revoke.objects.create(user=user, codename="a.perm")
resp = authenticated_client.delete(f"/api/v1/auth/revokes/{revoke.pk}/")
assert resp.status_code == status.HTTP_204_NO_CONTENT
assert not Revoke.objects.filter(pk=revoke.pk).exists()