Coverage for sm/management/commands/cleanup_api_keys.py: 100%
30 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-24 19:06 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-24 19:06 +0000
1from datetime import timedelta
3from django.core.management.base import BaseCommand
4from django.utils import timezone
6from sm.models import ApiKey
9class Command(BaseCommand):
10 help = (
11 "Revoke API keys that have expired or have been inactive for too long. "
12 "Use --inactive-days to control the inactivity threshold (default 90)."
13 )
15 def add_arguments(self, parser):
16 parser.add_argument(
17 "--inactive-days",
18 type=int,
19 default=90,
20 help="Revoke keys unused for this many days (default: 90).",
21 )
22 parser.add_argument(
23 "--dry-run",
24 action="store_true",
25 help="Report what would be revoked without changing anything.",
26 )
28 def handle(self, *args, **options):
29 inactive_days = options["inactive_days"]
30 dry_run = options["dry_run"]
31 now = timezone.now()
33 expired = ApiKey.objects.filter(
34 is_active=True, expires_at__isnull=False, expires_at__lte=now
35 )
36 inactive = ApiKey.objects.filter(
37 is_active=True,
38 expires_at__isnull=True,
39 last_used_at__isnull=False,
40 last_used_at__lte=now - timedelta(days=inactive_days),
41 )
43 expired_count = expired.count()
44 inactive_count = inactive.count()
45 self.stdout.write(
46 self.style.WARNING(
47 "Found %d expired and %d inactive API key(s)."
48 % (expired_count, inactive_count)
49 )
50 )
52 if dry_run:
53 self.stdout.write("Dry run - nothing was revoked.")
54 return
56 for key in expired:
57 key.is_active = False
58 key.revoked_at = now
59 key.save(update_fields=["is_active", "revoked_at"])
60 for key in inactive:
61 key.is_active = False
62 key.revoked_at = now
63 key.save(update_fields=["is_active", "revoked_at"])
65 self.stdout.write(
66 self.style.SUCCESS(
67 "Revoked %d expired and %d inactive API key(s)."
68 % (expired_count, inactive_count)
69 )
70 )