Skip to content

Commit

Permalink
#354 Login user from admin (#418)
Browse files Browse the repository at this point in the history
* #354 Login user from admin

* #354 Remodel AdminLoginAsBackend

* #354 Add review suggestions

* #354 Create AdminLoginAsMixin

* #354 Move AdminLoginAsMixin to poleno library

* #354 Move AdminLoginAsAdminMixin to poleno directory

* #354 Allow admin_login_as from UserAdmin and fix wrong pk in ProfileAdmin

* #354 Create AccountAdapter

* #354 Rename login_as_redirect_viewname attribute to lowercase

* #354 Set authentication_backend path

* #354 Remove unused import
  • Loading branch information
viliambalaz committed Apr 12, 2022
1 parent b5af447 commit 3343e2f
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 4 deletions.
10 changes: 10 additions & 0 deletions chcemvediet/adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from allauth.account.adapter import DefaultAccountAdapter

from poleno.invitations.adapters import InvitationsAdapter


class AccountAdapter(InvitationsAdapter, DefaultAccountAdapter):
def login(self, request, user):
if not hasattr(user, u'backend'):
user.backend = u'chcemvediet.auth_backends.AllauthAuthenticationBackendWithAdminLoginAs'
return super(AccountAdapter, self).login(request, user)
20 changes: 19 additions & 1 deletion chcemvediet/apps/accounts/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,28 @@
# -*- coding: utf-8 -*-
from django import forms
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError

from poleno.utils.misc import decorate
from poleno.utils.admin_login_as import AdminLoginAsAdminMixin
from poleno.utils.admin import simple_list_filter_factory, admin_obj_format
from poleno.utils.misc import decorate

from .models import Profile


admin.site.unregister(User)
@admin.register(User, site=admin.site)
class UserAdmin(AdminLoginAsAdminMixin, DjangoUserAdmin):
list_display = DjangoUserAdmin.list_display + (
decorate(
lambda o: admin_obj_format(o, u'Log in', link=u'login_as'),
short_description=u'Login As',
),
)
login_as_redirect_viewname = u'inforequests:mine'

class ProfileAdminForm(forms.ModelForm):

def clean_custom_anonymized_strings(self):
Expand Down Expand Up @@ -44,6 +58,10 @@ class ProfileAdmin(admin.ModelAdmin):
short_description=u'Undecided E-mails',
admin_order_field=u'undecided_emails_count',
),
decorate(
lambda o: admin_obj_format(o.user, u'Log in', link=u'login_as'),
short_description=u'Login As',
),
]
list_filter = [
u'anonymize_inforequests',
Expand Down
11 changes: 11 additions & 0 deletions chcemvediet/auth_backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from allauth.account.auth_backends import AuthenticationBackend
from django.contrib.auth.backends import ModelBackend

from poleno.utils.admin_login_as import AdminLoginAsBackendMixin


class DjangoModelBackendWithAdminLoginAs(AdminLoginAsBackendMixin, ModelBackend):
pass

class AllauthAuthenticationBackendWithAdminLoginAs(AdminLoginAsBackendMixin, AuthenticationBackend):
pass
6 changes: 3 additions & 3 deletions chcemvediet/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@
)

AUTHENTICATION_BACKENDS = (
u'django.contrib.auth.backends.ModelBackend',
u'allauth.account.auth_backends.AuthenticationBackend',
u'chcemvediet.auth_backends.DjangoModelBackendWithAdminLoginAs',
u'chcemvediet.auth_backends.AllauthAuthenticationBackendWithAdminLoginAs',
)

TEMPLATE_LOADERS = (
Expand Down Expand Up @@ -219,7 +219,7 @@
)

# Django-allauth settings
ACCOUNT_ADAPTER = u'poleno.invitations.adapters.InvitationsAdapter'
ACCOUNT_ADAPTER = u'chcemvediet.adapters.AccountAdapter'
ACCOUNT_AUTHENTICATION_METHOD = u'email'
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = u'optional'
Expand Down
74 changes: 74 additions & 0 deletions poleno/utils/admin_login_as.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from django.conf.urls import patterns, url
from django.contrib import admin
from django.contrib.auth.backends import ModelBackend
from django.core.urlresolvers import resolve, Resolver404
from django.http import HttpResponseRedirect

from poleno.utils.http import get_request
from poleno.utils.urls import reverse


class AdminLoginAsBackendMixin(ModelBackend):
u"""
Django authentication ModelBackend that allows admin to log in as any user (without being logged
off) and perform any action on his behalf, without the admin knowing the user's password. Admin
can simultaneously perform actions on admin page. Must be used together with
``AdminLoginAsAdminMixin``. Note that admin application namespace must be called 'admin'.
Example:
class MyApplicationBackend(AdminLoginAsBackendMixin, SomeAuthenticationBackend):
...
Settings:
AUTHENTICATION_BACKENDS = (
'path.to.MyApplicationBackend',
...
)
"""
def is_admin_path(self, path):
return resolve(path).namespace == u'admin'

def get_user(self, user_id):
request = get_request()
user = super(AdminLoginAsBackendMixin, self).get_user(user_id)
if request is None:
return user
try:
resolve(request.path)
except Resolver404:
return user
admin_login_as = request.session.get(u'admin_login_as')
if user and user.is_staff and not self.is_admin_path(request.path) and admin_login_as:
return super(AdminLoginAsBackendMixin, self).get_user(admin_login_as) or user
return user

