\n"
14 | "Language: ru\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20 | "X-Generator: Poedit 1.5.7\n"
21 |
22 | #: forms.py:18
23 | #| msgid "users"
24 | msgid "Users"
25 | msgstr "Пользователи"
26 |
27 | #: templates/admin/auth/user/change_form.html:8
28 | msgid "History"
29 | msgstr ""
30 |
31 | #: templates/admin/auth/user/change_form.html:11 templates/su/login.html:5
32 | msgid "Login as"
33 | msgstr "Войти как"
34 |
35 | #: templates/admin/auth/user/change_form.html:16
36 | msgid "View on site"
37 | msgstr ""
38 |
39 | #: templates/su/login.html:5
40 | msgid "Django site admin"
41 | msgstr ""
42 |
43 | #: templates/su/login.html:24
44 | msgid "Change Login"
45 | msgstr "Сменить пользователя"
46 |
47 | #: templates/su/login_link.html:3
48 | msgid "Login as other user"
49 | msgstr "Войти как другой пользователь"
50 |
--------------------------------------------------------------------------------
/django_su/models.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamcharnock/django-su/afc8c9cdb71ea62d12ab0365721339f5a43f4512/django_su/models.py
--------------------------------------------------------------------------------
/django_su/templates/admin/auth/user/change_form.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/change_form.html" %}
2 | {% load i18n %}
3 |
4 | {% block object-tools-items %}
5 | {{ block.super }}
6 | {% if object_id %}
7 |
8 |
9 | {% trans "Login as" %}
10 |
11 |
12 | {% endif %}
13 | {% endblock %}
14 |
15 | {% block content %}
16 | {{ block.super }}
17 | {% if object_id %}{% endif %}
18 | {% endblock %}
19 |
--------------------------------------------------------------------------------
/django_su/templates/admin/auth/user/change_list.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/change_list.html" %}
2 |
3 | {% load i18n su_tags %}
4 |
5 | {% block object-tools-items %}
6 | {% login_su_link user %}
7 | {{ block.super }}
8 | {% endblock %}
9 |
--------------------------------------------------------------------------------
/django_su/templates/admin/base_site.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/base.html" %}
2 |
3 | {% load i18n %}
4 |
5 | {% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
6 |
7 | {% block branding %}
8 |
9 | {% endblock %}
10 |
11 | {% block welcome-msg %}
12 | {% if IS_SU %}
13 | {% trans 'You are' %}{% trans ' logged in as,' %}
14 | {% else %}
15 | {% trans 'Welcome,' %}
16 | {% endif %}
17 | {% firstof user.get_short_name user.get_username %}.
18 | {% endblock %}
19 |
20 | {% block nav-global %}{% endblock %}
21 |
--------------------------------------------------------------------------------
/django_su/templates/su/is_su.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% if IS_SU %}
3 |
15 |
16 | {% trans "WARNING: You have assumed the identity of another account!" %}
17 |
18 | {% endif %}
19 |
--------------------------------------------------------------------------------
/django_su/templates/su/login.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/base_site.html" %}
2 |
3 | {% load i18n static %}
4 |
5 | {% block title %}{% trans "Login as" %} | {% trans 'Django site admin' %}{% endblock %}
6 |
7 | {% block extrahead %}
8 | {{ block.super }}
9 | {% if form.use_ajax_select %}
10 |
11 |
12 |
13 | {% endif %}
14 | {{ form.media }}
15 |
16 | {% endblock %}
17 |
18 | {% block breadcrumbs %}{% endblock %}
19 |
20 | {% block content %}
21 |
27 | {% endblock %}
28 |
--------------------------------------------------------------------------------
/django_su/templates/su/login_link.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% if can_su_login %}{% trans "Login as other user" %}{% endif %}
3 |
--------------------------------------------------------------------------------
/django_su/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamcharnock/django-su/afc8c9cdb71ea62d12ab0365721339f5a43f4512/django_su/templatetags/__init__.py
--------------------------------------------------------------------------------
/django_su/templatetags/su_tags.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from django import template
4 |
5 | from ..utils import su_login_callback
6 |
7 |
8 | register = template.Library()
9 |
10 |
11 | @register.inclusion_tag("su/login_link.html", takes_context=False)
12 | def login_su_link(user):
13 | return {"can_su_login": su_login_callback(user)}
14 |
--------------------------------------------------------------------------------
/django_su/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamcharnock/django-su/afc8c9cdb71ea62d12ab0365721339f5a43f4512/django_su/tests/__init__.py
--------------------------------------------------------------------------------
/django_su/tests/test_backends.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import get_user_model
2 | from django.test import TestCase
3 |
4 |
5 | User = get_user_model()
6 |
7 |
8 | class TestSuBackend(TestCase):
9 | def setUp(self):
10 | super(TestSuBackend, self).setUp()
11 | from django_su.backends import SuBackend
12 |
13 | self.user = User.objects.create(username="testuser")
14 | self.backend = SuBackend()
15 |
16 | def test_authenticate_do_it(self):
17 | """Ensure authentication passes when su=True and user id is valid"""
18 | self.assertEqual(
19 | self.backend.authenticate(su=True, user_id=self.user.pk), self.user
20 | )
21 |
22 | def test_authenticate_dont_do_it(self):
23 | """Ensure authentication fails when su=False and user id is valid"""
24 | self.assertEqual(
25 | self.backend.authenticate(su=False, user_id=self.user.pk), None
26 | )
27 |
28 | def test_authenticate_id_none(self):
29 | """Ensure authentication fails when user_id is None"""
30 | self.assertEqual(self.backend.authenticate(su=True, user_id=None), None)
31 |
32 | def test_authenticate_id_non_existent(self):
33 | """Ensure authentication fails when user_id doesn't exist"""
34 | self.assertEqual(self.backend.authenticate(su=True, user_id=999), None)
35 |
36 | def test_authenticate_id_invalid(self):
37 | """Ensure authentication fails when user_id is invalid"""
38 | self.assertEqual(self.backend.authenticate(su=True, user_id="abc"), None)
39 |
40 | def test_get_user_exists(self):
41 | """Ensure get_user returns the expected user"""
42 | self.assertEqual(self.backend.get_user(user_id=self.user.pk), self.user)
43 |
44 | def test_get_user_does_not_exist(self):
45 | """Ensure get_user returns None if user is not found"""
46 | self.assertEqual(self.backend.get_user(user_id=999), None)
47 |
--------------------------------------------------------------------------------
/django_su/tests/test_views.py:
--------------------------------------------------------------------------------
1 | from datetime import date, timezone
2 |
3 | from django.conf import settings
4 | from django.contrib import auth
5 | from django.contrib.auth import get_user_model
6 | from django.contrib.sessions.backends import cached_db
7 | from django.test import Client, TestCase
8 | from django.urls import reverse
9 | from django.utils.datetime_safe import datetime
10 |
11 |
12 | User = get_user_model()
13 |
14 |
15 | class SuViewsBaseTestCase(TestCase):
16 | def setUp(self):
17 | super(SuViewsBaseTestCase, self).setUp()
18 | from django_su.views import login_as_user
19 |
20 | self.authorized_user = self.user("authorized", is_superuser=True)
21 | self.unauthorized_user = self.user("unauthorized")
22 | self.destination_user = self.user("destination")
23 | self.view = login_as_user
24 | self.client = self.make_client()
25 | # Causes errors with validation.
26 | # TODO: Investigate
27 | if "ajax_select" in settings.INSTALLED_APPS:
28 | settings.INSTALLED_APPS.remove("ajax_select")
29 |
30 | def user(self, username, **kwargs):
31 | user = User.objects.create(username=username, **kwargs)
32 | user.set_password("pass")
33 | user.save()
34 | return user
35 |
36 | def make_client(self):
37 | client = Client()
38 | s = cached_db.SessionStore()
39 | s.save()
40 | client.cookies[settings.SESSION_COOKIE_NAME] = s.session_key
41 | return client
42 |
43 |
44 | class LoginAsUserViewTestCase(SuViewsBaseTestCase):
45 | def test_login_success(self):
46 | """Ensure login works for a valid user"""
47 | self.client.login(username="authorized", password="pass")
48 | response = self.client.post(
49 | reverse("login_as_user", args=[self.destination_user.id])
50 | )
51 | self.assertEqual(response.status_code, 302)
52 | # Check the user is logged in in the session
53 | self.assertIn(auth.SESSION_KEY, self.client.session)
54 | self.assertEqual(
55 | str(self.client.session[auth.SESSION_KEY]), str(self.destination_user.id)
56 | )
57 | # Check the 'exit_users_pk' is set so we know which user to change back to
58 | self.assertIn("exit_users_pk", self.client.session)
59 | pk, backend = self.client.session["exit_users_pk"][0]
60 | self.assertEqual(str(pk), str(self.authorized_user.pk))
61 | self.assertEqual(backend, "django.contrib.auth.backends.ModelBackend")
62 |
63 | def test_login_user_id_invalid(self):
64 | """Ensure login fails with an invalid user id"""
65 | self.client.login(username="authorized", password="pass")
66 | response = self.client.post("/su/abc/")
67 | self.assertEqual(response.status_code, 404)
68 | # User should still be logged in, but as the original user
69 | self.assertIn(auth.SESSION_KEY, self.client.session)
70 | self.assertEqual(
71 | str(self.client.session[auth.SESSION_KEY]), str(self.authorized_user.id)
72 | )
73 | # Exit user should never get set
74 | self.assertNotIn("exit_users_pk", self.client.session)
75 |
76 | def test_login_without_permission(self):
77 | """Ensure login fails when the current user lacks permission"""
78 | self.client.login(username="unauthorized", password="pass")
79 | with self.settings(SU_LOGIN_CALLBACK=None):
80 | response = self.client.post(
81 | reverse("login_as_user", args=[self.destination_user.id])
82 | )
83 | self.assertEqual(response.status_code, 302)
84 | # User should still be logged in, but as the original user
85 | self.assertIn(auth.SESSION_KEY, self.client.session)
86 | self.assertEqual(
87 | str(self.client.session[auth.SESSION_KEY]), str(self.unauthorized_user.id)
88 | )
89 | # Exit user should never get set
90 | self.assertNotIn("exit_users_pk", self.client.session)
91 |
92 | def test_custom_su_login_url(self):
93 | """Ensure user is sent to login url following successful login"""
94 | self.client.login(username="authorized", password="pass")
95 | with self.settings(SU_LOGIN_REDIRECT_URL="/foo/bar"):
96 | response = self.client.post(
97 | reverse("login_as_user", args=[self.destination_user.id])
98 | )
99 | self.assertEqual(response.status_code, 302)
100 | self.assertTrue("/foo/bar" in response["Location"])
101 |
102 | def test_custom_login_action(self):
103 | """Ensure custom login action is called"""
104 | self.client.login(username="authorized", password="pass")
105 |
106 | flag = {"called": False}
107 |
108 | def custom_action(request, user):
109 | flag["called"] = True
110 |
111 | with self.settings(SU_CUSTOM_LOGIN_ACTION=custom_action):
112 | self.client.post(reverse("login_as_user", args=[self.destination_user.id]))
113 | self.assertTrue(flag["called"])
114 |
115 | def test_last_login_not_changed(self):
116 | self.destination_user.last_login = datetime(2000, 1, 1, tzinfo=timezone.utc)
117 | self.destination_user.save()
118 | self.client.login(username="authorized", password="pass")
119 | self.client.post(reverse("login_as_user", args=[self.destination_user.id]))
120 | self.destination_user = User.objects.get(pk=self.destination_user.pk)
121 | self.assertEqual(self.destination_user.last_login.date(), date(2000, 1, 1))
122 | # Check the update_last_login function has been reconnected to the user_logged_in signal
123 | connections = [
124 | str(ref[1])
125 | for ref in auth.user_logged_in.receivers
126 | if "update_last_login" in str(ref[1])
127 | ]
128 | self.assertTrue(connections)
129 |
130 | def test_login_signal_reconnected_following_error(self):
131 | self.client.login(username="authorized", password="pass")
132 |
133 | def error_action(request, user):
134 | raise Exception()
135 |
136 | with self.settings(SU_CUSTOM_LOGIN_ACTION=error_action):
137 | try:
138 | self.client.post(
139 | reverse("login_as_user", args=[self.destination_user.id])
140 | )
141 | except Exception:
142 | pass
143 | # Check the update_last_login function has been reconnected to the user_logged_in signal
144 | connections = [
145 | str(ref[1])
146 | for ref in auth.user_logged_in.receivers
147 | if "update_last_login" in str(ref[1])
148 | ]
149 | self.assertTrue(connections)
150 |
151 |
152 | class LoginViewTestCase(SuViewsBaseTestCase):
153 | def test_get_authorised(self):
154 | """Load the login page as an authorised user"""
155 | self.client.login(username="authorized", password="pass")
156 | response = self.client.get(reverse("su_login"))
157 | self.assertEqual(response.status_code, 200)
158 |
159 | def test_get_unauthorised(self):
160 | """Load the login page as an authorised user"""
161 | self.client.login(username="unauthorized", password="pass")
162 | response = self.client.get(reverse("su_login"))
163 | self.assertEqual(response.status_code, 302)
164 |
165 | def test_post_unauthorised(self):
166 | """Post to the login page as an authorised user"""
167 | self.client.login(username="unauthorized", password="pass")
168 | response = self.client.post(reverse("su_login"))
169 | self.assertEqual(response.status_code, 302)
170 |
171 | def test_post_valid(self):
172 | """Ensure posting valid data logs the user in"""
173 | self.client.login(username="authorized", password="pass")
174 | response = self.client.post(
175 | reverse("su_login"), data=dict(user=self.destination_user.id)
176 | )
177 | self.assertEqual(response.status_code, 302)
178 | self.assertEqual(
179 | str(self.client.session[auth.SESSION_KEY]), str(self.destination_user.id)
180 | )
181 |
182 | def test_post_non_existent(self):
183 | """Ensure posting a non-existent user does not log the user in"""
184 | self.client.login(username="authorized", password="pass")
185 | response = self.client.post(reverse("su_login"), data=dict(user="999"))
186 | self.assertEqual(response.status_code, 200)
187 | self.assertEqual(
188 | str(self.client.session[auth.SESSION_KEY]), str(self.authorized_user.id)
189 | )
190 |
191 | def test_post_invalid(self):
192 | """Ensure posting invalid data redisplays the form and does not log the user in"""
193 | self.client.login(username="authorized", password="pass")
194 | response = self.client.post(reverse("su_login"), data=dict(user="abc"))
195 | self.assertEqual(response.status_code, 200)
196 | self.assertEqual(
197 | str(self.client.session[auth.SESSION_KEY]), str(self.authorized_user.id)
198 | )
199 |
200 |
201 | class LogoutViewTestCase(SuViewsBaseTestCase):
202 | def test_valid_get(self):
203 | """Ensure user can logout via get"""
204 | s = self.client.session
205 | s["exit_users_pk"] = [
206 | ["1", "django.contrib.auth.backends.ModelBackend"],
207 | ]
208 | s.save()
209 | response = self.client.get(reverse("su_logout"))
210 | self.assertEqual(response.status_code, 302)
211 | self.assertIn(auth.SESSION_KEY, self.client.session)
212 | self.assertEqual(
213 | str(self.client.session[auth.SESSION_KEY]), str(self.authorized_user.id)
214 | )
215 |
216 | def test_valid_post(self):
217 | """Ensure user can logout via post"""
218 | s = self.client.session
219 | s["exit_users_pk"] = [
220 | ["1", "django.contrib.auth.backends.ModelBackend"],
221 | ]
222 | s.save()
223 | response = self.client.post(reverse("su_logout"))
224 | self.assertEqual(response.status_code, 302)
225 | self.assertIn(auth.SESSION_KEY, self.client.session)
226 | self.assertEqual(
227 | str(self.client.session[auth.SESSION_KEY]), str(self.authorized_user.id)
228 | )
229 |
230 | def test_no_exit_pk(self):
231 | """Ensure logout fails if no exit pk present in session"""
232 | response = self.client.get(reverse("su_logout"))
233 | self.assertEqual(response.status_code, 400)
234 |
235 | def test_non_existant_exit_pk(self):
236 | """Ensure logout fails if no exit pk present in session"""
237 | s = self.client.session
238 | s["exit_users_pk"] = [
239 | ["999", "django.contrib.auth.backends.ModelBackend"],
240 | ]
241 | s.save()
242 | response = self.client.get(reverse("su_logout"))
243 | self.assertEqual(response.status_code, 404)
244 |
245 | def test_redirect_url(self):
246 | """Ensure logout redirect url setting respected"""
247 | s = self.client.session
248 | s["exit_users_pk"] = [
249 | ["1", "django.contrib.auth.backends.ModelBackend"],
250 | ]
251 | s.save()
252 | with self.settings(SU_LOGOUT_REDIRECT_URL="/foo/bar"):
253 | response = self.client.get(reverse("su_logout"))
254 | self.assertEqual(response.status_code, 302)
255 | self.assertTrue("/foo/bar" in response["Location"])
256 |
--------------------------------------------------------------------------------
/django_su/urls.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from django.urls import path
4 |
5 | from .views import login_as_user, su_login, su_logout
6 |
7 |
8 | urlpatterns = [
9 | path("", su_logout, name="su_logout"),
10 | path("login/", su_login, name="su_login"),
11 | path("/", login_as_user, name="login_as_user"),
12 | ]
13 |
--------------------------------------------------------------------------------
/django_su/utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import warnings
4 | from collections.abc import Callable
5 |
6 | from django.conf import settings
7 | from django.utils.module_loading import import_string
8 |
9 |
10 | def su_login_callback(user):
11 | if hasattr(settings, "SU_LOGIN"):
12 | warnings.warn(
13 | "SU_LOGIN is deprecated, use SU_LOGIN_CALLBACK",
14 | DeprecationWarning,
15 | )
16 |
17 | func = getattr(settings, "SU_LOGIN_CALLBACK", None)
18 | if func is not None:
19 | if not isinstance(func, Callable):
20 | func = import_string(func)
21 | return func(user)
22 | return user.has_perm("auth.change_user")
23 |
24 |
25 | def custom_login_action(request, user):
26 | func = getattr(settings, "SU_CUSTOM_LOGIN_ACTION", None)
27 | if func is None:
28 | return False
29 |
30 | if not isinstance(func, Callable):
31 | func = import_string(func)
32 | func(request, user)
33 |
34 | return True
35 |
--------------------------------------------------------------------------------
/django_su/views.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import warnings
4 |
5 | from django.conf import settings
6 | from django.contrib.auth import (
7 | BACKEND_SESSION_KEY,
8 | SESSION_KEY,
9 | authenticate,
10 | get_user_model,
11 | login,
12 | )
13 | from django.contrib.auth.decorators import user_passes_test
14 | from django.http import Http404, HttpResponseBadRequest, HttpResponseRedirect
15 | from django.shortcuts import get_object_or_404, render
16 | from django.views.decorators.csrf import csrf_protect
17 | from django.views.decorators.http import require_http_methods
18 |
19 | from .forms import UserSuForm
20 | from .utils import custom_login_action, su_login_callback
21 |
22 |
23 | User = get_user_model()
24 |
25 |
26 | @csrf_protect
27 | @require_http_methods(["POST"])
28 | @user_passes_test(su_login_callback)
29 | def login_as_user(request, user_id):
30 | userobj = authenticate(request=request, su=True, user_id=user_id)
31 | if not userobj:
32 | raise Http404("User not found")
33 |
34 | exit_users_pk = request.session.get("exit_users_pk", default=[])
35 | exit_users_pk.append(
36 | (request.session[SESSION_KEY], request.session[BACKEND_SESSION_KEY])
37 | )
38 |
39 | maintain_last_login = hasattr(userobj, "last_login")
40 | if maintain_last_login:
41 | last_login = userobj.last_login
42 |
43 | try:
44 | if not custom_login_action(request, userobj):
45 | login(request, userobj)
46 | request.session["exit_users_pk"] = exit_users_pk
47 | finally:
48 | if maintain_last_login:
49 | userobj.last_login = last_login
50 | userobj.save(update_fields=["last_login"])
51 |
52 | if hasattr(settings, "SU_REDIRECT_LOGIN"):
53 | warnings.warn(
54 | "SU_REDIRECT_LOGIN is deprecated, use SU_LOGIN_REDIRECT_URL",
55 | DeprecationWarning,
56 | )
57 |
58 | return HttpResponseRedirect(getattr(settings, "SU_LOGIN_REDIRECT_URL", "/"))
59 |
60 |
61 | @csrf_protect
62 | @require_http_methods(["POST", "GET"])
63 | @user_passes_test(su_login_callback)
64 | def su_login(request, form_class=UserSuForm, template_name="su/login.html"):
65 | form = form_class(request.POST or None)
66 | if form.is_valid():
67 | return login_as_user(request, form.get_user().pk)
68 |
69 | return render(
70 | request,
71 | template_name,
72 | {
73 | "form": form,
74 | },
75 | )
76 |
77 |
78 | def su_logout(request):
79 | exit_users_pk = request.session.get("exit_users_pk", default=[])
80 | if not exit_users_pk:
81 | return HttpResponseBadRequest(("This session was not su'ed into. Cannot exit."))
82 |
83 | user_id, backend = exit_users_pk.pop()
84 |
85 | userobj = get_object_or_404(User, pk=user_id)
86 | userobj.backend = backend
87 |
88 | if not custom_login_action(request, userobj):
89 | login(request, userobj)
90 | request.session["exit_users_pk"] = exit_users_pk
91 |
92 | if hasattr(settings, "SU_REDIRECT_EXIT"):
93 | warnings.warn(
94 | "SU_REDIRECT_EXIT is deprecated, use SU_LOGOUT_REDIRECT_URL",
95 | DeprecationWarning,
96 | )
97 |
98 | return HttpResponseRedirect(getattr(settings, "SU_LOGOUT_REDIRECT_URL", "/"))
99 |
--------------------------------------------------------------------------------
/example/README.rst:
--------------------------------------------------------------------------------
1 | Example
2 | =======
3 |
4 | To run the example application, make sure you have the required
5 | packages installed. You can do this using following commands :
6 |
7 | .. code-block:: bash
8 |
9 | mkvirtualenv example
10 | pip install -r example/requirements.txt
11 |
12 | This assumes you already have ``virtualenv`` and ``virtualenvwrapper``
13 | installed and configured.
14 |
15 | Next, you can setup the django instance using :
16 |
17 | .. code-block:: bash
18 |
19 | python example/manage.py syncdb --noinput
20 |
21 | And run it :
22 |
23 | .. code-block:: bash
24 |
25 | python example/manage.py runserver
26 |
27 | Good luck!
28 |
--------------------------------------------------------------------------------
/example/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamcharnock/django-su/afc8c9cdb71ea62d12ab0365721339f5a43f4512/example/__init__.py
--------------------------------------------------------------------------------
/example/lookups.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from ajax_select import LookupChannel, register
4 | from django.db.models import Q
5 | from django.utils.encoding import force_text
6 | from django.utils.html import escape
7 |
8 | from django_su import get_user_model
9 |
10 |
11 | @register("django_su")
12 | class UsersLookup(LookupChannel):
13 | model = get_user_model()
14 |
15 | def get_query(self, q, request):
16 | return self.model.objects.filter(
17 | Q(username__icontains=q) | Q(pk__icontains=q)
18 | ).order_by("pk")
19 |
20 | def format_match(self, obj):
21 | return escape(
22 | force_text("%s [pk: %s]" % (obj.get_full_name() or obj.username, obj.pk))
23 | )
24 |
25 | def format_item_display(self, obj):
26 | return escape(
27 | force_text("%s [pk: %s]" % (obj.get_full_name() or obj.username, obj.pk))
28 | )
29 |
--------------------------------------------------------------------------------
/example/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 |
6 | if __name__ == "__main__":
7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
8 |
9 | from django.core.management import execute_from_command_line
10 |
11 | # Allow starting the app without installing the module.
12 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
13 |
14 | execute_from_command_line(sys.argv)
15 |
--------------------------------------------------------------------------------
/example/requirements.txt:
--------------------------------------------------------------------------------
1 | django>=1.4
2 | django-form-admin
3 | django-ajax-selects
4 |
--------------------------------------------------------------------------------
/example/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for app project.
3 |
4 | For more information on this file, see
5 | https://docs.djangoproject.com/en/1.7/topics/settings/
6 |
7 | For the full list of settings and their values, see
8 | https://docs.djangoproject.com/en/1.7/ref/settings/
9 | """
10 |
11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
12 | import os
13 |
14 |
15 | BASE_DIR = os.path.dirname(os.path.dirname(__file__))
16 |
17 |
18 | # Quick-start development settings - unsuitable for production
19 | # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
20 |
21 | # SECURITY WARNING: keep the secret key used in production secret!
22 | SECRET_KEY = "YOUR_SECRET_KEY"
23 |
24 | # SECURITY WARNING: don't run with debug turned on in production!
25 | DEBUG = True
26 |
27 | TEMPLATE_DEBUG = True
28 |
29 | ALLOWED_HOSTS = []
30 |
31 | TEMPLATE_DIRS = (os.path.join(os.path.dirname(__file__), "templates"),)
32 |
33 | TEMPLATE_CONTEXT_PROCESSORS = (
34 | "django.core.context_processors.tz",
35 | "django.core.context_processors.i18n",
36 | "django.core.context_processors.media",
37 | "django.core.context_processors.static",
38 | "django.core.context_processors.request",
39 | "django.contrib.auth.context_processors.auth",
40 | "django.core.context_processors.debug",
41 | "django_su.context_processors.is_su",
42 | )
43 |
44 | TEMPLATES = [
45 | {
46 | "BACKEND": "django.template.backends.django.DjangoTemplates",
47 | "DIRS": [
48 | os.path.join(os.path.dirname(__file__), "templates"),
49 | ],
50 | "APP_DIRS": True,
51 | "OPTIONS": {
52 | "context_processors": [
53 | "django.template.context_processors.i18n",
54 | "django.template.context_processors.request",
55 | "django.contrib.auth.context_processors.auth",
56 | "django.template.context_processors.debug",
57 | "django.contrib.messages.context_processors.messages",
58 | "django_su.context_processors.is_su",
59 | ],
60 | },
61 | },
62 | ]
63 |
64 | # Application definition
65 |
66 | MIDDLEWARE = [
67 | "django.contrib.sessions.middleware.SessionMiddleware",
68 | "django.contrib.auth.middleware.AuthenticationMiddleware",
69 | "django.contrib.messages.middleware.MessageMiddleware",
70 | ]
71 |
72 | PROJECT_APPS = [
73 | "django_su",
74 | ]
75 |
76 | INSTALLED_APPS = [
77 | # 'suit', # pip install django-suit
78 | "django.contrib.auth",
79 | "django.contrib.sites",
80 | "django.contrib.sessions",
81 | "django.contrib.staticfiles",
82 | "django.contrib.contenttypes",
83 | "django.contrib.admin",
84 | "django.contrib.messages",
85 | # 'guardian',
86 | "formadmin", # pip install django-form-admin
87 | "ajax_select", # pip install django-ajax-selects
88 | ]
89 |
90 | INSTALLED_APPS = PROJECT_APPS + INSTALLED_APPS
91 |
92 | ROOT_URLCONF = "example.urls"
93 |
94 | SITE_ID = 1
95 |
96 | # Database
97 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases
98 |
99 | DATABASES = {
100 | "default": {
101 | "ENGINE": "django.db.backends.sqlite3",
102 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
103 | }
104 | }
105 |
106 | # Internationalization
107 | # https://docs.djangoproject.com/en/1.7/topics/i18n/
108 |
109 | LANGUAGE_CODE = "en-us"
110 |
111 | TIME_ZONE = "UTC"
112 |
113 | USE_I18N = True
114 |
115 | USE_L10N = True
116 |
117 | USE_TZ = True
118 |
119 | # Static files (CSS, JavaScript, Images)
120 | # https://docs.djangoproject.com/en/1.7/howto/static-files/
121 |
122 | STATIC_URL = "/static/"
123 |
124 | AUTHENTICATION_BACKENDS = (
125 | "django.contrib.auth.backends.ModelBackend",
126 | # "guardian.backends.ObjectPermissionBackend",
127 | "django_su.backends.SuBackend",
128 | )
129 |
130 | # ANONYMOUS_USER_ID = -1
131 |
132 | # URL to redirect after the login.
133 | # Default: "/"
134 | SU_LOGIN_REDIRECT_URL = "/"
135 |
136 | # URL to redirect after the logout.
137 | # Default: "/"
138 | SU_LOGOUT_REDIRECT_URL = "/"
139 |
140 | # A function to specify the perms that the user must have can use django_su
141 | # Default: None
142 | SU_LOGIN_CALLBACK = "example.utils.su_login_callback"
143 |
144 | # A function to override the django.contrib.auth.login(request, user)
145 | # function so you can set session data, etc.
146 | # Default: None
147 | SU_CUSTOM_LOGIN_ACTION = "example.utils.custom_login"
148 |
149 | if "ajax_select" in INSTALLED_APPS:
150 | AJAX_LOOKUP_CHANNELS = {
151 | "django_su": ("example.lookups", "UsersLookup"),
152 | }
153 |
--------------------------------------------------------------------------------
/example/templates/404.html:
--------------------------------------------------------------------------------
1 | Not Found
2 |
--------------------------------------------------------------------------------
/example/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {% include "su/is_su.html" %}
5 |
6 | O hai, {{ user }}!
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.contrib import admin
3 | from django.urls import include, path
4 | from django.views.generic import TemplateView
5 |
6 |
7 | admin.autodiscover()
8 |
9 | urlpatterns = [
10 | path("admin/", admin.site.urls),
11 | path("su/", include("django_su.urls")),
12 | path("", TemplateView.as_view(template_name="index.html")),
13 | ]
14 |
15 | if "ajax_select" in settings.INSTALLED_APPS:
16 | from ajax_select import urls as ajax_select_urls
17 |
18 | urlpatterns += [
19 | path("admin/lookups/", include(ajax_select_urls)),
20 | ]
21 |
--------------------------------------------------------------------------------
/example/utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from django.contrib.auth import BACKEND_SESSION_KEY, SESSION_KEY, get_user_model
4 |
5 |
6 | try:
7 | from django.contrib.auth import HASH_SESSION_KEY
8 | except ImportError:
9 | HASH_SESSION_KEY = "_auth_user_hash"
10 |
11 | User = get_user_model()
12 |
13 |
14 | def su_login_callback(user):
15 | if user.is_active and user.is_staff:
16 | return True
17 | return user.has_perm("auth.change_user")
18 |
19 |
20 | def _get_user_session_key(request):
21 | # This value in the session is always serialized to a string, so we need
22 | # to convert it back to Python whenever we access it.
23 |
24 | return User._meta.pk.to_python(request.session[SESSION_KEY])
25 |
26 |
27 | def custom_login(request, user):
28 | session_auth_hash = ""
29 | if user is None:
30 | user = request.user
31 | if hasattr(user, "get_session_auth_hash"):
32 | session_auth_hash = user.get_session_auth_hash()
33 |
34 | if SESSION_KEY in request.session:
35 | if _get_user_session_key(request) != user.pk or (
36 | session_auth_hash
37 | and request.session.get(HASH_SESSION_KEY) != session_auth_hash
38 | ):
39 | # To avoid reusing another user's session, create a new, empty
40 | # session if the existing session corresponds to a different
41 | # authenticated user.
42 | request.session.flush()
43 | else:
44 | request.session.cycle_key()
45 | request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)
46 | request.session[BACKEND_SESSION_KEY] = user.backend
47 | request.session[HASH_SESSION_KEY] = session_auth_hash
48 | if hasattr(request, "user"):
49 | request.user = user
50 |
51 | try:
52 | from django.middleware.csrf import rotate_token
53 |
54 | rotate_token(request)
55 | except ImportError:
56 | pass
57 |
--------------------------------------------------------------------------------
/requirements.test.txt:
--------------------------------------------------------------------------------
1 | coveralls
2 | django-form-admin
3 | django-ajax-selects
4 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [zest.releaser]
2 | current_version = 0.9.0
3 | commit = True
4 | tag = True
5 |
6 | [wheel]
7 | # create "py2.py3-none-any.whl" package
8 | universal = 1
9 |
10 | [flake8]
11 | max-line-length = 110
12 | ignore =
13 | # additional newline in imports
14 | I202,
15 | # line break before binary operator
16 | W503,
17 | exclude =
18 | *migrations/*,
19 | docs/,
20 | .eggs/
21 | application-import-names = django_su
22 | import-order-style = pep8
23 |
24 | [isort]
25 | multi_line_output = 3
26 | lines_after_imports = 2
27 | profile = black
28 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import codecs
4 | import os
5 | import sys
6 |
7 | from setuptools import find_packages, setup
8 |
9 |
10 | # When creating the sdist, make sure the django.mo file also exists:
11 | if "sdist" in sys.argv or "develop" in sys.argv:
12 | os.chdir("django_su")
13 | try:
14 | from django.core import management
15 |
16 | management.call_command("compilemessages", stdout=sys.stderr, verbosity=1)
17 | except ImportError:
18 | if "sdist" in sys.argv:
19 | raise
20 | finally:
21 | os.chdir("..")
22 |
23 |
24 | def read(*parts):
25 | file_path = os.path.join(os.path.dirname(__file__), *parts)
26 | return codecs.open(file_path, encoding="utf-8").read()
27 |
28 |
29 | setup(
30 | name="django-su",
31 | version=read("VERSION"),
32 | license="MIT License",
33 | install_requires=[
34 | "django>=2.2",
35 | ],
36 | requires=[
37 | "Django (>=2.2)",
38 | ],
39 | description="Login as any user from the Django admin interface, then switch back when done",
40 | long_description=read("README.rst"),
41 | author="Adam Charnock",
42 | author_email="adam@adamcharnock.com",
43 | maintainer="Basil Shubin",
44 | maintainer_email="basil.shubin@gmail.com",
45 | url="http://github.com/adamcharnock/django-su",
46 | download_url="https://github.com/adamcharnock/django-su/zipball/master",
47 | packages=find_packages(exclude=("example*", "*.tests*")),
48 | include_package_data=True,
49 | zip_safe=False,
50 | classifiers=[
51 | "Development Status :: 5 - Production/Stable",
52 | "Environment :: Web Environment",
53 | "Framework :: Django",
54 | "Intended Audience :: Developers",
55 | "Intended Audience :: System Administrators",
56 | "License :: OSI Approved :: MIT License",
57 | "Operating System :: OS Independent",
58 | "Programming Language :: Python",
59 | "Programming Language :: Python :: 3",
60 | "Programming Language :: Python :: 3.6",
61 | "Programming Language :: Python :: 3.7",
62 | "Programming Language :: Python :: 3.8",
63 | "Programming Language :: Python :: 3.9",
64 | "Topic :: Internet :: WWW/HTTP",
65 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
66 | ],
67 | )
68 |
--------------------------------------------------------------------------------
/test_settings.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}}
5 |
6 | TEMPLATE_DIRS = [
7 | os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_templates"),
8 | ]
9 |
10 | TEMPLATES = [
11 | {
12 | "BACKEND": "django.template.backends.django.DjangoTemplates",
13 | "DIRS": TEMPLATE_DIRS,
14 | "APP_DIRS": True,
15 | },
16 | ]
17 |
18 | INSTALLED_APPS = [
19 | "django.contrib.auth",
20 | "django.contrib.sites",
21 | "django.contrib.sessions",
22 | "django.contrib.staticfiles",
23 | "django.contrib.contenttypes",
24 | "django_su",
25 | "django.contrib.admin",
26 | ]
27 |
28 | MIDDLEWARE_CLASSES = [
29 | "django.contrib.sessions.middleware.SessionMiddleware",
30 | "django.contrib.auth.middleware.AuthenticationMiddleware",
31 | ]
32 |
33 | MIDDLEWARE = MIDDLEWARE_CLASSES
34 |
35 | ROOT_URLCONF = "test_urls"
36 |
37 | SITE_ID = 1
38 |
39 | STATIC_URL = "/static/"
40 |
41 | AUTHENTICATION_BACKENDS = [
42 | "django.contrib.auth.backends.ModelBackend",
43 | "django_su.backends.SuBackend",
44 | ]
45 |
--------------------------------------------------------------------------------
/test_templates/404.html:
--------------------------------------------------------------------------------
1 | Not Found
2 |
--------------------------------------------------------------------------------
/test_urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.urls import include, path
3 |
4 |
5 | urlpatterns = [
6 | path("admin/", admin.site.urls),
7 | path("su/", include("django_su.urls")),
8 | ]
9 |
--------------------------------------------------------------------------------