Impersonation system implementation
This commit is contained in:
+151
-1
@@ -10,7 +10,7 @@ from django.urls import reverse
|
||||
|
||||
from library.models import FileIndex
|
||||
|
||||
from .models import BIO_MAX_LENGTH, ApiToken, UserProfile
|
||||
from .models import BIO_MAX_LENGTH, ApiToken, ImpersonationLog, UserProfile
|
||||
|
||||
|
||||
def _read_media(stored_path):
|
||||
@@ -423,3 +423,153 @@ class FileRequestTests(MediaTestCase, GatedTestCase):
|
||||
self.gate()
|
||||
resp = self.client.get(self.url)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
|
||||
class ImpersonationTests(GatedTestCase):
|
||||
"""Staff impersonation: switch users without logging out, PK-based
|
||||
staff-to-staff rule, superusers off-limits, gate state preserved."""
|
||||
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
# Created in this order → increasing PKs: boss(1) < junior(2) < normal(3) < top(4).
|
||||
self.boss = User.objects.create_user(username='Boss', password='pw', is_staff=True)
|
||||
UserProfile.objects.get_or_create(user=self.boss)
|
||||
self.junior = User.objects.create_user(username='JuniorStaff', password='pw', is_staff=True)
|
||||
UserProfile.objects.get_or_create(user=self.junior)
|
||||
self.normal = User.objects.create_user(username='NormalUser', password='pw')
|
||||
UserProfile.objects.get_or_create(user=self.normal)
|
||||
self.top = User.objects.create_superuser(username='TopSuper', password='pw')
|
||||
UserProfile.objects.get_or_create(user=self.top)
|
||||
|
||||
def _login(self, user):
|
||||
self.gate()
|
||||
self.client.login(username=user.username, password='pw')
|
||||
|
||||
def _start(self, user, **extra):
|
||||
return self.client.post(
|
||||
reverse('profiles:impersonate_start', args=[user.pk]), data=extra,
|
||||
)
|
||||
|
||||
@property
|
||||
def active_user_id(self):
|
||||
value = self.client.session.get('_auth_user_id')
|
||||
return int(value) if value else None
|
||||
|
||||
def test_users_page_gated(self):
|
||||
resp = self.client.get(reverse('profiles:user_list'))
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
def test_users_page_lists_all(self):
|
||||
self.gate()
|
||||
resp = self.client.get(reverse('profiles:user_list'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
for name in ('Boss', 'JuniorStaff', 'NormalUser', 'TopSuper'):
|
||||
self.assertContains(resp, name)
|
||||
|
||||
def test_users_page_search(self):
|
||||
self.gate()
|
||||
resp = self.client.get(reverse('profiles:user_list'), {'q': 'Normal'})
|
||||
self.assertContains(resp, 'NormalUser')
|
||||
self.assertNotContains(resp, 'Boss')
|
||||
|
||||
def test_impersonate_requires_post(self):
|
||||
self._login(self.boss)
|
||||
resp = self.client.get(reverse('profiles:impersonate_start', args=[self.normal.pk]))
|
||||
self.assertEqual(resp.status_code, 405)
|
||||
|
||||
def test_non_staff_cannot_impersonate(self):
|
||||
self._login(self.normal)
|
||||
resp = self._start(self.normal)
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
self.assertEqual(ImpersonationLog.objects.count(), 0)
|
||||
|
||||
def test_staff_impersonates_normal_user(self):
|
||||
self._login(self.boss)
|
||||
resp = self._start(self.normal, reason='moderation review')
|
||||
self.assertRedirects(resp, reverse('profiles:user_profile', args=['NormalUser']))
|
||||
self.assertEqual(self.active_user_id, self.normal.pk)
|
||||
self.assertEqual(self.client.session.get('impersonator_id'), self.boss.pk)
|
||||
self.assertTrue(self.client.session.get('authorized'))
|
||||
log = ImpersonationLog.objects.get()
|
||||
self.assertEqual(log.impersonator, self.boss)
|
||||
self.assertEqual(log.target, self.normal)
|
||||
self.assertEqual(log.reason, 'moderation review')
|
||||
self.assertIsNone(log.ended_at)
|
||||
|
||||
def test_gate_preserved_while_impersonating(self):
|
||||
self._login(self.boss)
|
||||
self._start(self.normal)
|
||||
# Follow-up page loads render (200) instead of bouncing to the gate.
|
||||
resp = self.client.get(reverse('profiles:user_profile', args=['NormalUser']))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_cannot_impersonate_self(self):
|
||||
self._login(self.boss)
|
||||
resp = self._start(self.boss)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(self.active_user_id, self.boss.pk)
|
||||
self.assertNotIn('impersonator_id', self.client.session)
|
||||
self.assertEqual(ImpersonationLog.objects.count(), 0)
|
||||
|
||||
def test_older_staff_can_impersonate_newer_staff(self):
|
||||
self._login(self.boss)
|
||||
self._start(self.junior)
|
||||
self.assertEqual(self.active_user_id, self.junior.pk)
|
||||
|
||||
def test_newer_staff_cannot_impersonate_older_staff(self):
|
||||
self._login(self.junior)
|
||||
self._start(self.boss)
|
||||
self.assertEqual(self.active_user_id, self.junior.pk)
|
||||
self.assertNotIn('impersonator_id', self.client.session)
|
||||
self.assertEqual(ImpersonationLog.objects.count(), 0)
|
||||
|
||||
def test_cannot_impersonate_superuser(self):
|
||||
self._login(self.boss)
|
||||
self._start(self.top)
|
||||
self.assertEqual(self.active_user_id, self.boss.pk)
|
||||
self.assertEqual(ImpersonationLog.objects.count(), 0)
|
||||
|
||||
def test_stop_impersonation_restores_original(self):
|
||||
self._login(self.boss)
|
||||
self._start(self.normal)
|
||||
resp = self.client.post(reverse('profiles:impersonate_stop'))
|
||||
self.assertRedirects(resp, reverse('landing:home'))
|
||||
self.assertEqual(self.active_user_id, self.boss.pk)
|
||||
self.assertNotIn('impersonator_id', self.client.session)
|
||||
self.assertTrue(self.client.session.get('authorized'))
|
||||
log = ImpersonationLog.objects.get()
|
||||
self.assertIsNotNone(log.ended_at)
|
||||
|
||||
def test_logout_while_impersonating_stops_instead(self):
|
||||
self._login(self.boss)
|
||||
self._start(self.normal)
|
||||
resp = self.client.post(reverse('profiles:logout'))
|
||||
self.assertRedirects(resp, reverse('landing:home'))
|
||||
# Back on the real account, still logged in, gate still open.
|
||||
self.assertEqual(self.active_user_id, self.boss.pk)
|
||||
self.assertNotIn('impersonator_id', self.client.session)
|
||||
self.assertTrue(self.client.session.get('authorized'))
|
||||
self.assertTrue(ImpersonationLog.objects.get().ended_at)
|
||||
# A second logout is a real logout.
|
||||
self.client.post(reverse('profiles:logout'))
|
||||
self.assertIsNone(self.active_user_id)
|
||||
|
||||
def test_impersonate_button_visibility(self):
|
||||
self._login(self.boss)
|
||||
resp = self.client.get(reverse('profiles:user_profile', args=['NormalUser']))
|
||||
self.assertContains(resp, 'Impersonate')
|
||||
# Self → no button.
|
||||
resp = self.client.get(reverse('profiles:user_profile', args=['Boss']))
|
||||
self.assertNotContains(resp, 'Impersonate')
|
||||
|
||||
def test_impersonate_button_hidden_for_non_staff(self):
|
||||
self._login(self.normal)
|
||||
resp = self.client.get(reverse('profiles:user_profile', args=['NormalUser']))
|
||||
self.assertNotContains(resp, 'Impersonate')
|
||||
|
||||
def test_impersonate_button_hidden_while_impersonating(self):
|
||||
self._login(self.boss)
|
||||
self._start(self.junior) # acting as a staff account now
|
||||
# Even though the impersonated account is staff, no impersonate button.
|
||||
resp = self.client.get(reverse('profiles:user_profile', args=['Boss']))
|
||||
self.assertNotContains(resp, 'Impersonate')
|
||||
|
||||
Reference in New Issue
Block a user