198 lines
7.4 KiB
Python
198 lines
7.4 KiB
Python
from contextlib import contextmanager
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
from django.contrib.auth import get_user_model
|
|
from django.core.cache import cache
|
|
from django.db import transaction
|
|
from django.dispatch import receiver
|
|
from django.test import override_settings
|
|
from rest_framework.test import APIClient
|
|
|
|
from infrasynth.audit.models import ModelChangeLog, SecurityEvent
|
|
from infrasynth.audit.signals import security_event_occurred
|
|
from infrasynth.features.models import FeatureFlag, FeatureFlagOverride
|
|
from infrasynth.features.registry import FeatureRegistry
|
|
from infrasynth.features.services import FeatureService
|
|
from infrasynth.security.models import Grant, Revoke, Role
|
|
from infrasynth.security.services import AuthorizationService
|
|
from infrasynth.webhooks.models import (
|
|
OutboundDelivery,
|
|
OutboundEndpoint,
|
|
OutboundSubscription,
|
|
)
|
|
from infrasynth.webhooks.registry import EventRegistry
|
|
from infrasynth.webhooks.signals import outbound_delivery_succeeded
|
|
|
|
UserModel = get_user_model()
|
|
pytestmark = pytest.mark.django_db
|
|
|
|
|
|
@contextmanager
|
|
def _emit_and_run_commit_hooks():
|
|
with transaction.atomic():
|
|
yield
|
|
connection = transaction.get_connection()
|
|
pending = list(connection.run_on_commit)
|
|
connection.run_on_commit = []
|
|
for entry in pending:
|
|
func = entry[1]
|
|
func()
|
|
|
|
|
|
@override_settings(CELERY_TASK_ALWAYS_EAGER=True)
|
|
def test_e2e_login_permission_feature_webhook_audit(user, clean_feature_registry):
|
|
authz = AuthorizationService()
|
|
fs = FeatureService()
|
|
|
|
# a. Force-authenticate the user and verify the session check endpoint
|
|
client = APIClient()
|
|
client.force_authenticate(user=user)
|
|
resp = client.get("/api/auth/check/")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["id"] == user.pk
|
|
|
|
# b. Create Role with specific permissions and assign to user
|
|
role = Role.objects.create(
|
|
name="Webhook Manager",
|
|
slug="webhook-manager",
|
|
permissions=["webhooks.manage_endpoints"],
|
|
)
|
|
role.users.add(user)
|
|
|
|
# c. Create Grant for an additional permission
|
|
Grant.objects.create(user=user, codename="audit.view_logs")
|
|
|
|
# d. Verify permission resolution via AuthorizationService
|
|
assert authz.has_permission(user, "webhooks.manage_endpoints") is True
|
|
assert authz.has_permission(user, "audit.view_logs") is True
|
|
assert authz.has_permission(user, "unknown.perm") is False
|
|
|
|
# e. Feature flag registration and DB gating
|
|
FeatureRegistry.register("e2e.beta", default=True, name="E2E Beta")
|
|
FeatureFlag.objects.create(slug="e2e.beta", is_active=True)
|
|
cache.clear()
|
|
assert fs.is_enabled("e2e.beta", user=user) is True
|
|
assert fs.is_enabled("unknown.feature", user=user) is False
|
|
|
|
# f. Register event, create outbound endpoint + subscription
|
|
EventRegistry.register(
|
|
"e2e.test.event",
|
|
description="E2E test event",
|
|
example_payload={"test_key": "test_value"},
|
|
)
|
|
endpoint = OutboundEndpoint.objects.create(
|
|
name="E2E Target",
|
|
url="https://example.com/e2e-hook",
|
|
secret="e2e-secret",
|
|
)
|
|
sub = OutboundSubscription.objects.create(
|
|
endpoint=endpoint,
|
|
event_name="e2e.test.event",
|
|
)
|
|
|
|
# Connect receiver bridge: outbound_delivery_succeeded -> SecurityEvent
|
|
@receiver(outbound_delivery_succeeded)
|
|
def bridge_create_audit_event(sender, **kwargs):
|
|
security_event_occurred.send(
|
|
sender=OutboundDelivery,
|
|
event_type="webhook_delivered",
|
|
actor=user,
|
|
metadata={
|
|
"delivery_id": kwargs.get("delivery_id"),
|
|
"event_name": kwargs.get("event_name"),
|
|
"status_code": kwargs.get("status_code"),
|
|
},
|
|
)
|
|
|
|
payload = {"test_key": "test_value"}
|
|
with mock.patch("infrasynth.webhooks.dispatch.requests.post") as mock_post:
|
|
mock_post.return_value.status_code = 200
|
|
mock_post.return_value.text = "ok"
|
|
with _emit_and_run_commit_hooks():
|
|
EventRegistry.emit("e2e.test.event", payload)
|
|
|
|
# g. Verify OutboundDelivery was created via Celery eager delivery
|
|
assert OutboundDelivery.objects.filter(subscription=sub).exists()
|
|
delivery = OutboundDelivery.objects.get(subscription=sub)
|
|
assert delivery.status == OutboundDelivery.Status.SUCCESS
|
|
assert delivery.payload is not None
|
|
assert delivery.response_status == 200
|
|
|
|
# h. Verify SecurityEvent was created by the signal bridge
|
|
assert SecurityEvent.objects.filter(event_type="webhook_delivered").exists()
|
|
se = SecurityEvent.objects.get(event_type="webhook_delivered")
|
|
assert se.actor == user
|
|
assert se.metadata["delivery_id"] == delivery.id
|
|
assert se.metadata["event_name"] == "e2e.test.event"
|
|
assert se.metadata["status_code"] == 200
|
|
|
|
# i. Verify ModelChangeLog entries exist for the created models
|
|
labels = set(ModelChangeLog.objects.values_list("model_label", flat=True))
|
|
expected_labels = [
|
|
"infrasynth_security.Role",
|
|
"infrasynth_security.Grant",
|
|
"infrasynth_webhooks.OutboundEndpoint",
|
|
"infrasynth_webhooks.OutboundSubscription",
|
|
"infrasynth_webhooks.OutboundDelivery",
|
|
]
|
|
for label in expected_labels:
|
|
assert label in labels, f"Missing ModelChangeLog for {label}"
|
|
|
|
outbound_delivery_succeeded.disconnect(bridge_create_audit_event)
|
|
|
|
|
|
def test_e2e_permission_chain_resolution(user, admin_user):
|
|
authz = AuthorizationService()
|
|
|
|
# a. Create role + grant + revoke for the same user
|
|
role = Role.objects.create(
|
|
name="Tester",
|
|
slug="tester",
|
|
permissions=["perm.role_a", "perm.role_b"],
|
|
)
|
|
role.users.add(user)
|
|
Grant.objects.create(user=user, codename="perm.grant_c")
|
|
Revoke.objects.create(user=user, codename="perm.role_a")
|
|
|
|
# b. Verify Revoke > Grant > Role > Default precedence
|
|
assert authz.has_permission(user, "perm.role_a") is False
|
|
assert authz.has_permission(user, "perm.role_b") is True
|
|
assert authz.has_permission(user, "perm.grant_c") is True
|
|
assert authz.has_permission(user, "perm.default") is False
|
|
|
|
# c. Verify superuser gets ["*"] wildcard
|
|
assert authz.has_permission(admin_user, "anything.at.all") is True
|
|
assert authz.get_effective_permissions(admin_user) == {"*"}
|
|
|
|
# d. Verify get_effective_permissions returns union minus revokes
|
|
perms = authz.get_effective_permissions(user)
|
|
assert "perm.role_a" not in perms
|
|
assert "perm.role_b" in perms
|
|
assert "perm.grant_c" in perms
|
|
assert len(perms) == 2
|
|
|
|
|
|
def test_e2e_feature_flag_with_user_override(user, clean_feature_registry):
|
|
fs = FeatureService()
|
|
|
|
# a. Register feature in registry (default True)
|
|
FeatureRegistry.register("e2e.feature", default=True)
|
|
assert fs.is_enabled("e2e.feature") is True
|
|
|
|
# b. Create DB FeatureFlag (is_active=False) — overrides registry default
|
|
flag = FeatureFlag.objects.create(slug="e2e.feature", is_active=False)
|
|
cache.clear()
|
|
assert fs.is_enabled("e2e.feature") is False
|
|
|
|
# c. Create FeatureFlagOverride for the specific user (is_enabled=True)
|
|
FeatureFlagOverride.objects.create(flag=flag, user=user, is_enabled=True)
|
|
assert fs.is_enabled("e2e.feature", user=user) is True
|
|
|
|
# d. Verify the override is user-specific
|
|
other_user = UserModel.objects.create_user(
|
|
username="other",
|
|
email="other@example.com",
|
|
password="pass123",
|
|
)
|
|
assert fs.is_enabled("e2e.feature", user=other_user) is False
|