Flask-Admin & Authentication: "/admin" is protected but "/admin/anything-else" is not - python

I'm trying to customize my Admin views with Flask and Flask-SuperAdmin, however, the index view and subviews are apparently not using the same is_accessible method:
EDIT: I managed to figure out what I was doing wrong. I needed to define is_accessible in every view class. This is well-accomplished with a mixin-class, as show in the fixed code:
app/frontend/admin.py (FIXED & WORKING CODE)
from flask.ext.security import current_user, login_required
from flask.ext.superadmin import expose, AdminIndexView
from flask.ext.superadmin.model.base import ModelAdmin
from ..core import db
# all admin views should subclass AuthMixin
class AuthMixin(object):
def is_accessible(self):
if current_user.is_authenticated() and current_user.has_role('Admin'):
return True
return False
# the view that gets used for the admin home page
class AdminIndex(AuthMixin, AdminIndexView):
# use a custom template for the admin home page
#expose('/')
def index(self):
return self.render('admin/index.jade')
# base view for all other admin pages
class AdminBase(AuthMixin, ModelAdmin): # AuthMixin must come before ModelAdmin!
"""A base class for customizing admin views using our DB connection."""
session = db.session
# customize the form displays for User and Role models
class UserAdmin(AdminBase):
list_display = ('email',)
search_fields = ('email',)
exclude = ['password',]
#fields_order = ['email', 'active', 'last_login_at',]
class RoleAdmin(AdminBase):
field_args = {'name': {'label': 'Role Name'},
'description': {'description': "Duties & Responsibilities"}}
list_display = ('name', 'description')
Then set up the Flask app with our admin views:
apps/factory.py
app = Flask(package_name, instance_relative_config=True)
# other app setup stuff like db, mail, ...
from .frontend.admin import AdminIndex, UserAdmin, RoleAdmin
admin = Admin(app, name='PyCBM Admin',
index_view=AdminIndex(url='/admin', name='Admin Home'))
admin.register(User, UserAdmin)
admin.register(Role, RoleAdmin)
So, like the title says, here's the problem:
/admin throws a 403 when an 'Admin' user isn't logged in, like it should, but
/admin/user lets anybody right on in.
I dug through the source code to try to find another "global all-of-admin-blueprint" security function - maybe I'm blind - but I couldn't find one.

If you go to flask_superadmin/base.py, at line 193 there is the following code snippet:
def _handle_view(self, name, *args, **kwargs):
if not self.is_accessible():
return abort(403)
So maybe this method has to be overriden by AdminIndex to avoid returning abort(403) but to redirect to /login

Related

How to add your own decorator to the flask-admin panel?

I am using Flask-Login for the login handling. In addition I wrote myself a decorator to keep views visible just for role=admin.
def admin_required(f):
#wraps(f)
def wrap(*args, **kwargs):
if current_user.role.name == "admin":
return f(*args, **kwargs)
else:
flash("You need to be an admin to view this page.")
return redirect(url_for('home'))
return wrap
I can use that decorator for my routes. But I don't know how to use this decorator for my flask-admin panel.
The Flask-SQLAlchemy Models were added to the flask-admin view like:
from flask_admin.contrib.sqla import ModelView
from flask_admin import Admin
admin = Admin(app, name='admin', template_mode='bootstrap3')
admin.add_view(ModelView(User, db.session))
admin.add_view(ModelView(Role, db.session))
But I want if someone who is not an admin hits: 127.0.0.1:8000/admin do not get access and gets redirected to /home.
But I have no route for my /admin and when I implement one I do not know what html I should return?
Thank you
Use the built-in method is_accessible, by deafult, it returns True, which means that admin panel will be accessible by all, you can change it by overriding it like this:
class MyAdminViews(ModelView):
def is_accessible(self):
user = User.query.filter_by(role=current_user.role).first()
res = user.role == "admin"
return res
admin.add_view((MyAdminViews(User, db.session)))
admin.add_view((MyAdminViews(Post, db.session)))
Now, these admin views will be accessible if and only if the user possesses an admin role. Also, you might want to add a clause to make it accessible only when someone is logged in, else the browser will throw an internal server error. You can do it like this:
class MyAdminViews(ModelView):
def is_accessible(self):
if current_user.is_authenticated:
user = User.query.filter_by(role=current_user.role).first()
res = user.role == "admin"
return res

How to hide field from admin change page but keep it in admin add page in Django

