It's always a sign of good design when adding a new feature turns out to be easy.
In the
Django
authentication system, I
wanted a way for an administrator to view the site as if they were
a particular user; effectively an equivalent to
su
in UNIX-land.
The first easy step was to invent a URL to correspond to this action, which gets
encoded in
urls.py
:
(r'^su/(?P<username>.*)/$', 'qlockweb.accounts.views.su', {'redirect_url': '/qlockdata/'}),
That done, the second and final step is to write some view code.
@user_passes_test(lambda u: u.is_staff)
def su(request, username, redirect_url='/'):
su_user = get_object_or_404(User, username=username)
if su_user.is_active:
request.session[SESSION_KEY] = su_user.id
return HttpResponseRedirect(redirect_url)
Seven lines of code and we're done (modulo a bunch of import
statements).
Expanding what's going on here:
(r'^su/(?P<username>.*)/$', 'qlockweb.accounts.views.su', {'redirect_url': '/qlockdata/'}),
When an HTTP request arrives at the framework, Django goes through its list of URLs until it finds a
match. In this case, going to http://mysite/accounts/su/fred/ ends hitting the urls.py
line above; the /(?P<username>.*)/
part of the regexp pulls out "fred" and this gets
passed as a parameter named username
into the function su
in qlockweb/accounts/views.py
. This function also gets passed a parameter
called redirect_url
with value '/qlockdata/'
.
@user_passes_test(lambda u: u.is_staff)
Actually, we need to rewind one step before we get into the su
function. The
line before the function definition is a Python
decorator: some extra code wrapping the function that gets
executed just before the function itself is executed. This decorator needs some expansion of its own:
@user_passes_test(lambda u: u.is_staff)
The @ sign is the syntactic sugar that indicates that this line is a decorator for the function
that comes immediately afterwards.
@user_passes_test(lambda u: u.is_staff)
This is the decorator function (from contrib/auth/decorators.py
); its first
argument test_func
is a function that does the test. This test function is given
a single parameter: the current User
. If the test function returns
true, the wrapped view code is called; if not, then the user gets redirected to a login page.
@user_passes_test(lambda u: u.is_staff)
More syntactic sugar. We want a function is_this_a_staff_user(u)
that
checks whether its argument u
is an administrator. However, as this is the
only place that the function is used, we don't bother to give it a name—we just use
a lambda expression
to give the definition right here and now.
@user_passes_test(lambda u: u.is_staff)
Finally, the body of the lambda expression just uses the method of the User
class
that indicates whether the user is an administrator or not.
def su(request, username, redirect_url='/'):
So now we're in the su
function itself, and if we've got this far we're guaranteed that the person viewing the
page is logged in as an is_staff
user. The function has the username
and redirect_url
parameters mentioned earlier; it also has a request
parameter that holds all of the information about the original web request (in a
HttpRequest
object).
su_user = get_object_or_404(User, username=username)
The next line of code gets a User
object for the username that was
specified—fred
in other words. If there isn't a user called fred
, then
a Http404
exception gets raised, which will percolate up the stack and display a (surprise,
surprise) 404 page.
if su_user.is_active:
This particular version of our code only allows impersonation of active users, helpfully provided by
the is_active
field in the standard User
model.
request.session[SESSION_KEY] = su_user.id
The next line of code is the one that actually does the work. The requesting user's
session
is modified so that its user ID is the impersonated user's.
return HttpResponseRedirect(redirect_url)
The final line of code redirects the web browser off to the redirect_url
page.
(Statutory disclaimer: I am not a security expert, nor do I play one on TV. Adding this to a production
system is probably not a good idea.)
Another Django snippet: I finally
discovered
that the follow
argument to the standard
Manipulators
allows you to list fields in the model that the form should leave untouched. Very helpful: the
end result is more compact
and less brittle than the code I'd put together to manually override all of the hidden fields.
[A:37385 B:3278 C:346 D:9187 E:62544 Total:112740]