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,
|
||||
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)
|
||||
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.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 (
|
||||
MediaFileSerializer,
|
||||
MediaFileUploadSerializer,
|
||||
|
|
@ -30,7 +31,18 @@ class MediaFileViewSet(viewsets.ModelViewSet):
|
|||
queryset = MediaFile.objects.all()
|
||||
serializer_class = MediaFileSerializer
|
||||
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):
|
||||
if self.action == 'create':
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from django.views.decorators.csrf import csrf_exempt
|
|||
from django.utils.decorators import method_decorator
|
||||
|
||||
from files.models import SharedSpace, MediaFile, PublicShare
|
||||
from config.encryptid_auth import check_space_access
|
||||
|
||||
|
||||
def get_topic_or_404(request):
|
||||
|
|
@ -54,11 +55,19 @@ class SharedSpaceHomeView(View):
|
|||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
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):
|
||||
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'):
|
||||
return JsonResponse({'error': 'No file provided'}, status=400)
|
||||
|
||||
|
|
@ -137,6 +146,7 @@ class DirectUploadAPIView(View):
|
|||
"""Handle uploads via direct.rfiles.online (bypasses Cloudflare).
|
||||
|
||||
Space slug is passed as a form field instead of from the subdomain.
|
||||
Requires auth for non-public spaces.
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
|
|
@ -146,6 +156,14 @@ class DirectUploadAPIView(View):
|
|||
|
||||
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'):
|
||||
return JsonResponse({'error': 'No file provided'}, status=400)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue