Coverage for product_risk_suite/api/models.py: 98%

42 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-16 14:44 +0000

1import hashlib 

2import secrets 

3 

4from django.contrib.auth.models import User 

5from django.db import models 

6from django.utils import timezone 

7 

8 

9class APIToken(models.Model): 

10 name = models.CharField(max_length=100, help_text="A recognizable label, e.g. 'Jenkins CI'.") 

11 owner = models.ForeignKey( 

12 User, 

13 related_name="api_tokens", 

14 on_delete=models.CASCADE, 

15 help_text="The user whose permissions (including guardian object permissions) this token uses. " 

16 "Can be an existing person's account or a dedicated service account created for this token.", 

17 ) 

18 created_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name="+") 

19 

20 display_prefix = models.CharField(max_length=16, editable=False) 

21 hashed_key = models.CharField(max_length=64, unique=True, editable=False, db_index=True) 

22 

23 created_at = models.DateTimeField(auto_now_add=True) 

24 last_used_at = models.DateTimeField(null=True, blank=True) 

25 expires_at = models.DateTimeField(null=True, blank=True) 

26 is_active = models.BooleanField(default=True) 

27 

28 def __str__(self): 

29 return f"{self.name} ({self.owner})" 

30 

31 @property 

32 def is_valid(self): 

33 if not self.is_active: 

34 return False 

35 if self.expires_at is not None and self.expires_at <= timezone.now(): 

36 return False 

37 return True 

38 

39 @staticmethod 

40 def generate_raw_token(): 

41 return f"prs_{secrets.token_urlsafe(32)}" 

42 

43 @staticmethod 

44 def hash_token(raw_token): 

45 return hashlib.sha256(raw_token.encode()).hexdigest() 

46 

47 @classmethod 

48 def authenticate(cls, raw_token): 

49 if not raw_token: 

50 return None 

51 try: 

52 token = cls.objects.select_related("owner").get(hashed_key=cls.hash_token(raw_token)) 

53 except cls.DoesNotExist: 

54 return None 

55 if not token.is_valid: 

56 return None 

57 cls.objects.filter(pk=token.pk).update(last_used_at=timezone.now()) 

58 return token