Django Admin Change password link broken - python

Example for user "Bob"
Django Admin: Home › Authentication and Authorization › Users › Bob
When modifying a user in Django Admin interface, under the password it says:
Raw passwords are not stored, so there is no way to see this user's password, but you can change the password using this form.
However, the this form link fails with a Page not found 404, and the address is:
http://127.0.0.1:8000/password/
Whereas the correct URL (which works) should be:
http://127.0.0.1:8000/admin/auth/user/40/password/
After some searching I found: Lib/site-packages/django/contrib/auth/forms.py:129 :
class UserChangeForm(forms.ModelForm):
password = ReadOnlyPasswordHashField(
label=_("Password"),
help_text=_(
"Raw passwords are not stored, so there is no way to see this "
"user's password, but you can change the password using "
"this form."
),
)
But I dont see how the change password link is set in the href, it looks blank to me. I also dont want to change the Django sourcecode because then the same bug will manifest when there are updates. How to I get the link to point to the change password page which works?

Are you using a custom user model or user admin? If that's the case, your admin class should be BaseUserAdmin or inherited from that.

You are not using this UserChangeForm. For custom user admin, follow this documentation Customizing Authentication

Turned out to be something very obscure. So after a lot of reverting and modifying new config files to work with old code, managed to narrow it done to a single line in my: templates\admin\base.html, the first line of the <head> section was:
<base href="/"><!-- For CSP stylesheet href links to pass MIME TYPE checks -->
The <base> tag:
specify a default URL and a default target for all links on a page

The link in the href seems to be set in the UserChangeForm's __init__ method:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
password = self.fields.get('password')
if password:
password.help_text = password.help_text.format('../password/') # <-- here
# ...
I'm not sure why yours was taking you all the way back to localhost:8000/password/ as it's meant to go up one "directory" (from /admin/auth/user/40/change to /admin/auth/user/40) and then down to /admin/auth/user/40/password/.
The other implication of this href format is that if your URL structure doesn't have a trailing slash (eg /.../users/40/change), the ../password/ a. will take you to /.../users/password/ and you'll get a "No user with PK=password" error, and b. won't work with your URL structure anyway because it's added a trailing slash.
It does seem curious that Django doesn't have a way to override it (eg by passing the UserChangeForm a password_url or something). There might be a bug report / feature suggestion here...
As for actually dealing with it, one option, as suggested in this answer, is to manipulate your URL config to set the password change URL manually and point it to the relevant admin / auth view.
Unfortunately that doesn't help if you are subclassing or using the form elsewhere. In that case, the only solution I've thought of so far is to manually replace it in your subclass's __init__ like so:
from django.contrib.auth import forms as auth_forms
class UserEditForm(auth_forms.UserChangeForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
password = self.fields.get('password')
if password:
password.help_text = password.help_text.replace(
'../password/', kwargs.pop('password_url', './password'))
Unfortunately you have to use .replace as you can't reformat the help text once the super().__init__ has formatted it, and you can't do it before calling super().__init__ as you won't be able to access the field. This method does at least allow for passing a password_url as an argument to the form, and otherwise defaults to ./password which will work for URL structures without a trailing slash.

Related

Django two-factor authentication, require 2FA on specific views

I am implementing Django two-factor-auth on my website and I would love to have some views protected by two-FA, and some other not.
In order to do so, I use the decorator #otp_required which works great, but unfortunately asks the users to input their credentials again (to handle user sessions, I use the registration module).
Would you be able to give me a good to way to hack the form in order to just ask the user to input the token (skipping a step of the form, basically) ?
Thanks a lot,
For those who care, I found a way to do it that is quite clean.
The trick was to override the LoginView class in the core.py module of the two_factor_authentication module.
In order to do so, go to your views and insert the following code:
class CustomLoginView(LoginView):
form_list = (
('token', AuthenticationTokenForm),
('backup', BackupTokenForm),
)
def get_user(self):
self.request.user.backend = 'django.contrib.auth.backends.ModelBackend'
return self.request.user
Basically, I erase the 'auth' step and override the method get_user() in order to return the current user.
The backend must be specified otherwise Django raises an error.
Now, to make that class be used instead of the LoginView, go to your urls and insert the following line BEFORE including the two_factor.urls.
url(r'^account/login/$', tradingviews.CustomLoginView.as_view(), name='login'),
That's it!

Django creating non-auth single use access tokens

I'm creating an app where in users will be able to add reviews to specific places.
I'm creating a system in which each review will be held when it is submitted, and an email will be sent to site managers who'll be able to approve or reject the review by clicking on two distinct links.
These links will be mapped to a particular Django view function, which will receive the id of particular review being approved/rejected, and an access token to ensure the authenticity of link.
Everywhere I've search, I can only find "Auth tokens" for signing in users, which is not what I want in this case.
One way I've thought of is to create a hash of review and commend id, store a field in database about whether this token has been accessed or not. But this solution doesn't seem very "DRY".
Any way of generating these tokens?
There is a feature included with Django which you can use for this:
https://docs.djangoproject.com/en/1.9/topics/signing/
If I understood your use case correctly you can use the object id as the value to sign, then make a view which accepts the signed value as an arg, from which it can derive the id while ensuring that the link was authentic.
eg
from django.core import signing
from django.core.urlresolvers import reverse
from django.http import HttpResponseBadRequest
from django.shortcuts import get_object_or_404
signer = Signer()
def make_review_links(review_id):
# a function to generate the links you can use in the email template
signed = signer.sign(str(review_id))
approve_link = reverse('approve_review', args=[review_id])
reject_link = reverse('reject_review', args=[review_id])
return approve_link, reject_link
def approve_review(request, signed):
try:
review_id = signer.unsign(value)
except signing.BadSignature:
return HttpResponseBadRequest("Invalid link")
review = get_object_or_404(Review, pk=review_id)
# approval code here
I do agree with the commenters on your question though, as a actions that change data on the server these should be POST requests. The pattern would be the same as above, but instead of the view getting signed value from a url arg you'd get it from request.POST. You may even want to use a Form.
Obviously, to do a POST from an email is only possible with an HTML email. To support text emails you should link to an intermediary page (passing the signed value) that presents a form with Approve/Reject buttons, which should POST the signed value as hidden field to a view as above.

Include authenticated user in dictionary for all views

I am working through the Pyramid authorization tutorial and I have noticed the pattern where
logged_in = request.authenticated_userid
is added to each view dictionary. Can it be avoided? I.e. is there a configuration which automatically ads user id to each view. Or is there a way to create a base, abstract view with the user id and inherit from it?
Part of the code from the tutorial:
#view_config(context='.models.Page', renderer='templates/view.pt', permission='view')
def view_page(context, request):
# not relevant code
return dict(page = context, content = content, edit_url = edit_url,
logged_in = request.authenticated_userid)
#view_config(name='add_page', context='.models.Wiki', renderer='templates/edit.pt',
permission='edit')
def add_page(context, request):
# not relevant code
return dict(page=page, save_url=save_url,
logged_in=request.authenticated_userid)
It's been awhile since I last looked, but I think logged_in in the samples is just an example to use to conditionally check if there is a logged on user or not. You could probably just as easily refer to request.authenticated_userid within any of your views or templates, too, and get the same behavior and not have to explicitly add a status to the response dict. The request object should be available to be referenced in your view templates, too.
Alternatively, I've used their cookbook to add a user object to the request to make a friendly request.user object that I can use to both check for logged in status where needed, plus get at my other user object details if I need to as well.

