Add auditable superuser mode (#824)

* Remove is_superuser everywhere

* Session handling

* List of sessions, relative timeout

* Absolute timeout

* Optionally pseudo-force audit comments

* Fix failing tests

* Add tests

* Add docs

* Rebsae migration

* Typos

* Fix tests
This commit is contained in:
Raphael Michel
2018-03-28 14:16:58 +02:00
committed by GitHub
parent 558c920181
commit a284e0c2f7
56 changed files with 965 additions and 130 deletions

View File

@@ -1,7 +1,9 @@
import pytest
from django.test import RequestFactory
from django.utils.timezone import now
from pretix.base.models import Event, Organizer, Team, User
from pretix.multidomain.middlewares import SessionMiddleware
@pytest.fixture
@@ -25,7 +27,19 @@ def user():
@pytest.fixture
def admin():
return User.objects.create_user('admin@dummy.dummy', 'dummy', is_superuser=True)
u = User.objects.create_user('admin@dummy.dummy', 'dummy', is_staff=True)
return u
@pytest.fixture
def admin_request(admin, client):
factory = RequestFactory()
r = factory.get('/')
SessionMiddleware().process_request(r)
r.session.save()
admin.staffsession_set.create(date_start=now(), session_key=r.session.session_key)
admin.staffsession_set.create(date_start=now(), session_key=client.session.session_key)
return r
@pytest.mark.django_db
@@ -193,23 +207,20 @@ def test_organizer_permissions_multiple_teams(event, user):
@pytest.mark.django_db
def test_superuser(event, user):
user.is_superuser = True
user.save()
def test_superuser(event, admin, admin_request):
assert admin.has_organizer_permission(event.organizer, request=admin_request)
assert admin.has_organizer_permission(event.organizer, 'can_create_events', request=admin_request)
assert admin.has_event_permission(event.organizer, event, request=admin_request)
assert admin.has_event_permission(event.organizer, event, 'can_change_event_settings', request=admin_request)
assert user.has_organizer_permission(event.organizer)
assert user.has_organizer_permission(event.organizer, 'can_create_events')
assert user.has_event_permission(event.organizer, event)
assert user.has_event_permission(event.organizer, event, 'can_change_event_settings')
assert 'arbitrary' not in admin.get_event_permission_set(event.organizer, event)
assert 'arbitrary' not in admin.get_organizer_permission_set(event.organizer)
assert 'arbitrary' in user.get_event_permission_set(event.organizer, event)
assert 'arbitrary' in user.get_organizer_permission_set(event.organizer)
assert event in user.get_events_with_any_permission()
assert event in admin.get_events_with_any_permission(request=admin_request)
@pytest.mark.django_db
def test_list_of_events(event, user, admin):
def test_list_of_events(event, user, admin, admin_request):
orga2 = Organizer.objects.create(slug='d2', name='d2')
event2 = Event.objects.create(
organizer=event.organizer, name='Dummy', slug='dummy2',
@@ -236,25 +247,25 @@ def test_list_of_events(event, user, admin):
team2.limit_events.add(event)
team3.limit_events.add(event3)
events = list(user.get_events_with_any_permission())
events = list(user.get_events_with_any_permission(request=admin_request))
assert event in events
assert event2 in events
assert event3 in events
assert event4 not in events
events = list(user.get_events_with_permission('can_change_event_settings'))
events = list(user.get_events_with_permission('can_change_event_settings', request=admin_request))
assert event not in events
assert event2 not in events
assert event3 in events
assert event4 not in events
assert set(event.get_users_with_any_permission()) == {user, admin}
assert set(event2.get_users_with_any_permission()) == {user, admin}
assert set(event3.get_users_with_any_permission()) == {user, admin}
assert set(event4.get_users_with_any_permission()) == {admin}
assert set(event.get_users_with_any_permission()) == {user}
assert set(event2.get_users_with_any_permission()) == {user}
assert set(event3.get_users_with_any_permission()) == {user}
assert set(event4.get_users_with_any_permission()) == set()
assert set(event.get_users_with_permission('can_change_event_settings')) == {admin}
assert set(event2.get_users_with_permission('can_change_event_settings')) == {admin}
assert set(event3.get_users_with_permission('can_change_event_settings')) == {user, admin}
assert set(event4.get_users_with_permission('can_change_event_settings')) == {admin}
assert set(event.get_users_with_permission('can_change_orders')) == {admin, user}
assert set(event.get_users_with_permission('can_change_event_settings')) == set()
assert set(event2.get_users_with_permission('can_change_event_settings')) == set()
assert set(event3.get_users_with_permission('can_change_event_settings')) == {user}
assert set(event4.get_users_with_permission('can_change_event_settings')) == set()
assert set(event.get_users_with_permission('can_change_orders')) == {user}

View File

@@ -8,6 +8,7 @@ from django.contrib.auth.tokens import (
)
from django.core import mail as djmail
from django.test import TestCase, override_settings
from django.utils.timezone import now
from django_otp.oath import TOTP
from django_otp.plugins.otp_totp.models import TOTPDevice
from u2flib_server.jsapi import JSONDict
@@ -607,3 +608,105 @@ class SessionTimeOutTest(TestCase):
self.client.defaults['HTTP_USER_AGENT'] = 'Mozilla/5.0 (X11; Linux x86_64) Something else'
response = self.client.get('/control/')
self.assertEqual(response.status_code, 302)
@pytest.fixture
def user():
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
return user
@pytest.mark.django_db
def test_impersonate(user, client):
client.login(email='dummy@dummy.dummy', password='dummy')
user.is_staff = True
user.save()
ss = user.staffsession_set.create(date_start=now(), session_key=client.session.session_key)
t1 = int(time.time()) - 5
session = client.session
session['pretix_auth_long_session'] = False
session['pretix_auth_login_time'] = t1
session['pretix_auth_last_used'] = t1
session.save()
user2 = User.objects.create_user('dummy2@dummy.dummy', 'dummy')
response = client.post('/control/users/{user}/impersonate'.format(user=user2.pk), follow=True)
assert b'dummy2@' in response.content
response = client.get('/control/global/settings/')
assert response.status_code == 403
response = client.get('/control/')
response = client.post('/control/users/impersonate/stop', follow=True)
assert b'dummy@' in response.content
assert b'dummy2@' not in response.content
response = client.get('/control/global/settings/')
assert response.status_code == 200 # staff session is preserved
assert ss.logs.filter(url='/control/', impersonating=user2).exists()
@pytest.mark.django_db
def test_impersonate_require_recent_auth(user, client):
client.login(email='dummy@dummy.dummy', password='dummy')
user.is_staff = True
user.save()
user.staffsession_set.create(date_start=now(), session_key=client.session.session_key)
t1 = int(time.time()) - 5 * 3600
session = client.session
session['pretix_auth_long_session'] = False
session['pretix_auth_login_time'] = t1
session['pretix_auth_last_used'] = t1
session.save()
user2 = User.objects.create_user('dummy2@dummy.dummy', 'dummy')
response = client.post('/control/users/{user}/impersonate'.format(user=user2.pk), follow=True)
assert b'dummy2@' not in response.content
@pytest.mark.django_db
def test_staff_session(user, client):
client.login(email='dummy@dummy.dummy', password='dummy')
user.is_staff = True
user.save()
t1 = int(time.time()) - 5
session = client.session
session['pretix_auth_long_session'] = False
session['pretix_auth_login_time'] = t1
session['pretix_auth_last_used'] = t1
session.save()
response = client.get('/control/global/settings/')
assert response.status_code == 302
response = client.post('/control/sudo/')
assert response['Location'] == '/control/'
response = client.get('/control/global/settings/')
assert response.status_code == 200
client.post('/control/sudo/stop', follow=True)
response = client.get('/control/global/settings/')
assert response.status_code == 302
assert user.staffsession_set.last().logs.filter(url='/control/global/settings/').exists()
@pytest.mark.django_db
def test_staff_session_require_recent_auth(user, client):
client.login(email='dummy@dummy.dummy', password='dummy')
user.is_staff = True
user.save()
t1 = int(time.time()) - 5 * 3600
session = client.session
session['pretix_auth_long_session'] = False
session['pretix_auth_login_time'] = t1
session['pretix_auth_last_used'] = t1
session.save()
response = client.post('/control/sudo/')
assert response['Location'].startswith('/control/reauth/')
@pytest.mark.django_db
def test_staff_session_require_staff(user, client):
user.is_staff = False
user.save()
client.login(email='dummy@dummy.dummy', password='dummy')
t1 = int(time.time()) - 5
session = client.session
session['pretix_auth_long_session'] = False
session['pretix_auth_login_time'] = t1
session['pretix_auth_last_used'] = t1
session.save()
response = client.post('/control/sudo/')
assert response.status_code == 403

View File

@@ -26,13 +26,20 @@ def env():
superuser_urls = [
"global/settings/",
"global/update/",
"users/select2",
"users/",
"users/add",
"users/1/",
"users/1/impersonate",
"users/1/reset",
"sudo/sessions/",
]
staff_urls = [
"global/update/",
"sudo/",
"sudo/2/",
]
event_urls = [
@@ -146,10 +153,26 @@ def test_logged_out(client, env, url):
@pytest.mark.django_db
@pytest.mark.parametrize("url", superuser_urls)
def test_superuser_required(perf_patch, client, env, url):
client.login(email='dummy@dummy.dummy', password='dummy')
env[1].is_staff = True
env[1].save()
response = client.get('/control/' + url)
if response.status_code == 302:
assert '/sudo/' in response['Location']
else:
assert response.status_code == 403
env[1].staffsession_set.create(date_start=now(), session_key=client.session.session_key)
response = client.get('/control/' + url)
assert response.status_code in (200, 302, 404)
@pytest.mark.django_db
@pytest.mark.parametrize("url", staff_urls)
def test_staff_required(perf_patch, client, env, url):
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.get('/control/' + url)
assert response.status_code == 403
env[1].is_superuser = True
env[1].is_staff = True
env[1].save()
response = client.get('/control/' + url)
assert response.status_code in (200, 302, 404)

View File

@@ -99,8 +99,9 @@ class OrderSearchTest(SoupTest):
assert 'FO1' not in resp
assert 'FO2' not in resp
def test_suberuser(self):
self.user.is_superuser = True
def test_superuser(self):
self.user.is_staff = True
self.user.staffsession_set.create(date_start=now(), session_key=self.client.session.session_key)
self.user.save()
self.team.members.clear()
resp = self.client.get('/control/search/orders/').rendered_content

View File

@@ -34,7 +34,7 @@ def test_update_notice_displayed(client, user):
r = client.get('/control/')
assert 'pretix automatically checks for updates in the background' not in r.content.decode()
user.is_superuser = True
user.is_staff = True
user.save()
r = client.get('/control/')
assert 'pretix automatically checks for updates in the background' in r.content.decode()
@@ -46,7 +46,7 @@ def test_update_notice_displayed(client, user):
@pytest.mark.django_db
def test_settings(client, user):
user.is_superuser = True
user.is_staff = True
user.save()
client.login(email='dummy@dummy.dummy', password='dummy')
@@ -71,7 +71,7 @@ def test_trigger(client, user):
content_type='application/json',
)
user.is_superuser = True
user.is_staff = True
user.save()
client.login(email='dummy@dummy.dummy', password='dummy')

View File

@@ -65,6 +65,7 @@ def logged_in_client(client, event):
)
t.members.add(user)
client.force_login(user)
user.staffsession_set.create(date_start=now(), session_key=client.session.session_key)
return client