Using django-cas-ng to authenticate on admin site - python

I'm using django-cas-ng framework to authenticate users. The main problem is that the admin page still uses the default login view.
Methods used this far:
1.- Using env var
From docs:
CAS_ADMIN_PREFIX: The URL prefix of the Django administration site. If undefined, the CAS middleware will check the view being rendered to see if it lives in django.contrib.admin.views.
2.- Redirecting url on app/urls.py:
url(r'^arta/admin/login$', django_cas_ng.views.login, name='cas_ng_login')
Everything is just ignored and the admin login form is shown.
The goal of this is to only authenticate with CAS and redirect the current /app/admin/login to CAS

If anyone is interested in the answer, the solution was overriding AdminSite. Django admin module overrides it's own url redirects, so editing them on /app/urls.py was useless.
Creating an /app/admin.py and extending AdminSite like:
from django.contrib.admin import AdminSite
from functools import update_wrapper
from django.urls import NoReverseMatch, reverse
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.http import Http404, HttpResponseRedirect
import django_cas_ng.views
class Admin(AdminSite):
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.conf.urls import url
urls = super(MyAdminSite, self).get_urls()
urls += [
url(r'^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('cas_ng_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('cas_ng_login', current_app=self.name)
)
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)
def get_urls(self):
from django.conf.urls import url, include
# Since this module gets imported in the application's root package,
# it cannot import models from other applications at the module level,
# and django.contrib.contenttypes.views imports ContentType.
from django.contrib.contenttypes import views as contenttype_views
def wrap(view, cacheable=False):
def wrapper(*args, **kwargs):
return self.admin_view(view, cacheable)(*args, **kwargs)
wrapper.admin_site = self
return update_wrapper(wrapper, view)
# Admin-site-wide views.
urlpatterns = [
url(r'^$', wrap(self.index), name='index'),
url(r'^login/$', django_cas_ng.views.login, name='login'),
url(r'^logout/$', django_cas_ng.views.logout, name='logout'),
url(r'^password_change/$', wrap(self.password_change, cacheable=True), name='password_change'),
url(r'^password_change/done/$', wrap(self.password_change_done, cacheable=True),
name='password_change_done'),
url(r'^jsi18n/$', wrap(self.i18n_javascript, cacheable=True), name='jsi18n'),
url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$', wrap(contenttype_views.shortcut),
name='view_on_site'),
]
# Add in each model's views, and create a list of valid URLS for the
# app_index
valid_app_labels = []
for model, model_admin in self._registry.items():
urlpatterns += [
url(r'^%s/%s/' % (model._meta.app_label, model._meta.model_name), include(model_admin.urls)),
]
if model._meta.app_label not in valid_app_labels:
valid_app_labels.append(model._meta.app_label)
# If there were ModelAdmins registered, we should have a list of app
# labels for which we need to allow access to the app_index view,
if valid_app_labels:
regex = r'^(?P<app_label>' + '|'.join(valid_app_labels) + ')/$'
urlpatterns += [
url(regex, wrap(self.app_index), name='app_list'),
]
return urlpatterns
site = Admin()
Then you override the methods that you want, in this case were admin_view and get_urls
The interesting lines are:
→ admin_view
if request.path == reverse('cas_ng_logout', current_app=self.name):
----
reverse('cas_ng_login', current_app=self.name)
→ get_urls
url(r'^login/$', django_cas_ng.views.login, name='login'),
url(r'^logout/$', django_cas_ng.views.logout, name='logout')
That will let you redirect the login and logout steps into CAS

For being now (April, 2022), we can use builtin middleware of django-cas-ng (v4.x) as below:
MIDDLEWARE_CLASSES = (
...
'django_cas_ng.middleware.CASMiddleware',
...
)
You may add these url pattern in url.py as well, don't change the url name.
urlpatterns = [
...
path('accounts/login/', cas_ng_views.LoginView.as_view(), name='cas_ng_login'),
path('accounts/logout/', cas_ng_views.LogoutView.as_view(), name='cas_ng_logout'),
path('accounts/callback/',cas_ng_views.CallbackView.as_view(), name='cas_ng_proxy_callback'),
...
]

Related

Catch-all view break URL patterns in Django

