Impersonation system implementation
This commit is contained in:
@@ -8,14 +8,16 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm, UserCreationForm
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
from django.http import JsonResponse
|
||||
from django.core.paginator import Paginator
|
||||
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils import timezone
|
||||
|
||||
from common.markdown import render_markdown
|
||||
from library.models import FileIndex
|
||||
|
||||
from .forms import AvatarForm, BioForm, EmailForm, UsernameForm
|
||||
from .models import ApiToken, UserProfile
|
||||
from .models import ApiToken, ImpersonationLog, UserProfile
|
||||
|
||||
|
||||
def register(request):
|
||||
@@ -47,6 +49,10 @@ def user_login(request):
|
||||
def user_logout(request):
|
||||
if request.method == 'POST':
|
||||
was_authorized = request.session.get('authorized', False)
|
||||
# While impersonating, "logout" returns to the real account instead of
|
||||
# ending the session entirely.
|
||||
if _stop_impersonation(request):
|
||||
return redirect('landing:home')
|
||||
logout(request)
|
||||
# logout() flushes the session (wiping the gate flag). The gate is a
|
||||
# site-wide master password, separate from user accounts — preserve it.
|
||||
@@ -56,6 +62,85 @@ def user_logout(request):
|
||||
return redirect('landing:home')
|
||||
|
||||
|
||||
# ---------- Impersonation (staff only) ----------
|
||||
|
||||
def _stop_impersonation(request):
|
||||
"""Return the session to the original (impersonator) account. Shared by
|
||||
impersonate_stop and user_logout. login() flushes the session, so the gate
|
||||
flag must be restored afterwards."""
|
||||
impersonator_id = request.session.pop('impersonator_id', None)
|
||||
if not impersonator_id:
|
||||
return False
|
||||
original = get_user_model().objects.filter(pk=impersonator_id).first()
|
||||
if original is None:
|
||||
return False
|
||||
ImpersonationLog.objects.filter(
|
||||
impersonator=original, ended_at__isnull=True,
|
||||
).update(ended_at=timezone.now())
|
||||
login(request, original, backend='django.contrib.auth.backends.ModelBackend')
|
||||
request.session['authorized'] = True
|
||||
return True
|
||||
|
||||
|
||||
def user_list(request):
|
||||
"""Browse-style directory of every account on the site."""
|
||||
q = request.GET.get('q', '').strip()
|
||||
users = get_user_model().objects.select_related('userprofile').order_by('username')
|
||||
if q:
|
||||
users = users.filter(username__icontains=q)
|
||||
paginator = Paginator(users, 24)
|
||||
page_obj = paginator.get_page(request.GET.get('page'))
|
||||
return render(request, 'profiles/users.html', {
|
||||
'users': page_obj,
|
||||
'q': q,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def impersonate_start(request, user_id):
|
||||
if request.method != 'POST':
|
||||
return HttpResponse(status=405)
|
||||
if not request.user.is_staff:
|
||||
return HttpResponseForbidden('Only staff can impersonate users.')
|
||||
if request.session.get('impersonator_id'):
|
||||
messages.error(request, 'Stop the current impersonation first.')
|
||||
return redirect('profiles:user_profile', username=request.user.username)
|
||||
|
||||
actor = request.user
|
||||
target = get_object_or_404(get_user_model(), pk=user_id)
|
||||
|
||||
if target.pk == actor.pk:
|
||||
messages.error(request, 'You cannot impersonate yourself.')
|
||||
return redirect('profiles:user_profile', username=target.username)
|
||||
|
||||
if target.is_superuser:
|
||||
messages.error(request, 'Superusers cannot be impersonated.')
|
||||
return redirect('profiles:user_profile', username=target.username)
|
||||
|
||||
if target.is_staff and actor.pk >= target.pk:
|
||||
# Staff-to-staff: only an older (lower PK) account may impersonate.
|
||||
messages.error(request, 'You cannot impersonate a more senior staff member.')
|
||||
return redirect('profiles:user_profile', username=target.username)
|
||||
|
||||
reason = request.POST.get('reason', '')[:255]
|
||||
ImpersonationLog.objects.create(impersonator=actor, target=target, reason=reason)
|
||||
|
||||
login(request, target, backend='django.contrib.auth.backends.ModelBackend')
|
||||
request.session['authorized'] = True
|
||||
request.session['impersonator_id'] = actor.pk
|
||||
messages.success(request, f'Now acting as {target.username}.')
|
||||
return redirect('profiles:user_profile', username=target.username)
|
||||
|
||||
|
||||
@login_required
|
||||
def impersonate_stop(request):
|
||||
if request.method != 'POST':
|
||||
return HttpResponse(status=405)
|
||||
_stop_impersonation(request)
|
||||
messages.success(request, 'Impersonation stopped.')
|
||||
return redirect('landing:home')
|
||||
|
||||
|
||||
@login_required
|
||||
def settings_page(request):
|
||||
tokens = request.user.api_tokens.all()
|
||||
@@ -186,6 +271,14 @@ def user_profile(request, username):
|
||||
user = get_object_or_404(User, username__iexact=username)
|
||||
profile, created = UserProfile.objects.get_or_create(user=user)
|
||||
is_owner = request.user.is_authenticated and request.user.pk == user.pk
|
||||
can_impersonate = (
|
||||
request.user.is_authenticated
|
||||
and request.user.is_staff
|
||||
and not request.session.get('impersonator_id')
|
||||
and request.user.pk != user.pk
|
||||
and not user.is_superuser
|
||||
and (not user.is_staff or request.user.pk < user.pk)
|
||||
)
|
||||
bio_form = None
|
||||
if is_owner:
|
||||
if request.method == 'POST' and request.POST.get('action') == 'bio':
|
||||
@@ -201,6 +294,7 @@ def user_profile(request, username):
|
||||
'profile': profile,
|
||||
'profile_user': user,
|
||||
'is_owner': is_owner,
|
||||
'can_impersonate': can_impersonate,
|
||||
'bio_form': bio_form,
|
||||
'projects': user.projects.select_related('owner', 'thumbnail').prefetch_related(
|
||||
'tag_links__tag__category', 'versions',
|
||||
|
||||
Reference in New Issue
Block a user