Django user impersonation by admin
Asked Answered
C

8

30

I have a Django app. When logged in as an admin user, I want to be able to pass a secret parameter in the URL and have the whole site behave as if I were another user.

Let's say I have the URL /my-profile/ which shows the currently logged in user's profile. I want to be able to do something like /my-profile/?__user_id=123 and have the underlying view believe that I am actually the user with ID 123 (thus render that user's profile).

Why do I want that?

Simply because it's much easier to reproduce certain bugs that only appear in a single user's account.

My questions:

  1. What would be the easiest way to implement something like this?

  2. Is there any security concern I should have in mind when doing this? Note that I (obviously) only want to have this feature for admin users, and our admin users have full access to the source code, database, etc. anyway, so it's not really a "backdoor"; it just makes it easier to access a user's account.

Conversion answered 11/2, 2010 at 7:35 Comment(9)
Yes it's a backdoor. If you can hack this function you can then impersonant anyone. Also it allows admins to edit as if they are a user, so that an edit shows up as a user in the log when it in fact was another user, etc.Platonism
Django admins can edit anything from the admin interface anyway. And as I said, our admins have full access to the source code, database, server, etc. If they want to do something on behalf of a user, they could very well just do it using plain SQL. So I don't really see how providing a more friendly way of doing that is dangerous.Conversion
Also, note that we are not in the financial / banking / whatever industry where regulations might actually legally prevent us from doing this. We just have a service we provide and we want to make customer support as smooth as possible. That's it. :)Conversion
They can't edit it so that it looks like another user did it. And obviously, it's your problem (or rather your customers) if you introduce security holes, not mine. You do what you want. I'm just telling you it's a bad idea.Platonism
Of course it's my decision in the end. I was hoping for some more objective comments though, instead of "it's self-evident" and "you do what you want". Especially since I posted a piece of code: I hope people can tell me if they find any flaws in it, or how I can improve it. Of course, in the end it's all a compromise, and it shouldn't be turned on by default.Conversion
Using the piece of code I posted below, the admin can NOT do anything and make it "look like another user did it". The impersonation only applies to GET requests. It's only meant for debugging the way pages render in certain unusual conditions. For simulating actions, I can always write a unit test.Conversion
This is not a security hole and can be very useful. If a user complains that something is not rendering correctly or a permission is not working, impersonating that user is often the most practical way to reproduce the issue. In unix, admins can do this. It's the same thing. If the feature is limited to "super" admins it's perfectly reasonable but must of course be used with discretion as with any admin feature.Unmoor
I agree with lbz and Alex. It's a reasonable feature - and can be very useful. BUT, it needs to be done in such a way as to truly be able to control who has access to this feature. And ibz is right. If a user has sufficient access to the database, they can do anything they want anyway.Heder
For anyone reading this question in 2019, there are half a dozen 3rd part django apps providing this feature...Mervin
C
11

I solved this with a simple middleware. It also handles redirects (that is, the GET parameter is preserved during a redirect). Here it is:

class ImpersonateMiddleware(object):
    def process_request(self, request):
        if request.user.is_superuser and "__impersonate" in request.GET:
            request.user = models.User.objects.get(id=int(request.GET["__impersonate"]))

    def process_response(self, request, response):
        if request.user.is_superuser and "__impersonate" in request.GET:
            if isinstance(response, http.HttpResponseRedirect):
                location = response["Location"]
                if "?" in location:
                    location += "&"
                else:
                    location += "?"
                location += "__impersonate=%s" % request.GET["__impersonate"]
                response["Location"] = location
        return response
Conversion answered 11/2, 2010 at 10:42 Comment(4)
If you can easily turn on and off this middleware (from filesystem configuration of course) then this is a good compromise, as you can leave it off except for when you really need it.Platonism
There is no reason to he so paranoid about this feature. An admin can delete a user. An admin can edit a user. An admin can kill the site. Logging in as a them is no less secure. As long as the superadmin login is secure so is the "Su" feature. If there is a concern about the admin, the fact that a session was su'd can be logged such that the events are traceable.Unmoor
I should add that I regularly include this feature. It's extraordinarily useful. I would use some very explicit and visible logging if this was a banking site and severEly limit the privilege to super admins, but it's fine even there given appropriate safeguards.Unmoor
I use a similar middleware, but a custom header instead. X-Masquerade-As: <username>. This then sets the user attribute on the request, but also sets the superuser as the 'real_user', which is then used in logging.Hypoblast
U
43

I don't have enough reputation to edit or reply yet (I think), but I found that although ionaut's solution worked in simple cases, a more robust solution for me was to use a session variable. That way, even AJAX requests are served correctly without modifying the request URL to include a GET impersonation parameter.

class ImpersonateMiddleware(object):
    def process_request(self, request):
        if request.user.is_superuser and "__impersonate" in request.GET:
            request.session['impersonate_id'] = int(request.GET["__impersonate"])
        elif "__unimpersonate" in request.GET:
            del request.session['impersonate_id']
        if request.user.is_superuser and 'impersonate_id' in request.session:
            request.user = User.objects.get(id=request.session['impersonate_id'])

Usage:

log in: http://localhost/?__impersonate=[USERID]
log out (back to admin): http://localhost/?__unimpersonate=True
Unmerciful answered 12/1, 2011 at 19:2 Comment(1)
Thanks, this works wonderfully! I just changed it to operate on usernames instead of ids as that made more sense for us.Malnourished
O
15

It looks like quite a few other people have had this problem and have written re-usable apps to do this and at least some are listed on the django packages page for user switching. The most active at time of writing appear to be:

  • django-hijack puts a "hijack" button in the user list in the admin, along with a bit at the top of page for while you've hijacked an account.
  • impostor means you can login with username "me as other" and your own password
  • django-impersonate sets up URLs to start impersonating a user, stop, search etc
Otology answered 27/2, 2015 at 11:10 Comment(2)
django-hijack made my day. Thanks @hamish for suggesting these packages.Merline
Do these also work if you have a different frontend?Scullery
C
11

I solved this with a simple middleware. It also handles redirects (that is, the GET parameter is preserved during a redirect). Here it is:

class ImpersonateMiddleware(object):
    def process_request(self, request):
        if request.user.is_superuser and "__impersonate" in request.GET:
            request.user = models.User.objects.get(id=int(request.GET["__impersonate"]))

    def process_response(self, request, response):
        if request.user.is_superuser and "__impersonate" in request.GET:
            if isinstance(response, http.HttpResponseRedirect):
                location = response["Location"]
                if "?" in location:
                    location += "&"
                else:
                    location += "?"
                location += "__impersonate=%s" % request.GET["__impersonate"]
                response["Location"] = location
        return response
Conversion answered 11/2, 2010 at 10:42 Comment(4)
If you can easily turn on and off this middleware (from filesystem configuration of course) then this is a good compromise, as you can leave it off except for when you really need it.Platonism
There is no reason to he so paranoid about this feature. An admin can delete a user. An admin can edit a user. An admin can kill the site. Logging in as a them is no less secure. As long as the superadmin login is secure so is the "Su" feature. If there is a concern about the admin, the fact that a session was su'd can be logged such that the events are traceable.Unmoor
I should add that I regularly include this feature. It's extraordinarily useful. I would use some very explicit and visible logging if this was a banking site and severEly limit the privilege to super admins, but it's fine even there given appropriate safeguards.Unmoor
I use a similar middleware, but a custom header instead. X-Masquerade-As: <username>. This then sets the user attribute on the request, but also sets the superuser as the 'real_user', which is then used in logging.Hypoblast
M
3

@Charles Offenbacher's answer is great for impersonating users who are not being authenticated via tokens. However, it will not work with clients side apps that use token authentication. To get user impersonation to work with apps using tokens, one has to directly set the HTTP_AUTHORIZATION header in the Impersonate Middleware. My answer basically plagiarizes Charles's answer and adds lines for manually setting said header.

class ImpersonateMiddleware(object):
    def process_request(self, request):
        if request.user.is_superuser and "__impersonate" in request.GET:
            request.session['impersonate_id'] = int(request.GET["__impersonate"])
        elif "__unimpersonate" in request.GET:
            del request.session['impersonate_id']
        if request.user.is_superuser and 'impersonate_id' in request.session:
            request.user = User.objects.get(id=request.session['impersonate_id'])
            # retrieve user's token
            token = Token.objects.get(user=request.user)
            # manually set authorization header to user's token as it will be set to that of the admin's (assuming the admin has one, of course)
            request.META['HTTP_AUTHORIZATION'] = 'Token {0}'.format(token.key)
Marxist answered 20/11, 2016 at 15:51 Comment(0)
W
1

i don't see how that is a security hole any more than using su - someuser as root on a a unix machine. root or an django-admin with root/admin access to the database can fake anything if he/she wants to. the risk is only in the django-admin account being cracked at which point the cracker could hide tracks by becoming another user and then faking actions as the user.

yes, it may be called a backdoor, but as ibz says, admins have access to the database anyways. being able to make changes to the database in that light is also a backdoor.

Waterlogged answered 12/2, 2010 at 5:29 Comment(2)
Well, Unix security is pretty broken in principle. The difference is that after 40 years of unix bugfixing it's pretty hard to crack the su command and execute su unless you are supposed to. Thousands of people have tried to break it, and thousands of people has fixed the problems. This is hardly the case for ibz Django middleware. If you want to be secure, you have to be paranoid. :)Platonism
But, once you are an su-capable user (under unix) you can do anything. Same with having the is_superuser flag set. As long as the impersonation middleware checks this flag, then it is as secure as doing something via the admin.Hypoblast
S
0

Based on Charles' answer, I've updated to work with Django 4.2

class ImpersonateMiddleware:

  def __init__(self, get_response):
      self.get_response = get_response

  def __call__(self, request):
    if request.user.is_superuser and "__impersonate" in request.GET:
        request.session['impersonate_id'] = int(request.GET["__impersonate"])
    elif "__unimpersonate" in request.GET:
        del request.session['impersonate_id']
    if request.user.is_superuser and 'impersonate_id' in request.session:
        request.user = User.objects.get(id=request.session['impersonate_id'])
    response = self.get_response(request)
    return response
Sports answered 28/5, 2023 at 2:27 Comment(0)
E
0

You can make something much simpler:

def impersonate(request, name):
    assert settings.DEBUG
    login(request, user=User.objects.get(username=name), backend=_get_backends(return_tuples=True)[0][1])
    return HttpResponseRedirect('/')

I wrote the assert for DEBUG there because this code is clearly only good for local dev work.

Emasculate answered 18/7, 2023 at 19:41 Comment(0)
P
-3

Set up so you have two different host names to the same server. If you are doing it locally, you can connect with 127.0.0.1, or localhost, for example. Your browser will see this as three different sites, and you can be logged in with different users. The same works for your site.

So in addition to www.mysite.com you can set up test.mysite.com, and log in with the user there. I often set up sites (with Plone) so I have both www.mysite.com and admin.mysite.com, and only allow access to the admin pages from there, meaning I can log in to the normal site with the username that has the problems.

Platonism answered 11/2, 2010 at 7:45 Comment(6)
This doesn't answer my question. I don't want to login with different users, since I don't know the other users' passwords. I just want to impersonate other users (being an admin).Conversion
@ibz: No, you don't. That would be like the biggest security hole ever, it's a very bad idea. If you really have problem with one specific user (and not with a specific security group) then reset that users password, and let the users reset it back later.Platonism
How exactly is that a "security hole"?Conversion
To be honest, I think I both answered that in a comment to the question, and I also think it's pretty self-evident. Sorry.Platonism
@ibz: Knowing the admin password means ALL user accounts are compromised. Whereas knowing just a user password compromises only one account.Unaunabated
If an attacker knows the admin password, then you're lost anyway. It doesn't matter if you have an impersonating feature or not. You can just log in as the admin and do all kind of evil stuff. Impersonating makes it just more convenient for an attacker to do evil user related things.Izaak

© 2022 - 2024 — McMap. All rights reserved.