Add purge_old_personal_api_key_events management command. Fixes #3144. Commit ready for merge.
- Legacy-Id: 18941
This commit is contained in:
parent
49779a3553
commit
475fb37c29
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
|
|
||||||
import factory
|
import factory
|
||||||
|
from factory.fuzzy import FuzzyChoice
|
||||||
import faker
|
import faker
|
||||||
import faker.config
|
import faker.config
|
||||||
import os
|
import os
|
||||||
|
@ -18,7 +19,7 @@ from django.utils.encoding import force_text
|
||||||
|
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
|
|
||||||
from ietf.person.models import Person, Alias, Email
|
from ietf.person.models import Person, Alias, Email, PersonalApiKey, PersonApiKeyEvent, PERSON_API_KEY_ENDPOINTS
|
||||||
from ietf.person.name import normalize_name, unidecode_name
|
from ietf.person.name import normalize_name, unidecode_name
|
||||||
|
|
||||||
|
|
||||||
|
@ -144,3 +145,20 @@ class EmailFactory(factory.DjangoModelFactory):
|
||||||
active = True
|
active = True
|
||||||
primary = False
|
primary = False
|
||||||
origin = factory.LazyAttribute(lambda obj: obj.person.user.username if obj.person.user else '')
|
origin = factory.LazyAttribute(lambda obj: obj.person.user.username if obj.person.user else '')
|
||||||
|
|
||||||
|
|
||||||
|
class PersonalApiKeyFactory(factory.DjangoModelFactory):
|
||||||
|
person = factory.SubFactory(PersonFactory)
|
||||||
|
endpoint = FuzzyChoice(PERSON_API_KEY_ENDPOINTS)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PersonalApiKey
|
||||||
|
|
||||||
|
class PersonApiKeyEventFactory(factory.DjangoModelFactory):
|
||||||
|
key = factory.SubFactory(PersonalApiKeyFactory)
|
||||||
|
person = factory.LazyAttribute(lambda o: o.key.person)
|
||||||
|
type = 'apikey_login'
|
||||||
|
desc = factory.Faker('sentence', nb_words=6)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PersonApiKeyEvent
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Copyright The IETF Trust 2021, All Rights Reserved
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.db.models import Max, Min
|
||||||
|
|
||||||
|
from ietf.person.models import PersonApiKeyEvent
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Purge PersonApiKeyEvent instances older than KEEP_DAYS days'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('keep_days', type=int,
|
||||||
|
help='Delete events older than this many days')
|
||||||
|
parser.add_argument('-n', '--dry-run', action='store_true', default=False,
|
||||||
|
help="Don't delete events, just show what would be done")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
keep_days = options['keep_days']
|
||||||
|
dry_run = options['dry_run']
|
||||||
|
|
||||||
|
def _format_count(count, unit='day'):
|
||||||
|
return '{} {}{}'.format(count, unit, ('' if count == 1 else 's'))
|
||||||
|
|
||||||
|
if keep_days < 0:
|
||||||
|
raise CommandError('Negative keep_days not allowed ({} was specified)'.format(keep_days))
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
self.stdout.write('Dry run requested, records will not be deleted\n')
|
||||||
|
|
||||||
|
self.stdout.write('Finding events older than {}\n'.format(_format_count(keep_days)))
|
||||||
|
self.stdout.flush()
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
old_events = PersonApiKeyEvent.objects.filter(
|
||||||
|
time__lt=now - timedelta(days=keep_days)
|
||||||
|
)
|
||||||
|
|
||||||
|
stats = old_events.aggregate(Min('time'), Max('time'))
|
||||||
|
old_count = old_events.count()
|
||||||
|
if old_count == 0:
|
||||||
|
self.stdout.write('No events older than {} found\n'.format(_format_count(keep_days)))
|
||||||
|
return
|
||||||
|
|
||||||
|
oldest_date = stats['time__min']
|
||||||
|
oldest_ago = now - oldest_date
|
||||||
|
newest_date = stats['time__max']
|
||||||
|
newest_ago = now - newest_date
|
||||||
|
|
||||||
|
action_fmt = 'Would delete {}\n' if dry_run else 'Deleting {}\n'
|
||||||
|
self.stdout.write(action_fmt.format(_format_count(old_count, 'event')))
|
||||||
|
self.stdout.write(' Oldest at {} ({} ago)\n'.format(oldest_date, _format_count(oldest_ago.days)))
|
||||||
|
self.stdout.write(' Most recent at {} ({} ago)\n'.format(newest_date, _format_count(newest_ago.days)))
|
||||||
|
self.stdout.flush()
|
||||||
|
|
||||||
|
if not dry_run:
|
||||||
|
old_events.delete()
|
121
ietf/person/management/commands/tests.py
Normal file
121
ietf/person/management/commands/tests.py
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
# Copyright The IETF Trust 2021, All Rights Reserved
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
from django.core.management import call_command, CommandError
|
||||||
|
|
||||||
|
from ietf.person.factories import PersonApiKeyEventFactory
|
||||||
|
from ietf.person.models import PersonApiKeyEvent, PersonEvent
|
||||||
|
from ietf.utils.test_utils import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
class CommandTests(TestCase):
|
||||||
|
@staticmethod
|
||||||
|
def _call_command(command_name, *args, **options):
|
||||||
|
out = StringIO()
|
||||||
|
options['stdout'] = out
|
||||||
|
call_command(command_name, *args, **options)
|
||||||
|
return out.getvalue()
|
||||||
|
|
||||||
|
def _assert_purge_results(self, cmd_output, expected_delete_count, expected_kept_events):
|
||||||
|
self.assertNotIn('Dry run requested', cmd_output)
|
||||||
|
if expected_delete_count == 0:
|
||||||
|
delete_text = 'No events older than'
|
||||||
|
else:
|
||||||
|
delete_text = 'Deleting {} event'.format(expected_delete_count)
|
||||||
|
self.assertIn(delete_text, cmd_output)
|
||||||
|
self.assertCountEqual(
|
||||||
|
PersonApiKeyEvent.objects.all(),
|
||||||
|
expected_kept_events,
|
||||||
|
'Wrong events were deleted'
|
||||||
|
)
|
||||||
|
|
||||||
|
def _assert_purge_dry_run_results(self, cmd_output, expected_delete_count, expected_kept_events):
|
||||||
|
self.assertIn('Dry run requested', cmd_output)
|
||||||
|
if expected_delete_count == 0:
|
||||||
|
delete_text = 'No events older than'
|
||||||
|
else:
|
||||||
|
delete_text = 'Would delete {} event'.format(expected_delete_count)
|
||||||
|
self.assertIn(delete_text, cmd_output)
|
||||||
|
self.assertCountEqual(
|
||||||
|
PersonApiKeyEvent.objects.all(),
|
||||||
|
expected_kept_events,
|
||||||
|
'Events were deleted when dry-run option was used'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_purge_old_personal_api_key_events(self):
|
||||||
|
keep_days = 10
|
||||||
|
|
||||||
|
# Remember how many PersonEvents were present so we can verify they're cleaned up properly.
|
||||||
|
personevents_before = PersonEvent.objects.count()
|
||||||
|
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
# The first of these events will be timestamped a fraction of a second more than keep_days
|
||||||
|
# days ago by the time we call the management command, so will just barely chosen for purge.
|
||||||
|
old_events = [
|
||||||
|
PersonApiKeyEventFactory(time=now - datetime.timedelta(days=n))
|
||||||
|
for n in range(keep_days, 2 * keep_days + 1)
|
||||||
|
]
|
||||||
|
num_old_events = len(old_events)
|
||||||
|
|
||||||
|
recent_events = [
|
||||||
|
PersonApiKeyEventFactory(time=now - datetime.timedelta(days=n))
|
||||||
|
for n in range(0, keep_days)
|
||||||
|
]
|
||||||
|
# We did not create recent_event timestamped exactly keep_days ago because it would
|
||||||
|
# be treated as an old_event by the management command. Create an event a few seconds
|
||||||
|
# on the "recent" side of keep_days old to test the threshold.
|
||||||
|
recent_events.append(
|
||||||
|
PersonApiKeyEventFactory(
|
||||||
|
time=now + datetime.timedelta(seconds=3) - datetime.timedelta(days=keep_days)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
num_recent_events = len(recent_events)
|
||||||
|
|
||||||
|
# call with dry run
|
||||||
|
output = self._call_command('purge_old_personal_api_key_events', str(keep_days), '--dry-run')
|
||||||
|
self._assert_purge_dry_run_results(output, num_old_events, old_events + recent_events)
|
||||||
|
|
||||||
|
# call for real
|
||||||
|
output = self._call_command('purge_old_personal_api_key_events', str(keep_days))
|
||||||
|
self._assert_purge_results(output, num_old_events, recent_events)
|
||||||
|
self.assertEqual(PersonEvent.objects.count(), personevents_before + num_recent_events,
|
||||||
|
'PersonEvents were not cleaned up properly')
|
||||||
|
|
||||||
|
# repeat - there should be nothing left to delete
|
||||||
|
output = self._call_command('purge_old_personal_api_key_events', '--dry-run', str(keep_days))
|
||||||
|
self._assert_purge_dry_run_results(output, 0, recent_events)
|
||||||
|
|
||||||
|
output = self._call_command('purge_old_personal_api_key_events', str(keep_days))
|
||||||
|
self._assert_purge_results(output, 0, recent_events)
|
||||||
|
self.assertEqual(PersonEvent.objects.count(), personevents_before + num_recent_events,
|
||||||
|
'PersonEvents were not cleaned up properly')
|
||||||
|
|
||||||
|
# and now delete the remaining events
|
||||||
|
output = self._call_command('purge_old_personal_api_key_events', '0')
|
||||||
|
self._assert_purge_results(output, num_recent_events, [])
|
||||||
|
self.assertEqual(PersonEvent.objects.count(), personevents_before,
|
||||||
|
'PersonEvents were not cleaned up properly')
|
||||||
|
|
||||||
|
def test_purge_old_personal_api_key_events_rejects_invalid_arguments(self):
|
||||||
|
"""The purge_old_personal_api_key_events command should reject invalid arguments"""
|
||||||
|
event = PersonApiKeyEventFactory(time=datetime.datetime.now() - datetime.timedelta(days=30))
|
||||||
|
|
||||||
|
with self.assertRaises(CommandError):
|
||||||
|
self._call_command('purge_old_personal_api_key_events')
|
||||||
|
|
||||||
|
with self.assertRaises(CommandError):
|
||||||
|
self._call_command('purge_old_personal_api_key_events', '-15')
|
||||||
|
|
||||||
|
with self.assertRaises(CommandError):
|
||||||
|
self._call_command('purge_old_personal_api_key_events', '15.3')
|
||||||
|
|
||||||
|
with self.assertRaises(CommandError):
|
||||||
|
self._call_command('purge_old_personal_api_key_events', '15', '15')
|
||||||
|
|
||||||
|
with self.assertRaises(CommandError):
|
||||||
|
self._call_command('purge_old_personal_api_key_events', 'abc', '15')
|
||||||
|
|
||||||
|
self.assertCountEqual(PersonApiKeyEvent.objects.all(), [event])
|
Loading…
Reference in a new issue