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
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-16 14:44 +0000
1import hashlib
2import secrets
4from django.contrib.auth.models import User
5from django.db import models
6from django.utils import timezone
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="+")
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)
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)
28 def __str__(self):
29 return f"{self.name} ({self.owner})"
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
39 @staticmethod
40 def generate_raw_token():
41 return f"prs_{secrets.token_urlsafe(32)}"
43 @staticmethod
44 def hash_token(raw_token):
45 return hashlib.sha256(raw_token.encode()).hexdigest()
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