FossilRepo

fossilrepo / fossil / api_tokens.py
Blame History Raw 71 lines
1
"""API tokens scoped to a repository for CI/CD and automation.
2
3
Tokens are stored as SHA-256 hashes -- the raw value is shown once on creation
4
and never stored in plaintext.
5
"""
6
7
import hashlib
8
import secrets
9
10
from django.db import models
11
from django.utils import timezone
12
13
from core.models import ActiveManager, Tracking
14
15
16
class APIToken(Tracking):
17
"""API token scoped to a repository for CI/CD and automation."""
18
19
repository = models.ForeignKey("fossil.FossilRepository", on_delete=models.CASCADE, related_name="api_tokens")
20
name = models.CharField(max_length=200)
21
token_hash = models.CharField(max_length=64, unique=True, help_text="SHA-256 hash of the token")
22
token_prefix = models.CharField(max_length=12, help_text="First 12 chars for identification")
23
permissions = models.CharField(max_length=200, default="status:write", help_text="Comma-separated permissions")
24
expires_at = models.DateTimeField(null=True, blank=True)
25
last_used_at = models.DateTimeField(null=True, blank=True)
26
27
objects = ActiveManager()
28
all_objects = models.Manager()
29
30
class Meta:
31
ordering = ["-created_at"]
32
33
@staticmethod
34
def generate():
35
"""Generate a new token. Returns (raw_token, token_hash, prefix)."""
36
raw = f"frp_{secrets.token_urlsafe(32)}"
37
hash_val = hashlib.sha256(raw.encode()).hexdigest()
38
prefix = raw[:12]
39
return raw, hash_val, prefix
40
41
@staticmethod
42
def hash_token(raw_token):
43
return hashlib.sha256(raw_token.encode()).hexdigest()
44
45
def has_permission(self, permission):
46
"""Check if this token has a specific permission."""
47
perms = [p.strip() for p in self.permissions.split(",")]
48
return permission in perms or "*" in perms
49
50
def __str__(self):
51
return f"{self.name} ({self.token_prefix}...)"
52
53
54
def authenticate_api_token(request, repository):
55
"""Check Bearer token auth. Returns APIToken or None."""
56
auth = request.META.get("HTTP_AUTHORIZATION", "")
57
if not auth.startswith("Bearer "):
58
return None
59
raw_token = auth[7:]
60
token_hash = APIToken.hash_token(raw_token)
61
try:
62
token = APIToken.objects.get(token_hash=token_hash, repository=repository, deleted_at__isnull=True)
63
# Check expiry
64
if token.expires_at and token.expires_at < timezone.now():
65
return None
66
token.last_used_at = timezone.now()
67
token.save(update_fields=["last_used_at"])
68
return token
69
except APIToken.DoesNotExist:
70
return None
71

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button