Coverage for src/mesh/model/user/user_interfaces.py: 93%

48 statements  

« prev     ^ index     » next       coverage.py v7.7.0, created at 2025-04-28 07:45 +0000

1from dataclasses import asdict, dataclass, field 

2from datetime import datetime 

3from typing import ClassVar, Self 

4 

5from django.contrib.sessions.backends.base import SessionBase 

6 

7from mesh.models.user_models import User 

8 

9 

10@dataclass 

11class UserInfo: 

12 pk: int 

13 first_name: str 

14 last_name: str 

15 email: str 

16 

17 def __str__(self) -> str: 

18 return f"{self.first_name} {self.last_name} ({self.email})" 

19 

20 @classmethod 

21 def from_user(cls, user: User) -> Self: 

22 return cls( 

23 pk=user.pk, 

24 first_name=user.first_name, 

25 last_name=user.last_name, 

26 email=user.email, 

27 ) 

28 

29 

30@dataclass 

31class ImpersonateData: 

32 """ 

33 Interface for storing impersonation data. 

34 """ 

35 

36 _SESSION_KEY: ClassVar = "impersonate_data" 

37 # Max session duration in seconds 

38 _MAX_SESSION_DURATION: ClassVar = 10800 # 3 * 3600 

39 source: UserInfo 

40 target_id: int 

41 # Timestamp in seconds (use default datetime.timestamp())s 

42 timestamp_start: float | None = field(default=None) 

43 target_role: str | None = field(default=None) 

44 

45 def __post_init__(self): 

46 """ 

47 Dataclasses do not provide an effective fromdict method to deserialize 

48 a dataclass (JSON to python dataclass object). 

49 

50 This enables to effectively deserialize a JSON into a ImpersonateData object, 

51 by replacing the nested dict by their actual dataclass representation. 

52 Beware this might not work well with typing (?) 

53 """ 

54 source = self.source 

55 if source and not isinstance(source, UserInfo): 

56 self.source = UserInfo(**source) 

57 

58 if self.timestamp_start is None: 

59 self.timestamp_start = datetime.utcnow().timestamp() 

60 

61 @classmethod 

62 def from_session(cls, session: SessionBase) -> Self | None: 

63 """ 

64 Returns the impersonate data from the given session. 

65 """ 

66 data = session.get(cls._SESSION_KEY, None) 

67 if data: 

68 # In case the impersonate data is somehow corrupted. 

69 try: 

70 return cls(**data) 

71 except Exception: 

72 cls.clean_session(session) 

73 return None 

74 

75 @classmethod 

76 def clean_session(cls, session: SessionBase): 

77 """ 

78 Discards the impersonate data in the given session. 

79 """ 

80 if cls._SESSION_KEY in session: 80 ↛ exitline 80 didn't return from function 'clean_session' because the condition on line 80 was always true

81 del session[cls._SESSION_KEY] 

82 session.save() 

83 

84 def serialize(self, session: SessionBase): 

85 """ 

86 Serializes the ImpersonateData in the given session. 

87 """ 

88 session[self._SESSION_KEY] = asdict(self) 

89 

90 def is_valid(self) -> bool: 

91 """ 

92 Check if the impersonate data is still valid. 

93 """ 

94 return ( 

95 datetime.utcnow().timestamp() < self.timestamp_start + self._MAX_SESSION_DURATION # type:ignore 

96 )