Coverage for backend/django/authentication/permissions.py: 100%
44 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-13 02:47 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-13 02:47 +0000
1from __future__ import annotations
3import logging
4from typing import Any
6import jwt
7from django.conf import settings
8from rest_framework.permissions import BasePermission
11def _decode_proxy_access_token(request) -> dict[str, Any] | None:
12 access_token: str | None = request.META.get(settings.REMOTE_USER_ACCESS_TOKEN_HEADER)
13 if not access_token:
14 return None
16 try:
17 return jwt.decode(access_token, options={"verify_signature": False})
18 except jwt.InvalidTokenError as err:
19 logging.warning("Failed to decode proxy access token: %s", err)
20 return None
23def _get_token_scopes(access_token: dict[str, Any]) -> set[str]:
24 raw_scopes = access_token.get("scope")
25 if not isinstance(raw_scopes, str):
26 return set()
28 return {scope for scope in raw_scopes.split() if scope}
31def _resolve_assigned_permission_scope(token_scopes: set[str]) -> str | None:
32 general_scope = settings.AUTH_GENERAL_SCOPE_KEY
33 excel_scope = settings.AUTH_EXCEL_SCOPE_KEY
35 # ExcelClient takes priority if both configured scopes are present.
36 if excel_scope and excel_scope in token_scopes:
37 return "excel_client"
38 if general_scope and general_scope in token_scopes:
39 return "human_user"
41 return None
44def _has_assigned_permission(request, required_permission: str) -> bool:
45 has_token_header = bool(request.META.get(settings.REMOTE_USER_ACCESS_TOKEN_HEADER))
46 access_token = _decode_proxy_access_token(request)
48 # Enforce strict checks for malformed/invalid token values when header is present.
49 if has_token_header and access_token is None:
50 return False
52 if access_token is None:
53 return settings.KEYCLOAK_ALLOW_MISSING_ACCESS_TOKEN
55 token_scopes = _get_token_scopes(access_token)
56 assigned_permission = _resolve_assigned_permission_scope(token_scopes)
57 return assigned_permission == required_permission
60class HasHumanUserAccess(BasePermission):
61 def has_permission(self, request, view):
62 return _has_assigned_permission(request, required_permission="human_user")
65class HasExcelClientAccess(BasePermission):
66 def has_permission(self, request, view):
67 return _has_assigned_permission(request, required_permission="excel_client")