- 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)
139 lines
4.7 KiB
Python
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()
|