- infrasynth.configs: typed multi-tenant config store (registry, service, secrets, cache) + public config_changed/config_reset signals and API - emit the declared-but-dead signals (features flags/overrides, scheduler task_completed/task_failed, tenancy tenant_updated, audit model_changed) and per-model audit field exclusions - security: permission catalog (security_permission), Django-style model-derived AutoPermission, PermissionRegistry, RoleAssignment, global-or-tenant Grant/Revoke, catalog API - consolidate the permission surface: PermissionRegistry only (drop the settings dict), IsAuthenticatedAndPermitted aliases HybridPermission, require_permission replaced by required_permissions + require_all - packaging: add [build-system]; add Forgejo publish workflow (.forgejo)
130 lines
5.2 KiB
Python
130 lines
5.2 KiB
Python
from rest_framework import mixins, viewsets
|
|
from rest_framework.decorators import action
|
|
from rest_framework.response import Response
|
|
|
|
from infrasynth.security.permissions import IsAuthenticatedAndPermitted
|
|
|
|
from .models import FeatureFlag, FeatureFlagOverride
|
|
from .serializers import FeatureFlagOverrideSerializer, FeatureFlagSerializer
|
|
from .services import FeatureService
|
|
from .signals import flag_created, flag_deleted, flag_toggled, override_created, override_deleted
|
|
|
|
|
|
class FeatureFlagViewSet(viewsets.ModelViewSet):
|
|
queryset = FeatureFlag.objects.all()
|
|
serializer_class = FeatureFlagSerializer
|
|
permission_classes = [IsAuthenticatedAndPermitted]
|
|
|
|
def get_queryset(self):
|
|
return FeatureFlag.objects.all()
|
|
|
|
def perform_create(self, serializer):
|
|
instance = serializer.save()
|
|
FeatureService().invalidate(instance.slug, instance.tenant_id)
|
|
flag_created.send(
|
|
sender=FeatureFlag,
|
|
tenant_id=str(instance.tenant_id) if instance.tenant_id else None,
|
|
flag_slug=instance.slug,
|
|
is_active=instance.is_active,
|
|
actor_id=getattr(self.request.user, "pk", None),
|
|
)
|
|
|
|
def perform_update(self, serializer):
|
|
previous_active = serializer.instance.is_active
|
|
instance = serializer.save()
|
|
FeatureService().invalidate(instance.slug, instance.tenant_id)
|
|
if instance.is_active != previous_active:
|
|
flag_toggled.send(
|
|
sender=FeatureFlag,
|
|
tenant_id=str(instance.tenant_id) if instance.tenant_id else None,
|
|
flag_slug=instance.slug,
|
|
is_active=instance.is_active,
|
|
previous_is_active=previous_active,
|
|
actor_id=getattr(self.request.user, "pk", None),
|
|
)
|
|
|
|
def perform_destroy(self, instance):
|
|
slug = instance.slug
|
|
tenant_id = instance.tenant_id
|
|
FeatureService().invalidate(slug, tenant_id)
|
|
actor_id = getattr(self.request.user, "pk", None)
|
|
instance.delete()
|
|
flag_deleted.send(
|
|
sender=FeatureFlag,
|
|
tenant_id=str(tenant_id) if tenant_id else None,
|
|
flag_slug=slug,
|
|
actor_id=actor_id,
|
|
)
|
|
|
|
@action(detail=False, methods=["get"], url_path="active")
|
|
def active_flags(self, request):
|
|
from django.conf import settings
|
|
|
|
from infrasynth.tenancy.context import get_current_tenant
|
|
|
|
fs = FeatureService()
|
|
tenant = get_current_tenant()
|
|
flags = fs.get_active_flags(user=request.user, tenant_id=tenant.pk if tenant is not None else None)
|
|
# A list (not a map) so camelCase key conversion never mangles slug identifiers.
|
|
data = {"flags": [{"slug": slug, "enabled": enabled} for slug, enabled in sorted(flags.items())]}
|
|
config = getattr(settings, "INFRASYNTH_FEATURES", {})
|
|
if config.get("EXPOSE_PERMISSIONS_IN_ACTIVE_ENDPOINT"):
|
|
from infrasynth.security.services import AuthorizationService
|
|
|
|
data["effective_permissions"] = sorted(AuthorizationService().get_effective_permissions(request.user))
|
|
if config.get("EXPOSE_ROLES_IN_ACTIVE_ENDPOINT"):
|
|
roles_qs = getattr(request.user, "roles", None)
|
|
data["roles"] = list(roles_qs.values_list("slug", flat=True)) if roles_qs is not None else []
|
|
return Response(data)
|
|
|
|
@action(detail=False, methods=["get"], url_path="check/(?P<slug>[^/]+)")
|
|
def check_flag(self, request, slug=None):
|
|
from infrasynth.tenancy.context import get_current_tenant
|
|
|
|
fs = FeatureService()
|
|
tenant = get_current_tenant()
|
|
enabled = fs.is_enabled(slug, user=request.user, tenant_id=tenant.pk if tenant is not None else None)
|
|
return Response({"slug": slug, "is_enabled": enabled})
|
|
|
|
|
|
class FeatureFlagOverrideViewSet(
|
|
mixins.CreateModelMixin,
|
|
mixins.ListModelMixin,
|
|
mixins.DestroyModelMixin,
|
|
viewsets.GenericViewSet,
|
|
):
|
|
queryset = FeatureFlagOverride.objects.all()
|
|
serializer_class = FeatureFlagOverrideSerializer
|
|
permission_classes = [IsAuthenticatedAndPermitted]
|
|
|
|
def get_queryset(self):
|
|
return FeatureFlagOverride.objects.select_related("flag", "user").all()
|
|
|
|
def perform_create(self, serializer):
|
|
instance = serializer.save()
|
|
flag_slug = instance.flag.slug
|
|
tenant_id = instance.tenant_id
|
|
FeatureService().invalidate(flag_slug, tenant_id)
|
|
override_created.send(
|
|
sender=FeatureFlagOverride,
|
|
tenant_id=str(tenant_id) if tenant_id else None,
|
|
flag_slug=flag_slug,
|
|
user_id=instance.user_id,
|
|
is_enabled=instance.is_enabled,
|
|
actor_id=getattr(self.request.user, "pk", None),
|
|
)
|
|
|
|
def perform_destroy(self, instance):
|
|
flag_slug = instance.flag.slug
|
|
tenant_id = instance.tenant_id
|
|
user_id = instance.user_id
|
|
FeatureService().invalidate(flag_slug, tenant_id)
|
|
actor_id = getattr(self.request.user, "pk", None)
|
|
instance.delete()
|
|
override_deleted.send(
|
|
sender=FeatureFlagOverride,
|
|
tenant_id=str(tenant_id) if tenant_id else None,
|
|
flag_slug=flag_slug,
|
|
user_id=user_id,
|
|
actor_id=actor_id,
|
|
)
|