class AdminLoginAsAdminMixin(admin.ModelAdmin):
u"""
Django ModelAdmin that defines view, which allows admin to be simultaneously logged in as any
user. Must be used together with ``AdminLoginAsBackendMixin``.
Example:
class MyModelAdmin(AdminLoginAsAdminMixin, admin.ModelAdmin):
list_display = [
...
decorate(
lambda o: admin_obj_format(o, u'Log in', link=u'login_as'),
),
]
login_as_redirect_viewname = u'application:viewname'
...
"""
def login_as_view(self, request, obj_pk):
request.session[u'admin_login_as'] = obj_pk
return HttpResponseRedirect(reverse(self.login_as_redirect_viewname))

def get_urls(self):
info = self.model._meta.app_label, self.model._meta.model_name
login_as_view = self.admin_site.admin_view(self.login_as_view)
urls = patterns('',
url(r'^(\d+)/login-as/$', login_as_view, name=u'{}_{}_login_as'.format(*info)),
)
return urls + super(AdminLoginAsAdminMixin, self).get_urls()
109 changes: 109 additions & 0 deletions poleno/utils/tests/test_admin_login_as.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from django.contrib.auth.decorators import user_passes_test
from django.conf.urls import patterns, url, RegexURLPattern
from django.contrib.auth.models import User
from django.http import HttpResponse
from django.test import TestCase
from django.test.utils import override_settings


class AdminLoginAsBackendMixinTest(TestCase):

def public_view(request):
if isinstance(request.user, User):
pass # force request.user to evaluate
return HttpResponse()

@user_passes_test(lambda u: u.is_staff, login_url=u'/login/', redirect_field_name=u'next')
def admin_view(request):
if isinstance(request.user, User):
pass
return HttpResponse()

@user_passes_test(lambda u: u.is_staff, login_url=u'/login/', redirect_field_name=u'next')
def set_admin_login_as_attribute_admin_view(request, obj_pk):
request.session[u'admin_login_as'] = obj_pk
if isinstance(request.user, User):
pass
return HttpResponse()

urls = tuple(patterns(u'',
url(r'^$', public_view),
url(r'admin/', ([
RegexURLPattern(r'^$', admin_view),
RegexURLPattern(r'^(\d+)/login-as/$', set_admin_login_as_attribute_admin_view),
], None, u'admin')),
))

def create_users(self):
self.user = User.objects.create_user(
username=u'user',
email=u'[email protected]',
password=u'test',
)
self.superuser = User.objects.create_superuser(
username=u'superuser',
email=u'[email protected]',
password=u'test',
)

def setUp(self):
self.settings_override = override_settings(
AUTHENTICATION_BACKENDS=(u'poleno.utils.admin_login_as.AdminLoginAsBackendMixin',),
PASSWORD_HASHERS=(u'django.contrib.auth.hashers.MD5PasswordHasher',),
)
self.settings_override.enable()
self.create_users()

def tearDown(self):
self.settings_override.disable()


def test_public_route_uses_anonymous_user_if_user_is_not_logged_in(self):
response = self.client.get(u'/')
self.assertEqual(response.status_code, 200)
self.assertTrue(response.wsgi_request.user.is_anonymous())

def test_admin_route_uses_anonymous_user_and_fails_if_user_is_not_logged_in(self):
response = self.client.get(u'/admin/')
self.assertTrue(response.wsgi_request.user.is_anonymous())
self.assertEqual(response.status_code, 302)
self.assertRedirects(response, u'/login/?next=/admin/', fetch_redirect_response=False)

def test_public_route_uses_the_user_if_user_is_logged_in(self):
self.assertTrue(self.client.login(username=self.user.username, password=u'test'))
response = self.client.get(u'/')
self.assertEqual(response.status_code, 200)
self.assertTrue(response.wsgi_request.user, self.user)

def test_admin_route_uses_the_user_and_fails_if_user_is_logged_in(self):
self.assertTrue(self.client.login(username=self.user.username, password=u'test'))
response = self.client.get(u'/admin/')
self.assertEqual(response.status_code, 302)
self.assertRedirects(response, u'/login/?next=/admin/', fetch_redirect_response=False)
self.assertEqual(response.wsgi_request.user, self.user)

def test_public_route_uses_the_admin_if_admin_is_logged_in(self):
self.assertTrue(self.client.login(username=self.superuser.username, password=u'test'))
response = self.client.get(u'/')
self.assertEqual(response.status_code, 200)
self.assertTrue(response.wsgi_request.user, self.superuser)

def test_admin_route_uses_the_admin_if_admin_is_logged_in(self):
self.assertTrue(self.client.login(username=self.superuser.username, password=u'test'))
response = self.client.get(u'/admin/')
self.assertEqual(response.status_code, 200)
self.assertTrue(response.wsgi_request.user, self.superuser)

def test_public_route_uses_the_user_if_admin_is_logged_in_as_another_user(self):
self.assertTrue(self.client.login(username=self.superuser.username, password=u'test'))
self.client.get(u'/admin/{}/login-as/'.format(self.user.pk))
response = self.client.get(u'/')
self.assertEqual(response.status_code, 200)
self.assertTrue(response.wsgi_request.user, self.user)

def test_admin_route_uses_the_admin_if_admin_is_logged_in_as_another_user(self):
self.assertTrue(self.client.login(username=self.superuser.username, password=u'test'))
self.client.get(u'/admin/{}/login-as/'.format(self.user.pk))
response = self.client.get(u'/admin/')
self.assertEqual(response.status_code, 200)
self.assertTrue(response.wsgi_request.user, self.superuser)

0 comments on commit 3343e2f

Please sign in to comment.