def get_urls(self):
urls = super().get_urls()
url_patterns = [path("admin_profile", self.admin_view(self.profile_view))]
return urls + url_patterns
The above method cause the catch-all view to break URL patterns which I route after the admin URLs and cause the following error/exception:
Page not found (404) Request Method: GET Request
URL: http://127.0.0.1:8000/admin/admin_profile/ Raised
by: django.contrib.admin.sites.catch_all_view
But this only happen when
final_catch_all_view = True
in
django.contrib.admin.sites.catch_all_view
When setting
final_catch_all_view = False
No error or exception is made, everything went fine.
Now my question is how can make the function work when
final_catch_all_view = True
And this is what docs say about catch_all view:
> The new admin catch-all view will break URL patterns routed after the
> admin URLs and matching the admin URL prefix. You can either adjust
> your URL ordering or, if necessary, set AdminSite.final_catch_all_view
> to False, disabling the catch-all view. See What’s new in Django 3.2
> for more details.
Class that urls are appended for:
from django.contrib import admin
from django.template.response import TemplateResponse
from django.urls import path
class BookrAdmin(admin.AdminSite):
site_header = "Bookr Administration"
logout_template = "admin/logout.html"
def profile_view(self, request):
request.current_app = self.name
context = self.each_context(request)
return TemplateResponse(request, "admin/admin_profile.html", context)
def get_urls(self):
urls = super().get_urls()
url_patterns = [path("admin_profile", self.admin_view(self.profile_view))]
return url_patterns + urls
urls.py:
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from reviews import views
urlpatterns = [
path('accounts/', include(('django.contrib.auth.urls', 'auth'), namespace='accounts')),
path('accounts/profile/', views.profile, name='profile'),
path('admin/', admin.site.urls),
path('', include('reviews.urls')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
The docs seem to indicate that you need to specify your custom URLs before the django admin URLs. In your get_urls override function, you're appending rather than prepending the custom url. Try the following:
def get_urls(self):
urls = super().get_urls()
url_patterns = [path("admin_profile", self.admin_view(self.profile_view))]
return url_patterns + urls

Django 2.2 : Is there any way to remove app name from admin urls?

I am using Django Built-in Admin panel, is there any way to remove app name from urls?
If I want to access User listing, it redirects me to 27.0.0.1:8000/admin/auth/user/ can I make it 27.0.0.1:8000/admin/user/ without the app name auth?
Thanks,
As documented here you can create a custom AdminSite and override the get_urls method. This simple code should to the job:
In your common.admin.py
from django.contrib.admin import AdminSite
class MyAdminSite(AdminSite):
def get_urls(self):
urlpatterns = super().get_urls()
for model, model_admin in self._registry.items():
urlpatterns += [
path('%s/' % (model._meta.model_name), include(model_admin.urls)),
]
return urlpatterns
my_admin = MyAdminSite('My Admin')
my_admin.register(YourModel)
...
Note that you register your models with the new custom AdminSite instance.
Then in your projects urls.py
from common.admin import my_admin as admin
from django.urls import path
urlpatterns = [
path('admin/', admin.urls),
# Your other patterns
]

How do I test endpoints registered with Wagtail Hooks?

Background
OK, I have a library wagtail_references which, like many snippet libraries, uses wagtail hooks to add admin views into the wagtail CMS.
Say I have, in my wagtail_hooks.py
#hooks.register('register_admin_urls')
def register_admin_urls():
return [
url(r'^references/', include(admin_urls, namespace='wagtailreferences')),
]
And the views that get registered are in views/reference.py:
#permission_checker.require('add')
def add(request):
Reference = get_reference_model()
ReferenceForm = get_reference_form(Reference)
if request.method == 'POST':
# STUFF I WANT TO TEST BECAUSE IT DOESN'T WORK PROPERLY
else:
form = ReferenceForm(user=request.user)
return render(request, "wagtail_references/references/add.html", {'form': form})
So in my test_stuff.py file, I'd have:
class TestReferenceIndexView(TestCase, WagtailTestUtils):
def setUp(self):
self.login()
def post(self, params=None):
params = params if params else {}
return self.client.post(reverse('wagtailreferences:add'), params)
def test_simple(self):
response = self.post()
self.assertEqual(response.status_code, 201)
The Problem
But test_simple fails, because of course the urls for the view its testing are hooked in dynamically, not defined in urls.py. I get:
django.urls.exceptions.NoReverseMatch: 'wagtailreferences' is not a registered namespace
The question
How can I test endpoints whose URLs are registered by wagtail hooks?
I've tried
Registering the hooks manually in the test case, like:
class TestReferenceIndexView(TestCase, WagtailTestUtils):
def setUp(self):
self.register_hook('register_admin_urls', register_admin_urls)
DOH! I hadn't registered the admin urls in my test app.
tests/urls.py looked like this:
from django.conf.urls import include, url
from wagtail.core import urls as wagtail_urls
urlpatterns = [
url(r'', include(wagtail_urls)),
]
But now looks like this:
from django.conf.urls import include, url
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.core import urls as wagtail_urls
urlpatterns = [
url(r'^admin/', include(wagtailadmin_urls)),
url(r'', include(wagtail_urls)),
]
Fixed. Sigh.

Make new custom view at django admin

Sorry, I am still new at django. I want to make custom view at admin site that is not related to my model. I have read the documentation (https://docs.djangoproject.com/en/2.0/ref/contrib/admin/#django.contrib.admin.ModelAdmin.get_urls), but does not work. Reading some tutorials does not work too...
Here is what I tried:
admin.py
from django.contrib import admin
from django.urls import path
from .models import Question
from django.http import HttpResponse
class CustomAdminView(admin.ModelAdmin):
def get_urls(self):
urls = super().get_urls()
my_urls = [
path(r'^my_view/$', self.admin_site.admin_view(self.my_view))
]
urls = my_urls + urls
return urls
def my_view(self, request):
return HttpResponse("Hello, world.")
admin.site.register(Question)
urls.py
from django.contrib import admin
from django.urls import path
from django.conf.urls import include, url
admin.autodiscover()
urlpatterns = [
path(r'polls/',include('polls.urls')),
path('admin/', admin.site.urls),
]
when I go to admin/my_view the result is 404 not found.
I tried by extending the AdminView too.
admin.py
from django.contrib.admin import AdminSite
from django.urls import path
from .models import Question
from django.http import HttpResponse
class CustomAdminView(AdminSite):
def get_urls(self):
urls = super().get_urls()
my_urls = [
path(r'my_view/', self.admin_view(self.my_view))
]
urls = my_urls + urls
return urls
def my_view(self, request):
return HttpResponse("Hello, world.")
custom_admin = CustomAdminView()
custom_admin.register(Question)
urls.py
from django.contrib import admin
from django.urls import path
from django.conf.urls import include, url
from polls.admin import custom_admin
admin.autodiscover()
urlpatterns = [
path(r'polls/',include('polls.urls')),
path('admin/', custom_admin.urls),
]
I don't get 404 error on admin/my_view. But, the default models(user, and others) are not displayed. There is only my 'Question' model there. The previous one still has the default models.
How can I make the custom admin view with the right way?
Thanks.
It is solved. I am using my second admin.py and urls.py snippets and register django's default model, based on this answer: Django (1.10) override AdminSite
admin.py
from django.contrib.admin import AdminSite
from django.http import HttpResponse
from django.urls import path
from .models import Question
from django.contrib.auth.models import Group, User #add these moduls
from django.contrib.auth.admin import GroupAdmin, UserAdmin #and these
class CustomAdminView(AdminSite):
def get_urls(self):
urls = super().get_urls()
my_urls = [
path(r'my_view/', self.admin_view(self.my_view))
]
urls = my_urls + urls
return urls
def my_view(self, request):
return HttpResponse("Hello, world.")
custom_admin = CustomAdminView()
custom_admin.register(Question)
#register the default model
custom_admin.register(Group, GroupAdmin)
custom_admin.register(User, UserAdmin)

Django Write Authentication Redirect for Blackboxed Route URLs

I'm using a django library called django-dashing that has a predefined set of urls that render a dashboard. I import them like this
urlpatterns = [
...
url(r'^dashboard/', include(dashing_router.urls)),
...
]
I want the router to be accessible only by administrators, which I can do with some config settings within django-dashing. However, when a non-admin user attempts to access /dashboard/, I want to redirect them to django's /admin/ panel to have them log in, instead of throwing the 403 that django-dashing does.
Since the django-dashing views are effectively blackboxed, I was wondering if there was a way to write a 'pre-view' that would intercept the request to /dashboard/, run some code – specifically, doing the appropriate redirects – and then continue onto the actual dashboard.
I know this would be easy enough to do by writing two urls, like /dashboard-auth/ which redirects to /dashboard/, but I don't want the user to have to go to one URL to get to another
Any suggestions?
A Django simple custom middleware is another option...
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
class DashingRedirectMiddleware(object):
def process_request(self, request):
if request.path.startswith('/dashing/') and not request.user.is_staff:
return HttpResponseRedirect(reverse('admin:login'))
return
Don't forget to add this middleware to your DJANGO SETTINGS...
MIDDLEWARE_CLASSES = [
...
'django.contrib.auth.middleware.AuthenticationMiddleware',
'yourapp.middleware.DashingRedirectMiddleware',
...
]
Or something like that.
The way I would do it is by overridding dashing's default router. All of the urls are generated dynamically by the Router class, so by overriding the get_urls method, you can wrap each function in the staff_member_required decorator.
from django.contrib.admin.views.decorators import staff_member_required
from django.conf.urls import url
from dashing.utils import Router
from dashing.views import Dashboard
class AdminRequiredRouter(Router):
def get_urls(self):
urlpatterns = [
url(r'^$', staff_member_required(Dashboard.as_view()), name='dashboard'),
]
for widget, basename, parameters in self.registry:
urlpatterns += [
url(r'/'.join((
r'^widgets/{}'.format(basename),
r'/'.join((r'(?P<{}>{})'.format(parameter, regex)
for parameter, regex in parameters.items())),
)),
staff_member_required(widget.as_view()),
name='widget_{}'.format(basename)),
]
return urlpatterns
router = AdminRequiredRouter()
Then include your router instead of dashing's
from .router import router
urlpatterns = [
...
url(r'^dashboard/', include(router.urls)),
...
]
If you are willing to look inside the 'black box' of the dashing urls, you can see that the /dashboard/ is handled by the Dashboard view. You could subclass this view, and catch the PermissionDenied error.
from django.core.exceptions import PermissionDenied
from dashing.views import Dashboard
class MyDashboard(Dashboard):
def get(self, request, *args, **kwargs):
try:
return super(MyDashboard, self).get(request, *args, **kwargs)
except PermissionDenied:
return redirect('/admin/')
Then add your view above the dashing urls:
urlpatterns = [
...
url(r'^dashboard/$', MyDashboard.as_view())
url(r'^dashboard/', include(dashing_router.urls)),
...
]

Categories