infrasynth-backend-kit/infrasynth/security/serializers.py
jcv-dev a2930426b4 feat: tenant configs, live signals, and automatic permission management
- 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)
2026-09-29 17:06:54 -05:00

139 lines
4.7 KiB
Python

from rest_framework import serializers
from infrasynth.shared.settings_utils import get_setting
from .models import APIKey, Grant, Permission, Revoke, Role
from .services import AuthorizationService
class PermissionSerializer(serializers.ModelSerializer):
class Meta:
model = Permission
fields = [
"id",
"codename",
"name",
"app_label",
"model",
"action",
"group",
"description",
"is_custom",
"is_active",
]
read_only_fields = fields
class RoleSerializer(serializers.ModelSerializer):
class Meta:
model = Role
fields = ["id", "name", "slug", "description", "permissions", "is_system", "users"]
read_only_fields = ["id", "is_system"]
def validate_permissions(self, value):
if not get_setting("INFRASYNTH_SECURITY", "STRICT_PERMISSION_VALIDATION", False):
return value
known = set(Permission.objects.filter(is_active=True).values_list("codename", flat=True))
unknown = sorted({codename for codename in value if codename not in known})
if unknown:
raise serializers.ValidationError(f"Unknown permission(s): {', '.join(unknown)}")
return value
def _validate_grant_scope(scope, request):
if scope != "global":
return
user = getattr(request, "user", None)
allowed = bool(user and getattr(user, "is_authenticated", False)) and (
getattr(user, "is_superuser", False) or AuthorizationService().has_permission(user, "platform.roles.manage")
)
if not allowed:
from rest_framework.exceptions import PermissionDenied
raise PermissionDenied("Global grants/revokes require platform.roles.manage.")
class GrantSerializer(serializers.ModelSerializer):
scope = serializers.ChoiceField(choices=["tenant", "global"], default="tenant", write_only=True)
class Meta:
model = Grant
fields = ["id", "user", "codename", "granted_by", "reason", "expires_at", "scope"]
read_only_fields = ["id", "granted_by"]
def validate(self, attrs):
_validate_grant_scope(attrs.get("scope", "tenant"), self.context.get("request"))
return attrs
def create(self, validated_data):
force_global = validated_data.pop("scope", "tenant") == "global"
grant = Grant(**validated_data)
if force_global:
grant.tenant = None
grant.save(force_global=True)
else:
grant.save()
return grant
class RevokeSerializer(serializers.ModelSerializer):
scope = serializers.ChoiceField(choices=["tenant", "global"], default="tenant", write_only=True)
class Meta:
model = Revoke
fields = ["id", "user", "codename", "revoked_by", "reason", "scope"]
read_only_fields = ["id", "revoked_by"]
def validate(self, attrs):
_validate_grant_scope(attrs.get("scope", "tenant"), self.context.get("request"))
return attrs
def create(self, validated_data):
force_global = validated_data.pop("scope", "tenant") == "global"
revoke = Revoke(**validated_data)
if force_global:
revoke.tenant = None
revoke.save(force_global=True)
else:
revoke.save()
return revoke
class APIKeySerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
name = serializers.CharField()
prefix = serializers.CharField(read_only=True)
key = serializers.CharField(read_only=True, help_text="Full secret, only shown once on creation")
scopes = serializers.ListField(child=serializers.CharField(), default=list)
is_active = serializers.BooleanField(default=True)
expires_at = serializers.DateTimeField(allow_null=True, required=False)
last_used_at = serializers.DateTimeField(read_only=True)
def create(self, validated_data):
return APIKey.objects.create(**validated_data)
def update(self, instance, validated_data):
for field, value in validated_data.items():
setattr(instance, field, value)
instance.save()
return instance
class LoginSerializer(serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField(write_only=True)
class TwoFactorSetupSerializer(serializers.Serializer):
secret = serializers.CharField(read_only=True)
qr_base64 = serializers.CharField(read_only=True)
provisioning_uri = serializers.CharField(read_only=True)
class TwoFactorVerifySerializer(serializers.Serializer):
code = serializers.CharField(max_length=10)
secret = serializers.CharField(required=False)
class TwoFactorRecoverySerializer(serializers.Serializer):
recovery_code = serializers.CharField()