Skip to content

Commit

Permalink
su for contest admins (#400)
Browse files Browse the repository at this point in the history
* su for contest admins

* Dont allow two_factor namespace for contest admins

* Remove debug

* Allow blocking urls and namespaces

* Various limitations, tests

* Add changes suggested in code review

* Fix and optimize an old query

* Use `filter_users_with_accessible_personal_data` for finding switchable users

* Update __init__.py

* Check if effective user is not a superuser

* Fix bug

* Add test for switching to a user which becomes superuser
  • Loading branch information
MasloMaslane authored Sep 11, 2024
1 parent 0139795 commit a5be176
Show file tree
Hide file tree
Showing 13 changed files with 389 additions and 14 deletions.
5 changes: 2 additions & 3 deletions oioioi/contests/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.core.exceptions import PermissionDenied
from django.core.mail import EmailMessage
from django.db import transaction
from django.db.models import Q
from django.db.models import Subquery
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.safestring import mark_safe
Expand Down Expand Up @@ -295,8 +295,7 @@ def filter_participants(self, queryset):

def filter_users_with_accessible_personal_data(self, queryset):
submissions = Submission.objects.filter(problem_instance__contest=self.contest)
authors = [s.user for s in submissions]
return [q for q in queryset if q in authors]
return queryset.filter(id__in=Subquery(submissions.values('user_id')))


class ContestControllerContext(object):
Expand Down
6 changes: 6 additions & 0 deletions oioioi/deployment/settings.py.template
Original file line number Diff line number Diff line change
Expand Up @@ -581,3 +581,9 @@ ZEUS_INSTANCES = {

# Experimental
# USE_ACE_EDITOR = False

# If set to True, contest admins will be able to log in as participants of contests they admin.
CONTEST_ADMINS_CAN_SU = False

# If set to False, contest admins switched to a participant will be able to make any type of request.
ALLOW_ONLY_GET_FOR_SU_CONTEST_ADMINS = True
4 changes: 4 additions & 0 deletions oioioi/su/README.rst
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
An option for admins to log in as other user
for testing purposes.

You can enable su for contest admins with ``CONTEST_ADMINS_CAN_SU`` option set to ``True`` in ``settings.py``. By default
this option is disabled. You can allow contest admins using su to make other request than `GET` by setting
``ALLOW_ONLY_GET_FOR_SU_CONTEST_ADMINS`` to ``True`` (default ``False``).
10 changes: 10 additions & 0 deletions oioioi/su/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,13 @@

SU_UID_SESSION_KEY = 'su_effective_user_id'
SU_BACKEND_SESSION_KEY = 'su_effective_backend'
SU_REAL_USER_IS_SUPERUSER = 'su_real_user_is_superuser'
SU_ORIGINAL_CONTEST = 'su_original_contest'

BLOCKED_URLS = [
'api_token', 'api_regenerate_key',
'submitservice_view_user_token', 'submitservice_clear_user_token',
'edit_profile', 'delete_profile',
'auth_password_change', 'auth_password_done',
]
BLOCKED_URL_NAMESPACES = ['two_factor', 'oioioiadmin']
57 changes: 54 additions & 3 deletions oioioi/su/middleware.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.http.response import HttpResponseForbidden
from django.shortcuts import redirect
from django.conf import settings
from django.urls import resolve

from oioioi.base.utils.middleware import was_response_generated_by_exception
from oioioi.su import SU_BACKEND_SESSION_KEY, SU_UID_SESSION_KEY
from oioioi.su.utils import get_user
from oioioi.contests.current_contest import contest_re
from oioioi.su import (
SU_BACKEND_SESSION_KEY,
SU_UID_SESSION_KEY,
SU_REAL_USER_IS_SUPERUSER,
SU_ORIGINAL_CONTEST,
BLOCKED_URL_NAMESPACES,
BLOCKED_URLS
)
from oioioi.su.utils import get_user, reset_to_real_user

REDIRECTION_AFTER_SU_KEY = "redirection_after_su"

Expand All @@ -20,7 +30,9 @@ def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
self._process_request(request)
response = self._process_request(request)
if response:
return response

return self.get_response(request)

Expand All @@ -39,6 +51,45 @@ def _process_request(self, request):
request.session[SU_UID_SESSION_KEY],
request.session[SU_BACKEND_SESSION_KEY],
)
# Check if the user is contest admin.
if not request.session.get(SU_REAL_USER_IS_SUPERUSER, True):
# Might happen when switching to a user which then becomes a superuser.
if request.user.is_superuser:
reset_to_real_user(request)
return redirect('index')

original_contest_id = request.session.get(SU_ORIGINAL_CONTEST)
contest_id = None
m = contest_re.match(request.path)
if m is not None:
contest_id = m.group('c_name')
url = resolve(request.path_info)
is_su_reset_url = url.url_name == 'su_reset'
nonoioioi_namespace = url.namespaces == []
for ns in url.namespaces:
if ns != 'contest' and ns != 'noncontest':
nonoioioi_namespace = True
break
# Redirect if the url is not in the same contest, is not a su reset url and is an url made by oioioi.
# For example, `nonoioioi_namespace` can be True when the url is /jsi18n/
if (
not is_su_reset_url and
not nonoioioi_namespace and
(contest_id is None or contest_id != original_contest_id)
):
return redirect('su_url_not_allowed', contest_id=original_contest_id)

for ns in url.namespaces:
if ns in BLOCKED_URL_NAMESPACES:
return redirect('su_url_not_allowed', contest_id=original_contest_id)
if url.url_name in BLOCKED_URLS:
return redirect('su_url_not_allowed', contest_id=original_contest_id)

if (
not is_su_reset_url and
getattr(settings, 'ALLOW_ONLY_GET_FOR_SU_CONTEST_ADMINS', True) and request.method != 'GET'
):
return redirect('su_method_not_allowed', contest_id=original_contest_id)


class SuFirstTimeRedirectionMiddleware(object):
Expand Down
20 changes: 20 additions & 0 deletions oioioi/su/templates/su/method-not-allowed.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{% extends "simple-centered.html" %}

{% load i18n %}

{% block title %}{% trans "SU - Method not allowed" %}{% endblock %}

{% block content %}

<div class="text-center mt-3">
<h3>
{% blocktrans %}
Sorry, this OIOIOI instance only allows GET requests for contest admins using su.
{% endblocktrans %}
</h3>
<a href="{% url 'contest_dashboard' %}" class="btn btn-primary mt-3">
{% trans "Return to the contest" %}
</a>
</div>

{% endblock %}
2 changes: 1 addition & 1 deletion oioioi/su/templates/su/navbar-su-form.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% load i18n simple_filters %}

