From 6c85e8877a33fc202afa0aaf0f316e3b9d4cdeab Mon Sep 17 00:00:00 2001 From: stumpylog <797416+stumpylog@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:17:26 -0700 Subject: [PATCH] Chore: type GlobalPermissionsByMethod.perms_map keys as HTTP methods perms_map accepted any string key, so a misspelled method or a HEAD entry would never match. Keys are now a Literal of the mappable methods, without HEAD, and the map is typed read-only. --- src/documents/permissions.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/documents/permissions.py b/src/documents/permissions.py index 99247008e..c37eb5b77 100644 --- a/src/documents/permissions.py +++ b/src/documents/permissions.py @@ -1,7 +1,11 @@ +from collections.abc import Mapping +from collections.abc import Sequence from typing import Any from typing import ClassVar +from typing import Literal from typing import TypeGuard from typing import TypeVar +from typing import cast from django.contrib.auth.models import Group from django.contrib.auth.models import Permission @@ -702,6 +706,10 @@ def has_perms_owner_aware(user, perms, obj): return obj.owner is None or obj.owner == user or checker.has_perm(perms, obj) +# HEAD is deliberately absent: GlobalPermissionsByMethod checks it against GET. +MappedMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] + + class GlobalPermissionsByMethod(BasePermission): """ Requires the global (model-level) permissions listed in ``perms_map`` for @@ -710,14 +718,16 @@ class GlobalPermissionsByMethod(BasePermission): ``perms_map`` is denied. """ - perms_map: ClassVar[dict[str, list[str]]] + perms_map: ClassVar[Mapping[MappedMethod, Sequence[str]]] def has_permission(self, request: Any, view: Any) -> bool: if not request.user or not request.user.is_authenticated: return False method = "GET" if request.method == "HEAD" else request.method - perms = self.perms_map.get(method) + # An unknown method is not a MappedMethod, but .get() returns None for + # it, so it is still denied. + perms = self.perms_map.get(cast("MappedMethod", method)) return perms is not None and request.user.has_perms(perms)