Securing Flask-Admin When Using Blueprints and Application Factory - flask

I've set up Flask Admin and it is working, but am struggling with adding authentication. There are several tutorials I've followed but can't seem to get them to work with how I've set up my app. Per the documentation on Flask-Admin regarding authentication (and slightly tweaked based on how I'm importing various elements), you just need to add:
class AdminView(ModelView):
def is_accessible(self):
return current_user.is_authenticated
def inaccessible_callback(self, name, **kwargs):
# redirect to login page if user doesn't have access
return redirect(url_for('login', next=request.url))
But I can still access the /admin route without logging in. (I also would like to add an additional conditional that checks that the user is listed as an admin, which is a boolean column in the user table, but I haven't gotten this first part to work).
I've tried putting the above inside and outside of the create_app() function. Does this have to do with my blueprints? If so, where would I put this code?
# __init__.py
from flask import Flask
from dotenv import load_dotenv
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from flask_login import LoginManager, current_user
from flask_migrate import Migrate
from SIMS_Portal.config import Config
from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView
from flaskext.markdown import Markdown
load_dotenv()
db = SQLAlchemy()
bcrypt = Bcrypt()
login_manager = LoginManager()
login_manager.login_view = 'users.login'
login_manager.login_message_category = 'danger'
mail = Mail()
from SIMS_Portal import models
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
bcrypt.init_app(app)
login_manager.init_app(app)
mail.init_app(app)
admin = Admin(app, name='SIMS Admin Portal', template_mode='bootstrap4', endpoint='admin')
Markdown(app)
from SIMS_Portal.main.routes import main
from SIMS_Portal.assignments.routes import assignments
from SIMS_Portal.emergencies.routes import emergencies
from SIMS_Portal.portfolios.routes import portfolios
from SIMS_Portal.users.routes import users
from SIMS_Portal.errors.handlers import errors
app.register_blueprint(main)
app.register_blueprint(assignments)
app.register_blueprint(emergencies)
app.register_blueprint(portfolios)
app.register_blueprint(users)
app.register_blueprint(errors)
from SIMS_Portal.models import User, Assignment, Emergency, Portfolio, NationalSociety
admin.add_view(ModelView(User, db.session))
admin.add_view(ModelView(Assignment, db.session))
admin.add_view(ModelView(Emergency, db.session))
admin.add_view(ModelView(Portfolio, db.session))
admin.add_view(ModelView(NationalSociety, db.session))
return app

Got some help from the r/flask community which I'll share here for anyone else that has set their app up the same way and found existing tutorials unhelpful. The key when using an app factory like in my case is the swap out the ModelView for the AdminView you define, which can go before the create_app() function. In my __init__.py, I first defined that custom class that inherits from ModelView (and add a check that the user is not only logged in but also listed as an admin in my database):
class AdminView(ModelView):
def is_accessible(self):
if current_user.is_admin == 1:
return current_user.is_authenticated
def inaccessible_callback(self, name, **kwargs):
return render_template('errors/403.html'), 403
Then within the create_app() I swap out what I had previously included as admin.add_view(ModelView(User, db.session)) for admin.add_view(AdminView(User, db.session))
That little difference makes obvious sense, but again, I couldn't find tutorials that covered this.

Related

flask-security: how to use in blueprint/extension app pattern?

I want to use flask-security.
I'm using a template flask app which creates global objects for extensions, and then initialises them when the app is created.
e.g. in extensions.py there is code like this:
from flask_bcrypt import Bcrypt
from flask_caching import Cache ...
from flask_security import Security ...
bcrypt = Bcrypt() ...
security = Security()
and then in app.py a call to register_extensions(app) which uses init_app(app) methods like so:
bcrypt.init_app(app)
security.init_app(app)
and indeed flask-security has an init_app() method. But the documentation says that the Security object needs a DataStore object which needs the User and Role model. It doesn't feel right to import the User and Role model in app.py when so far no other extension needs that.
What is the best practice for using Flask-Security when using the 'large Flask app' model ... I don't find the documentation helpful. It is a simple case when all objects are defined in one place.
Below is what I have.
extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_security import Security
db = SQLAlchemy()
security = Security()
__init__.py
from .extensions import db, security
from .models import User, Role
def create_app(config_name):
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security.init_app(app, user_datastore)

Apply common custom authentication functionality to multiple Flask projects

I've created a class which authenticates users based on our company's user server. I'd like to apply it to any of our Flask apps which use Flask-Login rather than repeating the code in each project. I'm not sure what the right pattern for this is, or how to implement it.
I thought of a few options:
Python module - simply authentication, the module would do the login then return something (maybe credentials or token).
Flask 'app' - authenticates, includes a login and logout screen, and somehow gets linked in with #login_manager.user_loader. The issue I see is that the user loaded could have any application's User schema.
What is a good pattern for implementing this common authentication for multiple projects?
Extract the common functions of setting up a Flask-Login manger and the custom login views/functions you need to a simple Flask extension package. Install this package with pip in the environment of each project and use it when creating that project's Flask app.
company_auth/company_auth.py
from flask import Blueprint, redirect, url_for, render_template
from flask_login import LoginManager
from flask_wtf import Form
bp = Blueprint('auth', __name__)
class LoginForm(Form):
# define your login form
#bp.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
# do custom login stuff
return redirect(url_for('index'))
return render_template('auth/login.html', form=form)
def init_app(app, user_model):
# have to pass in the user model since it's different between apps
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
#login_manager.user_loader
def company_user_loader(id):
user = user_model.query.get(id)
# do custom user loading stuff
return user
app.register_blueprint(bp, url_prefix='/auth')
company_auth/setup.py
#!/usr/bin/env python
from setuptools import setup, find_packages
setup(
name='company_auth',
version='1.0',
py_modules=['company_auth'],
url='http://davidism.com/',
license='BSD',
author='davidism',
author_email='davidism#gmail.com',
description='Flask extension for company auth',
requires=['flask']
)
Create a distribution of the package to install in other projects.
$ python setup.py sdist
For each project, install the package, import and run the init_app function, and provide the auth templates. (Your extension could include default templates too, but this answer would get gigantic if I go down that path. See Flask-Security for an example of default templates.)
$ project_env/bin/activate
$ pip install /path/to/company_auth/dist/company_auth-1.0.tar.gz
Create the auth templates:
project/
templates/
auth/
login.html
app.py
Set up the app with the custom auth:
import company_auth
company_auth.init_app()

Flask questions about BaseConverter and __init.py__

I am trying to use a custom Baseconverter to ensure that my urls are "clean". I finally got it to work as follows:
My init.py is:
import os
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.login import LoginManager
from flask.ext.openid import OpenID
from config import basedir
from slugify import slugify
app = Flask(__name__)
from werkzeug.routing import BaseConverter
class MyStringConverter(BaseConverter):
def to_python(self, value):
return value
def to_url(self, values):
return slugify(values)
app.url_map.converters['mystring'] = MyStringConverter
app.config.from_object('config')
db = SQLAlchemy(app)
lm = LoginManager()
lm.init_app(app)
lm.login_view = 'login'
oid = OpenID(app, os.path.join(basedir, 'tmp'))
from app import views, models
But if I define the MyStringConverter class and add app.url_map.converters['mystring'] = MyStringConverter at the end of the file instead, I got a LookupError: the converter 'mystring' does not exist error. Why is this?
Is this the correct way to clean up my urls? I'm not sure if I need to do anything to the to_python() method or if I am going about this the right way.

How do I add flask-admin to a Blueprint?

for example:
from flask import Flask
from flask.ext.admin import Admin, BaseView, expose
class MyView(BaseView):
#expose('/')
def index(self):
return self.render('index.html')
app = Flask(__name__)
admin = Admin(app)
admin.add_view(MyView(name='Hello'))
app.run()
but, if I need a new file, called 'views.py', how can I add a view into views.py to admin?
Do I need to use a blueprint?
For my project I actually made a child class of Blueprint that supports flask admin:
from flask import Blueprint
from flask_admin.contrib.sqla import ModelView
from flask_admin import Admin
class AdminBlueprint(Blueprint):
views=None
def __init__(self,*args, **kargs):
self.views = []
return super(AdminBlueprint, self).__init__('admin2', __name__,url_prefix='/admin2',static_folder='static', static_url_path='/static/admin')
def add_view(self, view):
self.views.append(view)
def register(self,app, options, first_registration=False):
admin = Admin(app, name='microblog', template_mode='adminlte')
for v in self.views:
admin.add_view(v)
return super(AdminBlueprint, self).register(app, options, first_registration)
For details you may like to read my blog here: http://blog.sadafnoor.me/blog/how-to-add-flask-admin-to-a-blueprint/
I am very late for this question, but anyway... My guess is that you want to use the Application Factory pattern and use the Flask-Admin. There is a nice discussion about the underlying problems. I used a very ugly solution, instantiating the Flask-Admin in the init.py file:
from flask_admin.contrib.sqla import ModelView
class UserModelView(ModelView):
create_modal = True
edit_modal = True
can_export = True
def create_app(config_name):
app = Flask(__name__)
app.config.from_object(config[config_name])
db.init_app(app)
# import models here because otherwise it will throw errors
from models import User, Sector, Article
admin.init_app(app)
admin.add_view(UserModelView(User, db.session))
# attach blueprints
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint, url_prefix='/auth')
return app
You don't need a blueprint for that. In views.py add an import for the admin object you defined in your main project:
from projectmodule import admin
from flask.ext.admin import BaseView, expose
class MyView(BaseView):
#expose('/')
def index(self):
return self.render('index.html')
admin.add_view(MyView(name='Hello'))
and in your main projectmodule file use:
from flask import Flask
from flask.ext.admin import Admin
app = Flask(__name__)
admin = Admin(app)
# import the views
import views
app.run()
e.g. you add import views after the line that sets admin = Admin(app).
I have an flask app with one blueprint (and login/logout to admin).
This is the best solution I found to implement flask admin with some custom features.
My structure as follows:
my_app
main
__init__.py
routes.py
static
templates
__init__.py
config.py
models.py
run.py
Customized admin index view from models.py
from flask_admin import AdminIndexView
class MyAdminIndexView(AdminIndexView):
def is_accessible(self):
return current_user.is_authenticated
def inaccessible_callback(self, name, **kwargs):
return redirect(url_for('main.home'))
Main init.py as follows:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_admin import Admin
from flask_admin.menu import MenuLink
from my_app.config import Config
# create extensions
db = SQLAlchemy()
admin = Admin()
def create_app(config_class=Config): # default configutation
app = Flask(__name__)
app.config.from_object(Config)
#initialize extensions
db.init_app(app)
...
# customized admin views
from my_app.models import MyAdminIndexView
admin.init_app(app,index_view=MyAdminIndexView())
admin.add_link(MenuLink(name='Home', url='/'))
admin.add_link(MenuLink(name='Logout', url='/logout'))
#blueprint
from my_app.main.routes import main
app.register_blueprint(main)
return app
I think this is the most elegant solution I came up so far.
In order to keep clean the __init__.py root file:
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from app.config import Config
from flask_admin import Admin
db = SQLAlchemy()
admin = Admin(template_mode="bootstrap3")
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
db.init_app(app)
admin.init_app(app)
from app.admin import bp
app.register_blueprint(bp, url_prefix='/admin')
return app
Then in the __init__.py of the Blueprint admin app
# app/admin/__init__.py
from flask import Blueprint
from app import admin, db
from app.models import User
from flask_admin.contrib.sqla import ModelView
bp = Blueprint('admin_app', __name__)
admin.name = 'Admin panel'
admin.add_view(ModelView(User, db.session))
# all the rest stuff

Django: Using 2 different AdminSite instances with different models registered

Apart from the usual admin, I want to create a limited admin for non-staff users. This admin site will have different registered ModelAdmins.
I created a folder /useradmin/ in my project directory and similar to contrib/admin/_init_.py I added an autodiscover() which will register models defined in useradmin.py modules instead of admin.py:
# useradmin/__init__.py
def autodiscover():
# Same as admin.autodiscover() but registers useradmin.py modules
...
for app in settings.INSTALLED_APPS:
mod = import_module(app)
try:
before_import_registry = copy.copy(site._registry)
import_module('%s.useradmin' % app)
except:
site._registry = before_import_registry
if module_has_submodule(mod, 'useradmin'):
raise
I also cretated sites.py under useradmin/ to override AdminSite similar to contrib/admin/sites:
# useradmin/sites.py
class UserAdminSite(AdminSite):
def has_permission(self, request):
# Don't care if the user is staff
return request.user.is_active
def login(self, request):
# Do the login stuff but don't care if the user is staff
if request.user.is_authenticated():
...
else:
...
site = UserAdminSite(name='useradmin')
In the project's URLs:
# urls.py
from django.contrib import admin
import useradmin
admin.autodiscover()
useradmin.autodiscover()
urlpatterns = patterns('',
(r'^admin/', include(admin.site.urls)),
(r'^useradmin/', include(useradmin.site.urls)),
)
And I try to register different models in admin.py and useradmin.py modules under app directories:
# products/useradmin.py
import useradmin
class ProductAdmin(useradmin.ModelAdmin):
pass
useradmin.site.register(Product, ProductAdmin)
But when registering models in useradmin.py like useradmin.site.register(Product, ProductAdmin), I get 'module' object has no attribute 'ModelAdmin' exception. Though when I try this via shell;
import useradmin
from useradmin import ModelAdmin
does not raise any exception.
Any ideas what might be wrong?
Edit:
I tried going the #Luke way and arranged the code as follows as minimal as possible:
(file paths are relative to the project root)
# admin.py
from django.contrib.admin import autodiscover
from django.contrib.admin.sites import AdminSite
user_site = AdminSite(name='useradmin')
# urls.py (does not even have url patterns; just calls autodiscover())
import admin
admin.autodiscover()
# products/admin.py
import admin
from products.models import Product
admin.user_site.register(Product)
As a result I get an AttributeError: 'module' object has no attribute 'user_site' when admin.user_site.register(Product) in products/admin.py is called.
Any ideas?
Solution:
I don't know if there are better ways but, renaming the admin.py in the project root to useradmin.py and updating the imports accordingly resolved the last case, which was a naming and import conflict.
Does useradmin have a ModelAdmin class defined, or do you import it from contrib.admin? I don't see anywhere in the code you supplied where that class gets set up.
That being said, there's a much easier way to do this: just initialize two AdminSites, and then wire them up to the URLs that you want (You can even put them in the same urlconf).