{% if ctx.user.is_superuser %}
{% if ctx.user.is_superuser or is_valid_contest_admin %}
<div class="dropdown-divider mb-0" role="separator"></div>
<form class="p-3" action="{% url 'su' %}" method="post" id="su-dropdown-form">
{% csrf_token %}
Expand Down
20 changes: 20 additions & 0 deletions oioioi/su/templates/su/url-not-allowed.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{% extends "simple-centered.html" %}

{% load i18n %}

{% block title %}{% trans "SU - URL not allowed" %}{% endblock %}

{% block content %}

<div class="text-center mt-3">
<h3>
{% blocktrans %}
Sorry, you can't access this page.
{% endblocktrans %}
</h3>
<a href="{% url 'contest_dashboard' %}" class="btn btn-primary mt-3">
{% trans "Return to the contest" %}
</a>
</div>

{% endblock %}
13 changes: 12 additions & 1 deletion oioioi/su/templatetags/get_su.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,22 @@
@register.inclusion_tag('su/navbar-su-form.html', takes_context=True)
def su_dropdown_form(context):
from oioioi.su.forms import SuForm
from oioioi.su.utils import is_under_su
from oioioi.contests.utils import is_contest_basicadmin, contest_exists
from oioioi.su.utils import is_under_su, can_contest_admins_su, is_real_superuser

request = context['request']
# Checking if real user is a superuser blocks the ability to switch when switched to contest admin.
is_valid_contest_admin = (
can_contest_admins_su(request) and
contest_exists(request) and
is_contest_basicadmin(request) and
not is_real_superuser(request)
)

return {
'ctx': context,
'form': SuForm(auto_id='su-%s'),
'is_under_su': is_under_su(context['request']),
'num_hints': getattr(settings, 'NUM_HINTS', 10),
'is_valid_contest_admin': is_valid_contest_admin,
}
Loading

0 comments on commit a5be176

Please sign in to comment.