Skip to content
23 changes: 20 additions & 3 deletions ami/base/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@
from rest_framework import permissions

from ami.jobs.models import Job
from ami.main.models import BaseModel, Deployment, Device, Project, S3StorageSource, Site, SourceImageCollection
from ami.main.models import (
BaseModel,
Deployment,
Device,
Project,
S3StorageSource,
Site,
SourceImage,
SourceImageCollection,
SourceImageUpload,
)
from ami.users.roles import ProjectManager

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -79,11 +89,10 @@ def add_collection_level_permissions(user: User | None, response_data: dict, mod
"create" permission is added to the `user_permissions` set in the `response_data`.
"""

logger.info(f"add_collection_level_permissions model {model.__name__}, {type(model)} ")
logger.debug(f"add_collection_level_permissions model {model.__name__}, {type(model)} ")
permissions = response_data.get("user_permissions", set())
if user and user.is_superuser:
permissions.add("create")

if user and project and f"create_{model.__name__.lower()}" in get_perms(user, project):
permissions.add("create")
response_data["user_permissions"] = permissions
Expand Down Expand Up @@ -144,6 +153,14 @@ class SourceImageCollectionCRUDPermission(CRUDPermission):
model = SourceImageCollection


class SourceImageUploadCRUDPermission(CRUDPermission):
model = SourceImageUpload


class SourceImageCRUDPermission(CRUDPermission):
model = SourceImage


class CanStarSourceImage(permissions.BasePermission):
"""Custom permission to check if the user can star a Source image."""

Expand Down
2 changes: 2 additions & 0 deletions ami/main/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,7 @@ class SourceImageListSerializer(DefaultSerializer):
detections = CaptureDetectionsSerializer(many=True, read_only=True, source="filtered_detections")
deployment = DeploymentNestedSerializer(read_only=True)
event = EventNestedSerializer(read_only=True)
project = serializers.PrimaryKeyRelatedField(queryset=Project.objects.all(), required=False)
# file = serializers.ImageField(allow_empty_file=False, use_url=True)

class Meta:
Expand All @@ -946,6 +947,7 @@ class Meta:
"occurrences_count",
"taxa_count",
"detections",
"project",
]


Expand Down
10 changes: 7 additions & 3 deletions ami/main/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
S3StorageSourceCRUDPermission,
SiteCRUDPermission,
SourceImageCollectionCRUDPermission,
SourceImageCRUDPermission,
SourceImageUploadCRUDPermission,
)
from ami.base.serializers import FilterParamsSerializer, SingleParamSerializer
from ami.base.views import ProjectMixin
Expand Down Expand Up @@ -127,6 +129,7 @@ def create(self, request, *args, **kwargs):

# Create instance but do not save
instance = serializer.Meta.model(**serializer.validated_data)
logger.info(f"Creating {instance.__class__.__name__} with data: {serializer.validated_data}")
self.check_object_permissions(request, instance)
self.perform_create(serializer)
return Response(serializer.data, status=status.HTTP_201_CREATED)
Expand Down Expand Up @@ -445,7 +448,7 @@ def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)


class SourceImageViewSet(DefaultViewSet):
class SourceImageViewSet(DefaultViewSet, ProjectMixin):
"""
API endpoint that allows captures from monitoring sessions to be viewed or edited.

Expand Down Expand Up @@ -477,7 +480,7 @@ class SourceImageViewSet(DefaultViewSet):
"deployment__name",
"event__start",
]
permission_classes = [CanStarSourceImage]
permission_classes = [CanStarSourceImage, SourceImageCRUDPermission]

