How to restrict non staff users from accessing django-admin - django

My django-admin page is located at
http://127.0.0.1:8000/admin/
Suppose there are 3 users in my website.
Superuser
Staff
End user
If anyone of these three users tries to access this link http://127.0.0.1:8000/admin/, 1st and 2nd can access it.
And End User is not able to access it. Till this, it is fine.
What I want is to do is to show Error 404 to End User. He/She should never know that this link is for admin page. Moreover, if anyone is not logged in, they should also see same 404 Page.
I have referred this link but it takes me to another default page.
PFA for the same
PS: I am using Signin With Google, so it should not redirect me there.
I am trying this since 3 continous days, so any help is highly appreciated. Thanks in advance.
Django-Version: 3. 0. 5

You first need to make a custom decorator that would give a 404 if the user is not a staff:
from django.http import Http404
from functools import wraps
def staff_required(func):
#wraps(func)
def wrapper(request, *args, **kwargs):
if request.user.is_staff:
return func(request, *args, **kwargs)
raise Http404()
return wrapper
Next we will use this decorator as described in your linked question,:
from django.contrib import admin
admin.site.login = staff_required(admin.site.login)
urlpatterns = [
path('admin/', admin.site.urls),
]
Edit: Above method was a bit hacky it would show the login pages url to the user even if it gives a 404 error. It would be better to make a custom admin site class and use it. The admin site has a method admin_view which decorates the admin views which we shall override. We shall do this in the file admin.py in the projects main app (let's say the project is named myproject)
from functools import update_wrapper
from django.contrib import admin
from django.http import (
Http404, HttpResponseRedirect,
)
from django.urls import reverse
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
class MyAdminSite(admin.AdminSite):
def admin_view(self, view, cacheable=False):
def inner(request, *args, **kwargs):
if not self.has_permission(request):
if request.path == reverse('admin:logout', current_app=self.name):
index_path = reverse('admin:index', current_app=self.name)
return HttpResponseRedirect(index_path)
raise Http404()
return view(request, *args, **kwargs)
if not cacheable:
inner = never_cache(inner)
# We add csrf_protect here so this function can be used as a utility
# function for any view, without having to repeat 'csrf_protect'.
if not getattr(view, 'csrf_exempt', False):
inner = csrf_protect(inner)
return update_wrapper(inner, view)
Now we need to replace the default admin site with our custom admin site. To do this we will follow Overriding the default admin site [Django docs]:
In the projects main app's apps.py file (assuming project to be named myproject):
from django.contrib.admin.apps import AdminConfig
class MyAdminConfig(AdminConfig):
default_site = 'myproject.admin.MyAdminSite'
Now in settings.py we will replace 'django.contrib.admin' with our own config class:
INSTALLED_APPS = [
...
'myproject.apps.MyAdminConfig', # replaces 'django.contrib.admin'
...
]

A simpler way is to create your own middleware that whenever the path starts with /admin/ and the user is logged in but doesn't have is_staff set to True (both staff and superusers have it), then raise the 404.
from django.http import Http404
class NoDjangoAdminForEndUserMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.path.startswith("/admin/"):
if request.user.is_authenticated and not request.user.is_staff:
raise Http404()
response = self.get_response(request)
return response
Then, to activate it, add it to the MIDDLEWARE list in your Django settings.
You'll still see the URL with http://127.0.0.1:8000/admin/, despite seeing a 404 page. If you want to redirect the user to a different page, instead of raising the Http404(), then return an HttpResponseRedirect().

Related

How to disable "?next=" parameter for Django Admin to avoid Page Enumeration Attacks?

I'd like to disable the ?next=... parameter that Django Admin automatically sets if you try to access a page that's protected by the admin panel. I haven't been able to find a solution to do this so far. Does anyone know how to achieve this?
The reason why I want to do this is to avoid page enumeration attacks.
Found the answer myself after some trial & error.
I needed to create my custom AdminSite, and then provide my own custom admin_view, which does the redirect. Then, in the redirect, I just set the redirect_field_name to None like so:
def admin_view(self, view, cacheable=False):
"""
Decorator to create an admin view attached to this ``AdminSite``. This
wraps the view and provides permission checking by calling
``self.has_permission``.
You'll want to use this from within ``AdminSite.get_urls()``:
class MyAdminSite(AdminSite):
def get_urls(self):
from django.urls import path
urls = super().get_urls()
urls += [
path('my_view/', self.admin_view(some_view))
]
return urls
By default, admin_views are marked non-cacheable using the
``never_cache`` decorator. If the view can be safely cached, set
cacheable=True.
"""
def inner(request, *args, **kwargs):
if not self.has_permission(request):
if request.path == reverse('admin:logout', current_app=self.name):
index_path = reverse('admin:index', current_app=self.name)
return HttpResponseRedirect(index_path)
# Inner import to prevent django.contrib.admin (app) from
# importing django.contrib.auth.models.User (unrelated model).
from django.contrib.auth.views import redirect_to_login
return redirect_to_login(
request.get_full_path(),
reverse('admin:login', current_app=self.name),
redirect_field_name=None # <-- Set this to None to disable the "?next=" parameter.
)
return view(request, *args, **kwargs)
if not cacheable:
inner = never_cache(inner)
# We add csrf_protect here so this function can be used as a utility
# function for any view, without having to repeat 'csrf_protect'.
if not getattr(view, 'csrf_exempt', False):
inner = csrf_protect(inner)
return update_wrapper(inner, view)

Django Update Middleware to replace decorator

I have the following Decorator which works fine when applied to different views with: #otp_required(login_url='login') on my site:
Decorator
from django.contrib.auth.decorators import user_passes_test
from django_otp import user_has_device
from django_otp.conf import settings
def otp_required(view=None, redirect_field_name='next', login_url=None, if_configured=False):
"""
Similar to :func:`~django.contrib.auth.decorators.login_required`, but
requires the user to be :term:`verified`. By default, this redirects users
to :setting:`OTP_LOGIN_URL`.
:param if_configured: If ``True``, an authenticated user with no confirmed
OTP devices will be allowed. Default is ``False``.
:type if_configured: bool
"""
if login_url is None:
login_url = settings.OTP_LOGIN_URL
def test(user):
return user.is_verified() or (if_configured and user.is_authenticated and not user_has_device(user))
decorator = user_passes_test(test, login_url=login_url, redirect_field_name=redirect_field_name)
return decorator if (view is None) else decorator(view)
However, I’d like to convert this into a Middleware as I want to avoid having to apply a decorator to every view on my site, but not managed to get working.
I tried to amend the following Middleware which I currently have in place which is just for authorised users and has been working but as per above decorator I want this Middleware extended to also have OTP required as well:
Middleware
from django.utils.deprecation import MiddlewareMixin
from django.urls import resolve, reverse
from django.http import HttpResponseRedirect
from wfi_workflow import settings
from django_otp import user_has_device
from django_otp.decorators import otp_required
from django_otp.middleware import is_verified
class LoginRequiredMiddleware(MiddlewareMixin):
"""
Middleware that requires a user to be authenticated to view any page other
than LOGIN_URL. Exemptions to this requirement can optionally be specified
in settings by setting a tuple of routes to ignore
"""
##otp_required(login_url='login')
def process_request(self, request):
assert hasattr(request, 'user'), """
The Login Required middleware needs to be after AuthenticationMiddleware.
Also make sure to include the template context_processor:
'django.contrib.account.context_processors.account'."""
if not request.user.is_verified() and not request.path.startswith('/admin/') and not request.path.startswith('/account/' ):
current_route_name = resolve(request.path_info).url_name
if not current_route_name in settings.AUTH_EXEMPT_ROUTES:
return HttpResponseRedirect(reverse(settings.LOGIN_URL))
Help is much appreciated.
The fact that you return a HttpResponseRedirect will not work: Django's MiddlewareMixin will simply call the function to (optionally) alter the request, but it will never take the return into account.
What you can do is define middleware in a decorator-like structure, and return the HttpResponseRedirect in case the user should be authenticated with:
from django.urls import resolve, reverse
from django.http import HttpResponseRedirect
from wfi_workflow import settings
def OTPRequiredMiddleware(get_response):
"""
Middleware that requires a user to be authenticated to view any page other
than LOGIN_URL. Exemptions to this requirement can optionally be specified
in settings by setting a tuple of routes to ignore
"""
def middleware(request):
from django_otp import user_has_device
if not user.is_verified() and not (if_configured and user.is_authenticated and not user_has_device(user)):
return HttpResponseRedirect(settings.OTP_LOGIN_URL)
return get_response(request)

django-allauth login_redirect page with username as slug

I am using Django 3.2 and django-allauth 0.44
I have set my LOGIN_REDIRECT_URL in settings.py as follows:
LOGIN_REDIRECT_URL = 'profile-page'
in urls.py, I have the following route defined:
path('accounts/profile/slug:username', AccountProfileView.as_view(), name='profile-page'),
When I log in, (unsurprisingly), I get the following error message:
NoReverseMatch at /accounts/login/
Reverse for 'profile-page' with no arguments not found. 1 pattern(s) tried: ['accounts/profile/(?P[-a-zA-Z0-9_]+)$']
How do I pass (or specify) a parameter of the logged in user's username to the route?
If your view needs to perform redirects which are not very simple, you need to override the get_success_url method, considering that you use django-allauth, you will need to override allauth.account.views.LoginView and also write your own url pattern for it so that your overriden view is used. First override the view:
from django.urls import reverse
from allauth.account.views import LoginView as AllauthLoginView
from allauth.account.utils import get_next_redirect_url
class LoginView(AllauthLoginView):
def form_valid(self, form):
self.user = form.user # Get the forms user
return super().form_valid(form)
def get_success_url(self):
ret = (
get_next_redirect_url(self.request, self.redirect_field_name)
or reverse('profile-page', kwargs={'username': self.user.username})
)
return ret
Next wherever you define the urls for allauth, just add your own url before it:
from path_to.view_above import LoginView # Change this import properly
urlpatterns = [
...
path('accounts/login/', LoginView.as_view(), name="account_login"),
path('accounts/', include('allauth.urls')),
...
]
Another alternative solution using allauth is to use a custom ACCOUNT_ADAPTER and override it's get_login_redirect_url because LoginView will internally call it in case there is no next parameter. To do this, first inherit from allauth.account.adapter.DefaultAccountAdapter:
from django.urls import reverse
from allauth.account.adapter import DefaultAccountAdapter
class MyAccountAdapter(DefaultAccountAdapter):
def get_login_redirect_url(self, request):
return reverse('profile-page', kwargs={'username': request.user.username})
Next in settings.py set ACCOUNT_ADAPTER:
ACCOUNT_ADAPTER = "path.to.MyAccountAdapter"
The LOGIN_REDIRECT_URL should point to the page on which user lands after he successfully logs into your website, unless he was redirected to the login page by trying to visit any of the pages that require the authorization.
If you want to redirect user to a specific page, in this example to his own profile page, you can create an intermediate URL that will redirect the user to his own profile page, when visited. It can be accomplished like so:
Using class-based views:
class CurrentUserProfileRedirectView(LoginRequiredMixin, RedirectView):
def get_redirect_url(self, *args, **kwargs):
return reverse('profile-page', kwargs={'username': request.user.username})
or using function-based views:
#login_required
def current_user_profile(request):
return redirect('profile-page', username=request.user.username)
Next, register this redirect view as a regular view, not requiring any parameters, and set the LOGIN_REDIRECT_URL to this view's name.

How to restrict website users from seeing other user profiles?

I created some users for my website in Django and i want each user to access only his own profile page .
The template for the user profile page is fetched through a CBV Detail View called UserDetailView attached to a URL containing the user's and the page is loading only after authentication .( user logged in). So far so good.
urls.py:
from django.conf.urls import url
from django.urls import path
from basicapp import views
from django.contrib.auth.decorators import login_required
app_name='basicapp'
urlpatterns = [
url(r'^$',views.index,name='index'),
url(r'^user_list/',views.UserView.as_view(),name='user_list'),
url(r'^course_list/',views.CourseView.as_view(),name='course_list'),
url(r'^user_detail/(?P<pk>[-\w]+)/$',views.UserDetailView.as_view(),name='user_detail'),
]
The problem is after I login and get the user detail page : If I manually change the <pk> in the URL I get other user profile pages loaded. I don't want that to happen .
For ex, the URL for the logged in user profile is :
http://127.0.0.1:8000/basicapp/user_detail/1/
With the user already logged in i manually change the URL to :
http://127.0.0.1:8000/basicapp/user_detail/2/
and it works. It should retrict me or show me an error message
I tried using LoginRequiredMixin
views.py:
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.utils.decorators import method_decorator
class UserDetailView(LoginRequiredMixin,DetailView):
context_object_name='user_detail'
model=models.User
template_name='basicapp/user_detail.html'
raise_exception = True # Raise exception when no access instead of redirect
permission_denied_message = "This page dows not exist."
and I also tried usingmethod_decorator :
#method_decorator(login_required)
class UserDetailView(LoginRequiredMixin,DetailView):
context_object_name='user_detail'
model=models.User
template_name='basicapp/user_detail.html'
raise_exception = True # Raise exception when no access instead of redirect
permission_denied_message = "This page dows not exist."
but it doesn't seem to work . I restarted the server.
Any ideas what i am doing wrong?
The LoginRequiredMixin will ensure that you can only see the page if you are logged in, but that does not mean you have to be that user.
However if you can only see your own profile, it does not make much sense to add a primary key in the url anyway, you can just define the url as:
url(r'^user_detail/$', views.UserDetailView.as_view(), name='user_detail'),
In the view you then return the logged in user for the .get_object() method [Django-doc]:
class UserDetailView(LoginRequiredMixin,DetailView):
context_object_name='user_detail'
model=models.User
template_name='basicapp/user_detail.html'
def get_object(self, *args, **kwargs):
return self.request.user
Or you can restrict users by filtering the queryset:
path('^user_detail/<int:pk>/', views.UserDetailView.as_view(), name='user_detail'),
class UserDetailView(LoginRequiredMixin,DetailView):
context_object_name='user_detail'
model=models.User
template_name='basicapp/user_detail.html'
def get_queryset(self, *args, **kwargs):
qs = super().get_queryset(*args, **kwargs)
if not self.request.user.is_superuser:
qs = qs.filter(pk=self.request.user.pk)
return qs

Set all pages to require login, globally?

I want to redirect access of unauthenticated users to the login page, after which the logged-in user should be redirected to the originally requested page.
According to documentation, this is easily achieved using the #user_passes_test decorator. But it seems I'd have to decorate every view, which is crazy, there are too many and it's error-prone.
What is a good way to turn on this functionality globally (except for a small fixed set of views, such as login)? That is, default everything to logged-in-only + handle anonymous viewing explicitly, where needed.
from django.shortcuts import redirect
from django.conf import settings
class LoginRequiredMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.login_url = settings.LOGIN_URL
self.open_urls = [self.login_url] + \
getattr(settings, 'OPEN_URLS', [])
def __call__(self, request):
if not request.user.is_authenticated \
and not request.path_info in self.open_urls:
return redirect(self.login_url+'?next='+request.path)
return self.get_response(request)
have a look at middleware. these are functions run at various points in the request cycle, e.g. before each view is called.
since you may want to exclude certain views from this, i'd look at e.g. how the csrf middleware works, together with the csrf_exempt decorator.
see [SOURCE]/django/views/decorators/csrf.py and [SOURCE]/django/middleware/csrf.py
The way I solved this, was to have mixin class, with the decorator (or whatever code you need). Although you have to remember to call the super(Class, self).get(...) function, so I guess it's not so different after all.
On the other hand, having a set of mixins that does different things I found was quite good at getting a very simple view to do a lot without much code.
Edit
This is how I did in my last project:
class BaseAuthMixin(object):
def auth_check(self, user):
return True
def dispatch(self, request, *args, **kwargs):
if not self.auth_check(request.user):
from django.http import HttpResponseRedirect
from django.contrib.auth import logout
is_web = False
is_live = False
if hasattr(self, 'get_url_name'):
from django.core.urlresolvers import reverse
from django.core.urlresolvers import NoReverseMatch
try:
next = reverse(self.get_url_name(), kwargs=kwargs)
except NoReverseMatch:
next = ''
else:
next= '?next=' + next
logout(request)
redirect_url = settings.LOGIN_URL
redirect_url += next
return HttpResponseRedirect(redirect_url)
else:
return super(BaseAuthMixin, self).dispatch(request, *args, **kwargs)
class LoginRequiredMixin(BaseAuthMixin):
"""
Check if the view needs the user to be logged in.
"""
def auth_check(self, user):
if not super(LoginRequiredMixin, self).auth_check(user):
return False
else:
if hasattr(self, 'login_required'):
if self.login_required and not user.is_authenticated():
return False
return True
class MyDefaultMixin(LoginRequiredMixin):
"""
Mixin that inherits from all common view mixins.
"""
pass
The above is then used by the view-classes (I used Django 1.3 with class-based views):
from django.views.generic import TemplateView
class SomeViewClass(TemplateView, MyDefaultMixin):
# Used by 'LoginRequiredMixin' to check if a user has to be logged in
login_required = True
# Template for the Django TemplateView
template_name = "some_view_template.html"
You need a view to handle the login (with URL in settings.LOGIN_URL), containing a form with a hidden field called next. This field has to be set by a context variable to the page to go to after successful login.
If all views inherit from the base mixin (MyDefaultMixin in my code above), it will automatically check that the user is logged in iv the view contain an attribute called login_required and that is set to True.
There might be better ways to do this, but this is what I did and it worked very well.