I have a data model in which some fields can only be set initially per each instance of the class, and once set, they should never change. The only way that can be allowed to change such an object should be by deleting it and creating a new one.
Pseudo code:
from django.db import models
from django.core.exceptions import ValidationError
class NetworkConnection(models.Model):
description = models.CharField(max_length=1000)
config = models.CharField(max_length=1000)
connection_info = models.CharField(max_length=5000)
def clean(self):
from .methods import establish_connection
self.connection_info = establish_connection(self.config)
if not self.connection_info:
raise ValidationError('Unable to connect')
def delete(self):
from .methods import close_connection
close_connection(self.config)
super(NetworkConnection, self).delete()
As in the above code, the user should initially input both the config and the description fields. Then Django verifies the config and establishes some sort of network connection based on such configurations and saves its information to another field called connection_info.
Now since each object of this class represents something that cannot be edited once created, I need to hind the config field from the admin page that edits the object, leaving only the description field; However, the config field still needs to be there when adding a new connection. How do I do this?
The following is an example of my last admin.py attempt:
from django.contrib import admin
from .models import NetworkConnection
class NetworkConnectionAdmin(admin.ModelAdmin):
exclude = ('connection_info')
def change_view(self, request, object_id, extra_context=None):
self.exclude = ('config')
return super(NetworkConnection, self).change_view(request, object_id, extra_context)
admin.site.register(NetworkConnection, NetworkConnectionAdmin)
But unfortunately, it seems to hide the config field from the add page too. Not only the change page
You can achieve that with a custom method directly in your NetworkConnectionAdmin class:
from django.contrib import admin
from .models import NetworkConnection
class NetworkConnectionAdmin(admin.ModelAdmin):
exclude = ('connection_info')
def get_readonly_fields(self, request, obj=None):
if obj:
return ["config", "description"]
else:
return []
admin.site.register(NetworkConnection, NetworkConnectionAdmin)
I got it from the Django Admin Cookbook.
Turns out this could be done using the ModelAdmin.get_from function but if anybody has a better answer, please share it
Solution using get_from would be:
admin.py
from django.contrib import admin
from .models import NetworkConnection
class NetworkConnectionAdmin(admin.ModelAdmin):
exclude = ('connection_info')
def get_form(self, request, obj=None, **kwargs):
if obj:
kwargs['exclude'] = ('config')
return super(NetworkConnectionAdmin, self).get_form(request, obj, **kwargs)
admin.site.register(NetworkConnection, NetworkConnectionAdmin)

Hide Flask-Admin route

I'm building a Flask blog and setting up an admin interface now. I've read about setting up security for Flask-Admin. I've managed to set up security (access is restricted only to logged-in users) for all my models, but users can still access the '/admin' route which has just a bare home button in it.
My question is: is there any way I could hide or protect the '/admin' route, so an unauthenticated user is just redirected to the login page/ denied access?
Thanks a lot!
Attaching my current admin setup:
from flask_admin import Admin
from flask_login import current_user
from flask_admin.contrib import sqla
from wtforms.widgets import TextArea
from wtforms import TextAreaField
from samo.models import User, Post, Tag
from samo import app,db
admin = Admin(app, name='Admin', template_mode='bootstrap3')
class CKTextAreaWidget(TextArea):
def __call__(self, field, **kwargs):
if kwargs.get('class'):
kwargs['class'] += ' ckeditor'
else:
kwargs.setdefault('class', 'ckeditor')
return super(CKTextAreaWidget, self).__call__(field, **kwargs)
class CKTextAreaField(TextAreaField):
widget = CKTextAreaWidget()
class PostAdmin(sqla.ModelView):
form_overrides = dict(content=CKTextAreaField)
create_template = 'blog/ckeditor.html'
edit_template = 'blog/ckeditor.html'
form_excluded_columns = ('slug')
def is_accessible(self):
return current_user.is_authenticated
admin.add_view(PostAdmin(Post, db.session))
class TagAdmin(sqla.ModelView):
def is_accessible(self):
return current_user.is_authenticated
admin.add_view(TagAdmin(Tag, db.session))
class UserAdmin(sqla.ModelView):
def is_accessible(self):
return current_user.is_authenticated
admin.add_view(UserAdmin(User, db.session))
I use such a configuration like you described it for all my Websites. Use an AdminIndexView. Here is an example of how handle login, logout and redirection in case the user is not authorized.
class FlaskyAdminIndexView(AdminIndexView):
#expose('/')
def index(self):
if not login.current_user.is_authenticated:
return redirect(url_for('.login'))
return super(FlaskyAdminIndexView, self).index()
#expose('/login', methods=['GET', 'POST'])
def login(self):
form = LoginForm(request.form)
if helpers.validate_form_on_submit(form):
user = form.get_user()
if user is not None and user.verify_password(form.password.data):
login.login_user(user)
else:
flash('Invalid username or password.')
if login.current_user.is_authenticated:
return redirect(url_for('.index'))
self._template_args['form'] = form
return super(FlaskyAdminIndexView, self).index()
#expose('/logout')
#login_required
def logout(self):
login.logout_user()
return redirect(url_for('.login'))
In your __init__.py where you create your admin object do this:
admin = Admin(index_view=FlaskyAdminIndexView())

