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(self, user): request = type("R", (), {"user": user})() perm = HybridPermission() view = _PermissionView(["missing.perm"]) assert perm.has_permission(request, view) is False 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, user): Grant.objects.create(user=user, codename="perm.a") request = type("R", (), {"user": 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/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/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/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/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/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/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/auth/roles/") assert resp.status_code == status.HTTP_401_UNAUTHORIZED class TestGrantViewSet: def test_create_grant(self, authenticated_client, user): resp = authenticated_client.post( "/api/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/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/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/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/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/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/auth/revokes/{revoke.pk}/") assert resp.status_code == status.HTTP_204_NO_CONTENT assert not Revoke.objects.filter(pk=revoke.pk).exists()