feat: enforce space visibility on uploads and API views
Add visibility and owner_did fields to SharedSpace model. Protect upload endpoints with check_space_access(). Add SpacePermission DRF permission class to MediaFileViewSet. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e8ad0f7f31
commit
284a73d3fa
|
|
@ -40,6 +40,26 @@ class SharedSpace(models.Model):
|
||||||
default=0,
|
default=0,
|
||||||
help_text="Maximum file size in MB for uploads (0 = unlimited)"
|
help_text="Maximum file size in MB for uploads (0 = unlimited)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
VISIBILITY_CHOICES = [
|
||||||
|
('public', 'Public'),
|
||||||
|
('public_read', 'Public Read'),
|
||||||
|
('authenticated', 'Authenticated'),
|
||||||
|
('members_only', 'Members Only'),
|
||||||
|
]
|
||||||
|
visibility = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=VISIBILITY_CHOICES,
|
||||||
|
default='public_read',
|
||||||
|
help_text="Who can access this space"
|
||||||
|
)
|
||||||
|
owner_did = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
blank=True,
|
||||||
|
default='',
|
||||||
|
help_text="EncryptID DID of the space owner"
|
||||||
|
)
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||||
|
|
||||||
from .models import MediaFile, PublicShare, FileAccessLog
|
from .models import MediaFile, PublicShare, FileAccessLog, SharedSpace
|
||||||
|
from config.encryptid_auth import SpacePermission
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
MediaFileSerializer,
|
MediaFileSerializer,
|
||||||
MediaFileUploadSerializer,
|
MediaFileUploadSerializer,
|
||||||
|
|
@ -30,7 +31,18 @@ class MediaFileViewSet(viewsets.ModelViewSet):
|
||||||
queryset = MediaFile.objects.all()
|
queryset = MediaFile.objects.all()
|
||||||
serializer_class = MediaFileSerializer
|
serializer_class = MediaFileSerializer
|
||||||
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||||||
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
permission_classes = [permissions.IsAuthenticatedOrReadOnly, SpacePermission]
|
||||||
|
|
||||||
|
def get_space_config(self, request):
|
||||||
|
"""Provide space config for SpacePermission check."""
|
||||||
|
space_slug = request.query_params.get('space')
|
||||||
|
if not space_slug:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
space = SharedSpace.objects.get(slug=space_slug, is_active=True)
|
||||||
|
return {'visibility': space.visibility, 'owner_did': space.owner_did}
|
||||||
|
except SharedSpace.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
if self.action == 'create':
|
if self.action == 'create':
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
from files.models import SharedSpace, MediaFile, PublicShare
|
from files.models import SharedSpace, MediaFile, PublicShare
|
||||||
|
from config.encryptid_auth import check_space_access
|
||||||
|
|
||||||
|
|
||||||
def get_topic_or_404(request):
|
def get_topic_or_404(request):
|
||||||
|
|
@ -54,11 +55,19 @@ class SharedSpaceHomeView(View):
|
||||||
|
|
||||||
@method_decorator(csrf_exempt, name='dispatch')
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
class SharedSpaceUploadAPIView(View):
|
class SharedSpaceUploadAPIView(View):
|
||||||
"""Handle file uploads via AJAX for topic."""
|
"""Handle file uploads via AJAX for topic. Requires auth for non-public spaces."""
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
space = get_topic_or_404(request)
|
space = get_topic_or_404(request)
|
||||||
|
|
||||||
|
# Check space visibility — uploads are always writes
|
||||||
|
access = check_space_access(request, {
|
||||||
|
'visibility': space.visibility,
|
||||||
|
'owner_did': space.owner_did,
|
||||||
|
})
|
||||||
|
if not access['allowed']:
|
||||||
|
return JsonResponse({'error': access['reason']}, status=401)
|
||||||
|
|
||||||
if not request.FILES.get('file'):
|
if not request.FILES.get('file'):
|
||||||
return JsonResponse({'error': 'No file provided'}, status=400)
|
return JsonResponse({'error': 'No file provided'}, status=400)
|
||||||
|
|
||||||
|
|
@ -137,6 +146,7 @@ class DirectUploadAPIView(View):
|
||||||
"""Handle uploads via direct.rfiles.online (bypasses Cloudflare).
|
"""Handle uploads via direct.rfiles.online (bypasses Cloudflare).
|
||||||
|
|
||||||
Space slug is passed as a form field instead of from the subdomain.
|
Space slug is passed as a form field instead of from the subdomain.
|
||||||
|
Requires auth for non-public spaces.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
|
@ -146,6 +156,14 @@ class DirectUploadAPIView(View):
|
||||||
|
|
||||||
space = get_object_or_404(SharedSpace, slug=space_slug, is_active=True)
|
space = get_object_or_404(SharedSpace, slug=space_slug, is_active=True)
|
||||||
|
|
||||||
|
# Check space visibility — uploads are always writes
|
||||||
|
access = check_space_access(request, {
|
||||||
|
'visibility': space.visibility,
|
||||||
|
'owner_did': space.owner_did,
|
||||||
|
})
|
||||||
|
if not access['allowed']:
|
||||||
|
return JsonResponse({'error': access['reason']}, status=401)
|
||||||
|
|
||||||
if not request.FILES.get('file'):
|
if not request.FILES.get('file'):
|
||||||
return JsonResponse({'error': 'No file provided'}, status=400)
|
return JsonResponse({'error': 'No file provided'}, status=400)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue