Source code for fairdm.api.permissions

"""FairDM API permission classes."""

from __future__ import annotations

from typing import TYPE_CHECKING, cast

from rest_framework.exceptions import NotAuthenticated, NotFound
from rest_framework.permissions import DjangoObjectPermissions

if TYPE_CHECKING:
    from rest_framework.request import Request
    from rest_framework.views import APIView


[docs] class FairDMObjectPermissions(DjangoObjectPermissions): """Object-level permission class for FairDM API endpoints. Extends ``DjangoObjectPermissions`` to: 1. Add ``view`` permissions to ``perms_map`` (required for guardian integration). 2. Return HTTP 404 (not 403) for objects the user cannot view — non-disclosure. 3. Allow unauthenticated read access for public data. 4. Require authentication for all write operations. Integration: - :class:`fairdm.api.filters.FairDMVisibilityFilter` handles queryset-level list filtering so private objects never appear in list results. - The serializers of projects, datasets, samples and measurements list the creator at the manage level on create. - Calls ``user.has_perm()`` which routes through ModelBackend, guardian ``ObjectPermissionBackend``, and any custom permission backends. For a project, dataset, sample or measurement the answer is the user's contribution level (``RecordLevelBackend``). """ perms_map = { "GET": ["%(app_label)s.view_%(model_name)s"], "OPTIONS": ["%(app_label)s.view_%(model_name)s"], "HEAD": ["%(app_label)s.view_%(model_name)s"], "POST": ["%(app_label)s.add_%(model_name)s"], "PUT": ["%(app_label)s.change_%(model_name)s"], "PATCH": ["%(app_label)s.change_%(model_name)s"], "DELETE": ["%(app_label)s.delete_%(model_name)s"], } authenticated_users_only = False
[docs] def has_permission(self, request: Request, view: APIView) -> bool: """Allow reads to anyone and writes to any signed-in user.""" if request.method in ("GET", "HEAD", "OPTIONS"): return True # Raised, not returned False, so DRF answers 401 whatever the authenticator. if not request.user or not request.user.is_authenticated: raise NotAuthenticated() # Model-level permissions are skipped on purpose: access is decided per object, # in has_object_permission, by the contribution level for a project, dataset, # sample or measurement and by guardian for any other model. return True
[docs] def has_object_permission(self, request: Request, view: APIView, obj) -> bool: """Return 404 (not 403) when user lacks permission on an existing object.""" if request.method in ("GET", "HEAD", "OPTIONS"): from fairdm.utils.choices import Visibility visibility = getattr(obj, "visibility", None) if visibility is not None and visibility == Visibility.PUBLIC: return True # Sample and Measurement are public when their dataset is public and published. if getattr(getattr(obj, "dataset", None), "data_is_public", False): return True if not request.user or not request.user.is_authenticated: raise NotFound() if not super().has_object_permission(request, view, obj): raise NotFound() return True if not request.user or not request.user.is_authenticated: return False from fairdm.utils.choices import Visibility # Visibility decides between "public + no write perm" (403) and "private + # no perm at all" (404). visibility = getattr(obj, "visibility", None) is_publicly_visible = ( visibility is not None and visibility == Visibility.PUBLIC ) or getattr(getattr(obj, "dataset", None), "data_is_public", False) # super().has_object_permission() raises Http404 when the user lacks both # write and read permission, which would hide the 403 a public object needs. # `method` is only None before the handler runs, so the cast is safe. write_perms = self.get_required_object_permissions( cast(str, request.method), obj.__class__ ) has_perm = all(request.user.has_perm(perm, obj) for perm in write_perms) if not has_perm and not is_publicly_visible: # A private object the user cannot even view must not be disclosed. view_perms = self.get_required_object_permissions("GET", obj.__class__) can_view = all(request.user.has_perm(perm, obj) for perm in view_perms) if not can_view: raise NotFound() return has_perm