component_framework.adapters.django_permissions

Django FBV decorators for component access control.

These decorators always return JSON responses (401/403), never redirects, making them safe for HTMX/fetch consumers.

  1"""Django FBV decorators for component access control.
  2
  3These decorators always return JSON responses (401/403), never redirects,
  4making them safe for HTMX/fetch consumers.
  5"""
  6
  7import functools
  8
  9try:
 10    from django.http import HttpRequest, JsonResponse
 11except ImportError as e:
 12    from . import _require_extra
 13
 14    raise _require_extra("django", "django") from e
 15
 16
 17def login_required_component(view_func):
 18    """
 19    Decorator that requires the user to be authenticated.
 20
 21    Returns a JSON 401 response (never a redirect) for unauthenticated requests.
 22
 23    Usage::
 24
 25        from component_framework.adapters.django_permissions import login_required_component
 26
 27        urlpatterns = [
 28            path("components/<str:name>/", login_required_component(component_view)),
 29        ]
 30    """
 31
 32    @functools.wraps(view_func)
 33    def wrapper(request: HttpRequest, *args, **kwargs):
 34        user = getattr(request, "user", None)
 35        if not user or not user.is_authenticated:
 36            return JsonResponse({"error": "Authentication required"}, status=401)
 37        return view_func(request, *args, **kwargs)
 38
 39    return wrapper
 40
 41
 42def permission_required_component(perm: str | list[str]):
 43    """
 44    Decorator factory that requires the user to hold specific Django permissions.
 45
 46    Returns a JSON 403 response (never a redirect) if permission is denied.
 47
 48    Args:
 49        perm: A permission string (e.g. ``"myapp.change_model"``) or list of
 50              permission strings. When a list is given, the user must hold *all*
 51              permissions.
 52
 53    Usage::
 54
 55        from component_framework.adapters.django_permissions import permission_required_component
 56
 57        urlpatterns = [
 58            path(
 59                "components/<str:name>/",
 60                permission_required_component("myapp.change_model")(component_view),
 61            ),
 62        ]
 63    """
 64    perms: list[str] = [perm] if isinstance(perm, str) else list(perm)
 65
 66    def decorator(view_func):
 67        @functools.wraps(view_func)
 68        def wrapper(request: HttpRequest, *args, **kwargs):
 69            user = getattr(request, "user", None)
 70            if not user or not user.is_authenticated:
 71                return JsonResponse({"error": "Authentication required"}, status=401)
 72            if not all(user.has_perm(p) for p in perms):
 73                return JsonResponse({"error": "Permission denied"}, status=403)
 74            return view_func(request, *args, **kwargs)
 75
 76        return wrapper
 77
 78    return decorator
 79
 80
 81def staff_required_component(view_func):
 82    """
 83    Decorator that requires the user to be a staff member.
 84
 85    Returns a JSON 403 response (never a redirect) for non-staff requests.
 86
 87    Usage::
 88
 89        from component_framework.adapters.django_permissions import staff_required_component
 90
 91        urlpatterns = [
 92            path("components/<str:name>/", staff_required_component(component_view)),
 93        ]
 94    """
 95
 96    @functools.wraps(view_func)
 97    def wrapper(request: HttpRequest, *args, **kwargs):
 98        user = getattr(request, "user", None)
 99        if not user or not user.is_authenticated:
100            return JsonResponse({"error": "Authentication required"}, status=401)
101        if not user.is_staff:
102            return JsonResponse({"error": "Staff access required"}, status=403)
103        return view_func(request, *args, **kwargs)
104
105    return wrapper
def login_required_component(view_func):
18def login_required_component(view_func):
19    """
20    Decorator that requires the user to be authenticated.
21
22    Returns a JSON 401 response (never a redirect) for unauthenticated requests.
23
24    Usage::
25
26        from component_framework.adapters.django_permissions import login_required_component
27
28        urlpatterns = [
29            path("components/<str:name>/", login_required_component(component_view)),
30        ]
31    """
32
33    @functools.wraps(view_func)
34    def wrapper(request: HttpRequest, *args, **kwargs):
35        user = getattr(request, "user", None)
36        if not user or not user.is_authenticated:
37            return JsonResponse({"error": "Authentication required"}, status=401)
38        return view_func(request, *args, **kwargs)
39
40    return wrapper

Decorator that requires the user to be authenticated.

Returns a JSON 401 response (never a redirect) for unauthenticated requests.

Usage::

from component_framework.adapters.django_permissions import login_required_component

urlpatterns = [
    path("components/<str:name>/", login_required_component(component_view)),
]
def permission_required_component(perm: str | list[str]):
43def permission_required_component(perm: str | list[str]):
44    """
45    Decorator factory that requires the user to hold specific Django permissions.
46
47    Returns a JSON 403 response (never a redirect) if permission is denied.
48
49    Args:
50        perm: A permission string (e.g. ``"myapp.change_model"``) or list of
51              permission strings. When a list is given, the user must hold *all*
52              permissions.
53
54    Usage::
55
56        from component_framework.adapters.django_permissions import permission_required_component
57
58        urlpatterns = [
59            path(
60                "components/<str:name>/",
61                permission_required_component("myapp.change_model")(component_view),
62            ),
63        ]
64    """
65    perms: list[str] = [perm] if isinstance(perm, str) else list(perm)
66
67    def decorator(view_func):
68        @functools.wraps(view_func)
69        def wrapper(request: HttpRequest, *args, **kwargs):
70            user = getattr(request, "user", None)
71            if not user or not user.is_authenticated:
72                return JsonResponse({"error": "Authentication required"}, status=401)
73            if not all(user.has_perm(p) for p in perms):
74                return JsonResponse({"error": "Permission denied"}, status=403)
75            return view_func(request, *args, **kwargs)
76
77        return wrapper
78
79    return decorator

Decorator factory that requires the user to hold specific Django permissions.

Returns a JSON 403 response (never a redirect) if permission is denied.

Arguments:
  • perm: A permission string (e.g. "myapp.change_model") or list of permission strings. When a list is given, the user must hold all permissions.

Usage::

from component_framework.adapters.django_permissions import permission_required_component

urlpatterns = [
    path(
        "components/<str:name>/",
        permission_required_component("myapp.change_model")(component_view),
    ),
]
def staff_required_component(view_func):
 82def staff_required_component(view_func):
 83    """
 84    Decorator that requires the user to be a staff member.
 85
 86    Returns a JSON 403 response (never a redirect) for non-staff requests.
 87
 88    Usage::
 89
 90        from component_framework.adapters.django_permissions import staff_required_component
 91
 92        urlpatterns = [
 93            path("components/<str:name>/", staff_required_component(component_view)),
 94        ]
 95    """
 96
 97    @functools.wraps(view_func)
 98    def wrapper(request: HttpRequest, *args, **kwargs):
 99        user = getattr(request, "user", None)
100        if not user or not user.is_authenticated:
101            return JsonResponse({"error": "Authentication required"}, status=401)
102        if not user.is_staff:
103            return JsonResponse({"error": "Staff access required"}, status=403)
104        return view_func(request, *args, **kwargs)
105
106    return wrapper

Decorator that requires the user to be a staff member.

Returns a JSON 403 response (never a redirect) for non-staff requests.

Usage::

from component_framework.adapters.django_permissions import staff_required_component

urlpatterns = [
    path("components/<str:name>/", staff_required_component(component_view)),
]