def get_serializer_class(self):
"""
Expand Down Expand Up @@ -746,14 +749,15 @@ def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)


class SourceImageUploadViewSet(DefaultViewSet):
class SourceImageUploadViewSet(DefaultViewSet, ProjectMixin):
"""
Endpoint for uploading images.
"""

queryset = SourceImageUpload.objects.all()

serializer_class = SourceImageUploadSerializer
permission_classes = [SourceImageUploadCRUDPermission]

def get_queryset(self) -> QuerySet:
# Only allow users to see their own uploads
Expand Down
54 changes: 54 additions & 0 deletions ami/main/migrations/0059_alter_project_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Generated by Django 4.2.10 on 2025-04-02 10:53

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("main", "0058_alter_project_options"),
]

operations = [
migrations.AlterModelOptions(
name="project",
options={
"ordering": ["-priority", "created_at"],
"permissions": [
("create_identification", "Can create identifications"),
("update_identification", "Can update identifications"),
("delete_identification", "Can delete identifications"),
("create_job", "Can create a job"),
("update_job", "Can update a job"),
("run_job", "Can run a job"),
("delete_job", "Can delete a job"),
("retry_job", "Can retry a job"),
("cancel_job", "Can cancel a job"),
("create_deployment", "Can create a deployment"),
("delete_deployment", "Can delete a deployment"),
("update_deployment", "Can update a deployment"),
("create_sourceimagecollection", "Can create a collection"),
("update_sourceimagecollection", "Can update a collection"),
("delete_sourceimagecollection", "Can delete a collection"),
("populate_sourceimagecollection", "Can populate a collection"),
("create_sourceimage", "Can create a source image"),
("update_sourceimage", "Can update a source image"),
("delete_sourceimage", "Can delete a source image"),
("star_sourceimage", "Can star a source image"),
("create_sourceimageupload", "Can create a source image upload"),
("update_sourceimageupload", "Can update a source image upload"),
("delete_sourceimageupload", "Can delete a source image upload"),
("create_s3storagesource", "Can create storage"),
("delete_s3storagesource", "Can delete storage"),
("update_s3storagesource", "Can update storage"),
("create_site", "Can create a site"),
("delete_site", "Can delete a site"),
("update_site", "Can update a site"),
("create_device", "Can create a device"),
("delete_device", "Can delete a device"),
("update_device", "Can update a device"),
("view_private_data", "Can view private data"),
("trigger_exports", "Can trigger data exports"),
],
},
),
]
18 changes: 18 additions & 0 deletions ami/main/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,15 @@ class Permissions:
POPULATE_COLLECTION = "populate_sourceimagecollection"

# Source Image permissions
CREATE_SOURCE_IMAGE = "create_sourceimage"
UPDATE_SOURCE_IMAGE = "update_sourceimage"
DELETE_SOURCE_IMAGE = "delete_sourceimage"
STAR_SOURCE_IMAGE = "star_sourceimage"

# SourceImageUpload permissions
CREATE_SOURCE_IMAGE_UPLOAD = "create_sourceimageupload"
UPDATE_SOURCE_IMAGE_UPLOAD = "update_sourceimageupload"
DELETE_SOURCE_IMAGE_UPLOAD = "delete_sourceimageupload"
# Storage permissions
CREATE_STORAGE = "create_s3storagesource"
DELETE_STORAGE = "delete_s3storagesource"
Expand Down Expand Up @@ -273,7 +280,14 @@ class Meta:
("delete_sourceimagecollection", "Can delete a collection"),
("populate_sourceimagecollection", "Can populate a collection"),
# Source Image permissions
("create_sourceimage", "Can create a source image"),
("update_sourceimage", "Can update a source image"),
("delete_sourceimage", "Can delete a source image"),
("star_sourceimage", "Can star a source image"),
# SourceImageUpload permissions
("create_sourceimageupload", "Can create a source image upload"),
("update_sourceimageupload", "Can update a source image upload"),
("delete_sourceimageupload", "Can delete a source image upload"),
# Storage permissions
("create_s3storagesource", "Can create storage"),
("delete_s3storagesource", "Can delete storage"),
Expand Down Expand Up @@ -1279,6 +1293,10 @@ class SourceImageUpload(BaseModel):
"SourceImage", on_delete=models.CASCADE, null=True, blank=True, related_name="upload"
)

def get_project(self):
"""Get the project associated with the model instance."""
return self.deployment.get_project()

def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# @TODO Use a "dirty" flag to mark the deployment as having new uploads, needs refresh
Expand Down
Loading