How to prevent user changing URL <pk> to see other submission data Django

I'm new to the web development world, to Django, and to applications that require securing the URL from users that change the foo/bar/pk to access other user data.
Is there a way to prevent this? Or is there a built-in way to prevent this from happening in Django?
E.g.:
foo/bar/22 can be changed to foo/bar/14 and exposes past users data.
I have read the answers to several questions about this topic and I have had little luck in an answer that can clearly and coherently explain this and the approach to prevent this. I don't know a ton about this so I don't know how to word this question to investigate it properly. Please explain this to me like I'm 5.
There are a few ways you can achieve this:
If you have the concept of login, just restrict the URL to:
/foo/bar/
and in the code, user=request.user and display data only for the logged in user.
Another way would be:
/foo/bar/{{request.user.id}}/
and in the view:
def myview(request, id):
if id != request.user.id:
HttpResponseForbidden('You cannot view what is not yours') #Or however you want to handle this
You could even write a middleware that would redirect the user to their page /foo/bar/userid - or to the login page if not logged in.
I'd recommend using django-guardian if you'd like to control per-object access. Here's how it would look after configuring the settings and installing it (this is from django-guardian's docs):
>>> from django.contrib.auth.models import User
>>> boss = User.objects.create(username='Big Boss')
>>> joe = User.objects.create(username='joe')
>>> task = Task.objects.create(summary='Some job', content='', reported_by=boss)
>>> joe.has_perm('view_task', task)
False
If you'd prefer not to use an external library, there's also ways to do it in Django's views.
Here's how that might look:
from django.http import HttpResponseForbidden
from .models import Bar
def view_bar(request, pk):
bar = Bar.objects.get(pk=pk)
if not bar.user == request.user:
return HttpResponseForbidden("You can't view this Bar.")
# The rest of the view goes here...
Just check that the object retrieved by the primary key belongs to the requesting user. In the view this would be
if some_object.user == request.user:
...
This requires that the model representing the object has a reference to the User model.
In my project, for several models/tables, a user should only be able to see data that he/she entered, and not data that other users entered. For these models/tables, there is a user column.
In the list view, that is easy enough to implement, just filter the query set passed to the list view for model.user = loggged_id.user.
But for the detail/update/delete views, seeing the PK up there in the URL, it is conceivable that user could edit the PK in the URL and access another user's row/data.
I'm using Django's built in class based views.
The views with PK in the URL already have the LoginRequiredMixin, but that does not stop a user from changing the PK in the URL.
My solution: "Does Logged In User Own This Row Mixin"
(DoesLoggedInUserOwnThisRowMixin) -- override the get_object method and test there.
from django.core.exceptions import PermissionDenied
class DoesLoggedInUserOwnThisRowMixin(object):
def get_object(self):
'''only allow owner (or superuser) to access the table row'''
obj = super(DoesLoggedInUserOwnThisRowMixin, self).get_object()
if self.request.user.is_superuser:
pass
elif obj.iUser != self.request.user:
raise PermissionDenied(
"Permission Denied -- that's not your record!")
return obj
Voila!
Just put the mixin on the view class definition line after LoginRequiredMixin, and with a 403.html template that outputs the message, you are good to go.
In django, the currently logged in user is available in your views as the property user of the request object.
The idea is to filter your models by the logged in user first, and then if there are any results only show those results.
If the user is trying to access an object that doesn't belong to them, don't show the object.
One way to take care of all of that is to use the get_object_or_404 shortcut function, which will raise a 404 error if an object that matches the given parameters is not found.
Using this, we can just pass the primary key and the current logged in user to this method, if it returns an object, that means the primary key belongs to this user, otherwise it will return a 404 as if the page doesn't exist.
Its quite simple to plug it into your view:
from django.shortcuts import get_object_or_404, render
from .models import YourModel
def some_view(request, pk=None):
obj = get_object_or_404(YourModel, pk=pk, user=request.user)
return render(request, 'details.html', {'object': obj})
Now, if the user tries to access a link with a pk that doesn't belong to them, a 404 is raised.
You're going to want to look into user authentication and authorization, which are both supplied by [Django's Auth package] (https://docs.djangoproject.com/en/4.0/topics/auth/) . There's a big difference between the two things, as well.
Authentication is making sure someone is who they say they are. Think, logging in. You get someone to entire their user name and password to prove they are the owner of the account.
Authorization is making sure that someone is able to access what they are trying to access. So, a normal user for instance, won't be able to just switch PK's.
Authorization is well documented in the link I provided above. I'd start there and run through some of the sample code. Hopefully that answers your question. If not, hopefully it provides you with enough information to come back and ask a more specific question.
This is a recurring question and also implies a serious security flaw. My contribution is this:
There are 2 basic aspects to take care of.
The first is the view:
a) Take care to add a decorator to the function-based view (such as #login_required) or a mixin to the class-based function (such as LoginRequiredMixin). I find the official Django documentation quite helpful on this (https://docs.djangoproject.com/en/4.0/topics/auth/default/).
b) When, in your view, you define the data to be retrieved or inserted (GET or POST methods), the data of the user must be filtered by the ID of that user. Something like this:
def get(self, request, *args, **kwargs):
self.object = self.get_object(queryset=User.objects.filter(pk=self.request.user.id))
return super().get(request, *args, **kwargs)
The second aspect is the URL:
In the URL you should also limit the URL to the pk that was defined in the view. Something like this:
path('int:pk/blog-add/', AddBlogView.as_view(), name='blog-add'),
In my experience, this prevents that an user sees the data of another user, simply by changing a number in the URL.
Hope it helps.
In django CBV (class based views) you can prevent this by comparing the
user entered pk and the current logged in user:
Note: I tested it in django 4 and python 3.9.
from django.http import HttpResponseForbidden
class UserDetailView(LoginRequiredMixin, DetailView):
model = your_model
def dispatch(self, request, *args, **kwargs):
if kwargs.get('pk') != self.request.user.pk:
return HttpResponseForbidden(_('You do not have permission to view this page'))
return super().dispatch(request, *args, **kwargs)

Django user impersonation by admin

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:
What would be the easiest way to implement something like this?
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.
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
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
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
#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)
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.
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.

Categories

Resources