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

1from __future__ import annotations 

2 

3import logging 

4from typing import Any 

5 

6import jwt 

7from django.conf import settings 

8from rest_framework.permissions import BasePermission 

9 

10 

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 

15 

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 

21 

22 

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() 

27 

28 return {scope for scope in raw_scopes.split() if scope} 

29 

30 

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 

34 

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" 

40 

41 return None 

42 

43 

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) 

47 

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 

51 

52 if access_token is None: 

53 return settings.KEYCLOAK_ALLOW_MISSING_ACCESS_TOKEN 

54 

55 token_scopes = _get_token_scopes(access_token) 

56 assigned_permission = _resolve_assigned_permission_scope(token_scopes) 

57 return assigned_permission == required_permission 

58 

59 

60class HasHumanUserAccess(BasePermission): 

61 def has_permission(self, request, view): 

62 return _has_assigned_permission(request, required_permission="human_user") 

63 

64 

65class HasExcelClientAccess(BasePermission): 

66 def has_permission(self, request, view): 

67 return _has_assigned_permission(request, required_permission="excel_client")