Django allauth custom login form not rendering all fields in custom user model

I am trying to implement login of that consists a custom user model. Is it possible to inherit from allauth.account.forms.LoginForm and add a custom field to the custom login form?
Idea is to assign user with a role at the time of login by overriding
login() method.
I have followed allauth configuration and mentioned what forms to use for login in settings.py with following code
AUTH_USER_MODEL = 'core.User'
ACCOUNT_SIGNUP_FORM_CLASS = 'core.forms.SignupForm'
ACCOUNT_FORMS = {'login': 'core.forms.CoreLoginForm'}
I am using no auth backends other than django.contrib.auth.backends.ModelBackend and allauth.account.auth_backends.AuthenticationBackend. custom signup is working fine for me without any issues. But custom loginform is not rendering all fields in the user model. Allauth LoginForm was inherited as per accepted answer in this SO Post and a choicefield was added to the custom login form.
from allauth.account.forms import LoginForm
class CoreLoginForm(LoginForm):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(CoreLoginForm, self).__init__(*args, **kwargs)
role = forms.ChoiceField(widget=forms.Select(), choices=User.roles, initial=User.roles[0])
Upon a ./manage.py runserver it says Module "core.forms" does not define a "SignupForm" class. I have already defined a SignupForm in core.forms as below and signup will work if CoreLoginForm is inherited from forms.Form instead of LoginForm. So if I do
class CoreLoginForm(forms.Form):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(CoreLoginForm, self).__init__(*args, **kwargs)
role = forms.ChoiceField(widget=forms.Select(), choices=User.roles, initial=User.roles[0])
I can render custom login form to html page. But problem here is that I have to redefine every methods in the class including authenticate(), perform_login() etc. This will end up in copying whole LoginForm and pasting it in forms.py of the app. I dont want to do this as I think this is against DRY principle. Is there a simple way to add a custom field to custom loginform and override login() method?
TIA
Probably you have solved your problem already but i ll leave this solution for other guys who ve spent more then 10 minutes on this problem like me :)
I have found an easy way to add a field to Allauth login form:
As you have done - add to settings.py :
ACCOUNT_FORMS = {'login': 'core.forms.CoreLoginForm'}
and after that in your forms.py you need to add :
from django import forms
from allauth.account.forms import LoginForm
class CoreLoginForm(LoginForm):
def __init__(self, *args, **kwargs):
super(CoreLoginForm, self).__init__(*args, **kwargs)
## here i add the new fields that i need
self.fields["new-field"] = forms.CharField(label='Some label', max_length=100)

Is it possible to implement a "change password at next logon" type feature in the django admin?

