Source code for fairdm.contrib.plugins.access

"""One access decision, consulted by both navigation and dispatch through :func:`can_open`."""

from __future__ import annotations

import inspect
import logging
from collections.abc import Callable
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from django.db.models import Model
    from django.http import HttpRequest

logger = logging.getLogger(__name__)

_MEMO_ATTR = "_fairdm_plugin_perm_cache"


[docs] def is_instance_of(*model_classes: type[Model]) -> Callable[..., bool]: """Return a predicate that passes when the record is one of ``model_classes``. A record of ``None`` passes, so the plugin is admitted where no record is in hand. Args: *model_classes: The accepted model classes. Returns: A ``check(request, obj)`` predicate. Example: Narrow a plugin to one subtype of a polymorphic record:: class RockAnalysis(Plugin, FairDMTemplateView): check = staticmethod(is_instance_of(RockSample)) """ def check(request: HttpRequest, obj: Model | None) -> bool: """Pass when there is no record or it is one of the model classes.""" if obj is None: return True return isinstance(obj, model_classes) return check
[docs] def has_perm(request: HttpRequest, permission: str, obj: Model | None = None) -> bool: """Resolve ``permission`` for the request's user, memoised for the life of the request. Args: request: The current request. permission: The permission codename, with app label. obj: The record to check against, if any. Returns: True when the user holds the permission globally or on the record. """ cache: dict[tuple[Any, ...], bool] = getattr(request, _MEMO_ATTR, None) if cache is None: cache = {} setattr(request, _MEMO_ATTR, cache) # Keyed by label and pk, never `id()`, which CPython reuses for short-lived objects. if obj is None: key: tuple[Any, ...] = (permission, None) else: key = (permission, obj._meta.label, obj.pk) if key not in cache: user = request.user # With an object, ModelBackend contributes nothing, so a global grant needs its own check. granted = user.has_perm(permission) if not granted and obj is not None: granted = user.has_perm(permission, obj) cache[key] = granted return cache[key]
#: The action each level stands for, as it appears in a core record's permission names. LEVEL_ACTIONS = {"view": "view", "edit": "change", "manage": "delete"}
[docs] def holds_level(request: HttpRequest, level: str, obj: Model | None) -> bool: """Say whether the viewer holds a level on a project, dataset, sample or measurement. The level is asked as the permission the record's own pages are opened with: ``view_<model>``, ``change_<model>`` or ``delete_<model>``, resolved by :func:`has_perm`. So a level held on a record above counts, and so does the permission held for the whole portal. A visitor holds no level, on a public record as on a private one: the question is who is on the record's team, not who may read it. Args: request: The current request. level: ``"view"``, ``"edit"`` or ``"manage"``. obj: The record asked about. Returns: True when the viewer holds the level or a higher one. False when there is no record, or it is not a project, dataset, sample or measurement. Raises: ValueError: ``level`` is not one of the three. """ from fairdm.contrib.contributors.access import RecordAccess if level not in LEVEL_ACTIONS: msg = f"{level!r} is not a level; use one of {', '.join(LEVEL_ACTIONS)}" raise ValueError(msg) if not RecordAccess.is_core_record(obj): return False meta = RecordAccess(obj).model._meta permission = f"{meta.app_label}.{LEVEL_ACTIONS[level]}_{meta.model_name}" return has_perm(request, permission, obj)
[docs] def resolve_check(view_class: type) -> Callable[..., bool] | bool: """Read a view's predicate as declared, without invoking the descriptor protocol. Args: view_class: The view class. Returns: The ``check`` attribute, or True when the view declares none. """ return inspect.getattr_static(view_class, "check", True)
[docs] def check_is_valid(check: Any) -> bool: """Report whether ``check`` is a bool or a callable that :func:`can_open` can evaluate. A ``classmethod`` object is neither but is truthy, so it would publish a page meant to be hidden. Args: check: The value read from a view's ``check`` attribute. Returns: True when the value is a bool or callable. """ return isinstance(check, bool) or callable(check)
[docs] def can_open( view_class: type, request: HttpRequest, obj: Model | None = None, ) -> bool: """Decide whether the user may open this view for this record. A view answers for itself: only its own ``check`` and ``permission`` are read. An additional view listed in a plugin's ``extra_views`` takes nothing from that plugin, so it states its own rule or stays open while the plugin is refused. Args: view_class: The plugin or additional view class. request: The current request. obj: The record the page belongs to, if any. Returns: True when the predicate and every required permission pass. """ owner = getattr(view_class, "plugin_class", None) or view_class check = resolve_check(owner) if callable(check): if not check(request, obj): return False elif not check: return False permission = getattr(view_class, "permission", None) if permission: permissions = [permission] if isinstance(permission, str) else list(permission) return all(has_perm(request, perm, obj) for perm in permissions) return True