I want to be able to set an option in the user's settings that forces them to change their password upon the next login to the admin interface. Is this possible? How would it go about being implemented? I'm using the default auth model right now but not opposed to modifying or changing it. Thanks for any help.
I'm actually in the process of doing this myself. You need three components: a user profile (if not already in use on your site), a middleware component, and a pre_save signal.
My code for this is in an app named 'accounts'.
# myproject/accounts/models.py
from django.db import models
from django.db.models import signals
from django.contrib.auth.models import User
class UserProfile(models.Model):
user = models.ForeignKey(User, unique=True)
force_password_change = models.BooleanField(default=False)
def create_user_profile_signal(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
def password_change_signal(sender, instance, **kwargs):
try:
user = User.objects.get(username=instance.username)
if not user.password == instance.password:
profile = user.get_profile()
profile.force_password_change = False
profile.save()
except User.DoesNotExist:
pass
signals.pre_save.connect(password_change_signal, sender=User, dispatch_uid='accounts.models')
signals.post_save.connect(create_user_profile_signal, sender=User, dispatch_uid='accounts.models')
First, we create a UserProfile with a foreign key to User. The force_password_change boolean will, as its name describes, be set to true for a user whenever you want to force them to change their password. You could do anything here though. In my organization, we also chose to implement a mandatory change every 90 days, so I also have a DateTimeField that stores the last time a user changed their password. You then set that in the pre_save signal, password_changed_signal.
Second, we have the create_user_profile_signal. This is mostly added just for completeness. If you're just now adding user profiles into your project, you'll need a post_save signal that will create a UserProfile every time a User is created. This accomplishes that task.
Third, we have the password_changed_signal. This is a pre_save signal because at this point in the process the actual row in the User table hasn't be updated. Therefore, we can access both the previous password and the new password about to be saved. If the two don't match, that means the user has changed their password, and we can then reset the force_password_change boolean. This would be the point, also where you would take care of any other things you've added such as setting the DateTimeField previously mentioned.
The last two lines attach the two functions to their appropriate signals.
If you haven't already, you will also need to add the following line to your project's settings.py (changing the app label and model name to match your setup):
AUTH_PROFILE_MODULE = 'accounts.UserProfile'
That covers the basics. Now we need a middleware component to check the status of our force_password_change flag (and any other necessary checks).
# myproject/accounts/middleware.py
from django.http import HttpResponseRedirect
import re
class PasswordChangeMiddleware:
def process_request(self, request):
if request.user.is_authenticated() and \
re.match(r'^/admin/?', request.path) and \
not re.match(r'^/admin/password_change/?', request.path):
profile = request.user.get_profile()
if profile.force_password_change:
return HttpResponseRedirect('/admin/password_change/')
This very simple middleware hooks into the process_request stage of the page loading process. It checks that 1) the user has already logged in, 2) they are trying to access some page in the admin, and 3) the page they are accessing is not the password change page itself (otherwise, you'd get an infinite loop of redirects). If all of these are true and the force_password_change flag has been set to True, then the user is redirected to the password change page. They will not be able to navigate anywhere else until they change their password (firing the pre_save signal discussed previously).
Finally, you just need to add this middleware to your project's settings.py (again, changing the import path as necessary):
MIDDLEWARE_CLASSES = (
# Other middleware here
'myproject.accounts.middleware.PasswordChangeMiddleware',
)
I have used Chris Pratt's solution, with a little change: instead of using a middleware, that'd be executed for every page with the consequent resource use, I figured I'd just intercept the login view.
In my urls.py I have added this to my urlpatterns:
url(r'^accounts/login/$', 'userbase.views.force_pwd_login'),
then I added the following to userbase.views:
def force_pwd_login(request, *args, **kwargs):
response = auth_views.login(request, *args, **kwargs)
if response.status_code == 302:
#We have a user
try:
if request.user.get_profile().force_password_change:
return redirect('django.contrib.auth.views.password_change')
except AttributeError: #No profile?
pass
return response
It seems to work flawlessly on Django 1.2, but I have no reason to believe 1.3+ should have problems with it.
This is the middleware I use with Django 1.11 :
# myproject/accounts/middleware.py
from django.http import HttpResponseRedirect
from django.urls import reverse
class PasswordChangeMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
next = reverse('client:password-update')
if request.user.is_authenticated() and request.path != next:
if request.user.account.force_password_change:
return HttpResponseRedirect(next)
return response
Still adding it to the settings middleware list :
MIDDLEWARE_CLASSES = (
# Other middleware here
'myproject.accounts.middleware.PasswordChangeMiddleware',
)
I spent 2 days on this issue recently, and a new solution came out.
Hopefully it's useful.
Just as above said, a new user model created.
newuser/models.py
class Users(AbstractUser):
default_pwd_updated = models.NullBooleanField(default=None, editable=False)
pwd_update_time = models.DateTimeField(editable=False, null=True, default=None) # reserved column to support further interval password (such as 60 days) update policy
def set_password(self, raw_password):
if self.default_pwd_updated is None:
self.default_pwd_updated = False
elif not self.default_pwd_updated:
self.default_pwd_updated = True
self.pwd_update_time = timezone.now()
else:
self.pwd_update_time = timezone.now()
super().set_password(raw_password)
Set this model as the AUTH_USER_MODEL.
[project]/settings.py
AUTH_USER_MODEL = 'newuser.Users'
Now you just need to customize LoginView and some methods in AdminSite.
[project]/admin.py
from django.contrib.admin import AdminSite
from django.contrib.auth.views import LoginView
from django.utils.translation import gettext as _, gettext_lazy
from django.urls import reverse
from django.views.decorators.cache import never_cache
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.http import HttpResponseRedirect
class NewLoginView(LoginView):
def get_redirect_url(self):
if self.request.method == "POST" and self.request.user.get_username()\
and not self.request.user.default_pwd_updated:
redirect_to = reverse("admin:password_change")
else:
redirect_to = self.request.POST.get(
self.redirect_field_name,
self.request.GET.get(self.redirect_field_name, '')
)
return redirect_to
class NewAdminSite(AdminSite):
site_header = site_title = gettext_lazy("Customized Admin Site")
def __init__(self, name="admin"):
super().__init__(name)
#never_cache
def login(self, request, extra_context=None):
"""
Display the login form for the given HttpRequest.
"""
if request.method == 'GET' and self.has_permission(request):
# Already logged-in, redirect to admin index
if request.user.get_username() and not request.user.default_pwd_updated:
# default password not changed, force to password_change view
path = reverse('admin:password_change', current_app=self.name)
else:
path = reverse('admin:index', current_app=self.name)
return HttpResponseRedirect(path)
from django.contrib.auth.views import LoginView
from django.contrib.admin.forms import AdminAuthenticationForm
context = {
**self.each_context(request),
'title': _('Log in'),
'app_path': request.get_full_path(),
'username': request.user.get_username(),
}
if (REDIRECT_FIELD_NAME not in request.GET and
REDIRECT_FIELD_NAME not in request.POST):
context[REDIRECT_FIELD_NAME] = reverse('admin:index', current_app=self.name)
context.update(extra_context or {})
defaults = {
'extra_context': context,
'authentication_form': self.login_form or AdminAuthenticationForm,
'template_name': self.login_template or 'admin/login.html',
}
request.current_app = self.name
return NewLoginView.as_view(**defaults)(request) # use NewLoginView
#never_cache
def index(self, request, extra_context=None):
if request.user.get_username() and not request.user.default_pwd_updated:
# if default password not updated, force to password_change page
context = self.each_context(request)
context.update(extra_context or {})
return self.password_change(request, context)
return super().index(request, extra_context)
admin_site = NewAdminSite(name="admin")
NOTE: if you intend to use custom template for changing default password, you could override each_context method and then determine which template should be used up to the flag force_pwd_change.
[project]/admin.py
def using_default_password(self, request):
if self.has_permission(request) and request.user.get_username() and not request.user.default_pwd_updated:
return True
return False
def each_context(self, request):
context = super().each_context(request)
context["force_pwd_change"] = self.using_default_password(request)
return context
From a thread on the Django Users mailing list:
This isn't ideal, but it should work
(or prompt someone to propose
something better).
Add a one-to-one table for the user,
with a field containing the initial
password (encrypted, of course, so it
looks like the password in the
auth_user table).
When the user logs in, have the login
page check to see if the passwords
match. If they do, redirect to the
password change page instead of the
normal redirect page.
Checkout this simple package based on session (Tested with django 1.8). https://github.com/abdullatheef/django_force_reset_password
Create custom view in myapp.views.py
class PassWordReset(admin.AdminSite):
def login(self, request, extra_context=None):
if request.method == 'POST':
response = super(PassWordReset, self).login(request, extra_context=extra_context)
if response.status_code == 302 and request.user.is_authenticated():
if not "fpr" in request.session or request.session['fpr']:
request.session['fpr'] = True
return HttpResponseRedirect("/admin/password_change/")
return response
return super(PassWordReset, self).login(request, extra_context=extra_context)
def password_change(self, request, extra_context=None):
if request.method == 'POST':
response = super(PassWordReset, self).password_change(request, extra_context=extra_context)
if response.status_code == 302 and request.user.is_authenticated():
request.session['fpr'] = False
return response
return super(PassWordReset, self).password_change(request, extra_context=extra_context)
pfr_login = PassWordReset().login
pfr_password_change = PassWordReset().admin_view(PassWordReset().password_change, cacheable=True)
Then in project/urls.py
from myapp.views import pfr_password_change, pfr_login
urlpatterns = [
......
url(r'^admin/login/$', pfr_login),
url(r'^admin/password_change/$', pfr_password_change),
url(r'^admin/', admin.site.urls),
....
]
Then add this middleware myapp/middleware.py
class FPRCheck(object):
def process_request(self, request):
if request.user.is_authenticated() \
and re.match(r'^/admin/?', request.path) \
and (not "fpr" in request.session or ("fpr" in request.session and request.session['fpr'])) \
and not re.match(r"/admin/password_change|/admin/logout", request.path):
return HttpResponseRedirect("/admin/password_change/")
Order of middleware
MIDDLEWARE_CLASSES = [
....
'myapp.middleware.FPRCheck'
]
Note
This will not need any extra model.
Also work with any Session Engine.
No db query inside middleware.

Categories