As described in the project page (https://pythonhosted.org/Flask-Principal/), Flask-Principal is a permission extension. It manages who can access what and to what extent. You usually should use it with an authentication and session manager, as is the case of Flask-Login, another extension we'll learn in this section.
Flask-Principal handles permissions through four simple entities: Identity, IdentityContext, Need, and Permission.
A Need is a criterion you need (aha moment!) to satisfy in order to do something, such as having a role or a permission. There are a few preset needs available with Principal, but you may create your own easily, as a Need is just a named tuple such as this one:
from collections import namedtuplenamedtuple('RoleNeed', ['role', 'admin'])
Given that we have our authorization extension all set, we need to authorize against something. A usual scenario is to restrict access to an administrative interface to administrators (don't say anything). To do that, we need to identify who is an administrator and who isn't. Flask-Login can be of help here by providing us with user session management (login and logout). Let's try an example. First, we make sure the required dependencies are installed:
pip install flask-wtf flask-login flask-principal flask-sqlalchemy
And then:
# coding:utf-8 # this example is based in the examples available in flask-login and flask-principal docs from flask_wtf import Form from wtforms import StringField, PasswordField, ValidationError from wtforms import validators from flask import Flask, flash, render_template, redirect, url_for, request, session, current_app from flask.ext.login import UserMixin from flask.ext.sqlalchemy import SQLAlchemy from flask.ext.login import LoginManager, login_user, logout_user, login_required, current_user from flask.ext.principal import Principal, Permission, Identity, AnonymousIdentity, identity_changed from flask.ext.principal import RoleNeed, UserNeed, identity_loaded principal = Principal() login_manager = LoginManager() login_manager.login_view = 'login_view' # you may also overwrite the default flashed login message # login_manager.login_message = 'Please log in to access this page.' db = SQLAlchemy() # Create a permission with a single Need # we use it to see if an user has the correct rights to do something admin_permission = Permission(RoleNeed('admin'))
As our example now is just too big, we'll understand it piecemeal. First, we make the necessary imports and create our extension instances. We set the login_view
for the login_manager
so that it knows where to redirect our user if he tries to access a page that requires user authentication. Be aware that Flask-Principal does not handle or keep track of logged users. That is Flask-Login abracadabra!
We also create our admin_permission
. Our admin permission has only one need: the role admin. This way, we are defining that, for our permission to accept a user, this user needs to have the Role admin
.
# UserMixin implements some of the methods required by Flask-Login class User(db.Model, UserMixin): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) active = db.Column(db.Boolean, default=False) username = db.Column(db.String(60), unique=True, nullable=False) password = db.Column(db.String(20), nullable=False) roles = db.relationship( 'Role', backref='roles', lazy='dynamic') def __unicode__(self): return self.username # flask login expects an is_active method in your user model # you usually inactivate a user account if you don't want it # to have access to the system anymore def is_active(self): """ Tells flask-login if the user account is active """ return self.active class Role(db.Model): """ Holds our user roles """ __tablename__ = 'roles' name = db.Column(db.String(60), primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id')) def __unicode__(self): return self.name
We have two models here, one to hold our user information and another to hold our user roles. A role is usually used to categorize users, like admin
; you may have three admins in your system and all of them will have the role admin. As a result, they will all be able to do "admin stuff", if the permissions are properly configured. Notice we define an is_active
method for User. That method is required and I advise you to always overwrite it, even though UserMixin
already provides an implementation. is_active
is used to tell login
whether the user is active or not; if not active, he may not log in.
class LoginForm(Form): def get_user(self): return User.query.filter_by(username=self.username.data).first() user = property(get_user) username = StringField(validators=[validators.InputRequired()]) password = PasswordField(validators=[validators.InputRequired()]) def validate_username(self, field): "Validates that the username belongs to an actual user" if self.user is None: # do not send a very specific error message here, otherwise you'll # be telling the user which users are available in your database raise ValidationError('Your username and password did not match') def validate_password(self, field): username = field.data user = User.query.get(username) if user is not None: if not user.password == field.data: raise ValidationError('Your username and password did not match')
Here we write the LoginForm
ourselves. You could say: "Why not use model_form
, dude?" Well, to use model_form
here, you would have to initialize your database with your app (that you do not have yet) and set up a context. Just too much trouble.
We also define two custom validators, one to check if the username
is valid and another to check if the password
and username
match.
class Config(object): "Base configuration class" DEBUG = False SECRET_KEY = 'secret' SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/ex03.db' class Dev(Config): "Our dev configuration" DEBUG = True SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/dev.db' def setup(app): # initializing our extensions ; ) db.init_app(app) principal.init_app(app) login_manager.init_app(app) # adding views without using decorators app.add_url_rule('/admin/', view_func=admin_view) app.add_url_rule('/admin/context/', view_func=admin_only_view) app.add_url_rule('/login/', view_func=login_view, methods=['GET', 'POST']) app.add_url_rule('/logout/', view_func=logout_view) # connecting on_identity_loaded signal to our app # you may also connect using the @identity_loaded.connect_via(app) decorator identity_loaded.connect(on_identity_loaded, app, False) # our application factory def app_factory(name=__name__, config=Dev): app = Flask(name) app.config.from_object(config) setup(app) return app
Here we define our configuration objects, our app
setup, and application factory. I would say the tricky part is the setup, as it registers views using an app
method and not a decorator (yes, the same result as using @app.route
) and we connect our identity_loaded
signal to our app, so that the user identity is loaded and available in each request. We could also register it as a decorator, like this:
@identity_loaded.connect_via(app) # we use the decorator to let the login_manager know of our load_user # userid is the model id attribute by default @login_manager.user_loader def load_user(userid): """ Loads an user using the user_id Used by flask-login to load the user with the user id stored in session """ return User.query.get(userid) def on_identity_loaded(sender, identity): # Set the identity user object identity.user = current_user # in case you have resources that belong to a specific user if hasattr(current_user, 'id'): identity.provides.add(UserNeed(current_user.id)) # Assuming the User model has a list of roles, update the # identity with the roles that the user provides if hasattr(current_user, 'roles'): for role in current_user.roles: identity.provides.add(RoleNeed(role.name))
The load_user
function is required by Flask-Login to load the user using the userid
stored in the session storage. It should return None
, if the userid
was not found. Do not throw an exception here.
on_identity_loaded
is registered with the identity_loaded
signal and is used to load identity needs stored in your models. This is required because Flask-Principal is a generic solution and has no idea of how you have your permissions stored.
def login_view(): form = LoginForm() if form.validate_on_submit(): # authenticate the user... login_user(form.user) # Tell Flask-Principal the identity changed identity_changed.send( # do not use current_app directly current_app._get_current_object(), identity=Identity(form.user.id)) flash("Logged in successfully.") return redirect(request.args.get("next") or url_for("admin_view")) return render_template("login.html", form=form) @login_required # you can't logout if you're not logged def logout_view(): # Remove the user information from the session # Flask-Login can handle this on its own = ] logout_user() # Remove session keys set by Flask-Principal for key in ('identity.name', 'identity.auth_type'): session.pop(key, None) # Tell Flask-Principal the user is anonymous identity_changed.send( current_app._get_current_object(), identity=AnonymousIdentity()) # it's good practice to redirect after logout return redirect(request.args.get('next') or '/')
login_view
and logout_view
are just what is expected of them: a view to authenticate and another to unauthenticate the user. In both cases, you just have to make sure to call the appropriate Flask-Login functions (login_user
and logout_user
) and send an adequate Flask-Principal signal (and clean the session in the logout).
# I like this approach better ... @login_required @admin_permission.require() def admin_view(): """ Only admins can access this """ return render_template('admin.html') # Meh ... @login_required def admin_only_view(): """ Only admins can access this """ with admin_permission.require(): # using context return render_template('admin.html')
Finally, we have our actual views: admin_view
and admin_only_view
. Both of them do the exact same thing, they check whether the user is logged with Flask-Login and then check if they have adequate permission to access the view. The difference here is that, in the first scenario, admin_view
uses permission as a decorator to verify the user's credentials and as a context in the second scenario.
def populate(): """ Populates our database with a single user, for testing ; ) Why not use fixtures? Just don't wanna ... """ user = User(username='student', password='passwd', active=True) db.session.add(user) db.session.commit() role = Role(name='admin', user_id=user.id) db.session.add(role) db.session.commit() if __name__ == '__main__': app = app_factory() # we need to use a context here, otherwise we'll get a runtime error with app.test_request_context(): db.drop_all() db.create_all() populate() app.run()
populate
is used to add a proper user and role to our database in case you want to test it.
A word of caution about our earlier example: we used plain text for the user database. In actual live code, you don't want to do that because it is common practice for users to use the same password for multiple sites. If the password is in plain text, anyone with access to the database will be able know it and test it against sensitive sites. The solution provided in http://flask.pocoo.org/snippets/54/ might help you avoid this scenario.
Now here is an example base.html
template you could use with the preceding code:
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{% block title %}{% endblock %}</title> <link rel="stylesheet" media="screen,projection" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.96.1/css/materialize.min.css" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/> <style type="text/css"> .messages{ position: fixed; list-style: none; margin:0px; padding: .5rem 2rem; bottom: 0; left: 0; width:100%; background-color: #abc; text-align: center; } </style> </head> <body> {% with messages = get_flashed_messages() %} {% if messages %} <ul class='messages'> {% for message in messages %} <li>{{ message }}</li> {% endfor %} </ul> {% endif %} {% endwith %} <header> <nav> <div class="container nav-wrapper"> {% if current_user.is_authenticated() %} <span>Welcome to the admin interface, {{ current_user.username }}</span> {% else %}<span>Welcome, stranger</span>{% endif %} <ul id="nav-mobile" class="right hide-on-med-and-down"> {% if current_user.is_authenticated() %} <li><a href="{{ url_for('logout_view') }}?next=/admin/">Logout</a></li> {% else %} <li><a href="{{ url_for('login_view') }}?next=/admin/">Login</a></li> {% endif %} </ul> </div> </nav> </header> <div class="container"> {% block content %}{% endblock %} </div> <script type="text/javascript" src="https://code.jquery.com/jquery-2.1.1.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.96.1/js/materialize.min.js"></script> </body> </html>
Notice we use current_user.is_authenticated()
to check if the user is authenticated in the template as current_user
is available in all templates. Now, try writing login.html
and admin.html
on your own, extending base.html
.
One of the reasons why Django got so famous is because it has a nice and flexible administrative interface and we want one too!
Just like Flask-Principal and Flask-Login, Flask-Admin, the extension we'll use to build our administrative interface, does not require a particular database to work with. You may use MongoDB as a relational database (with SQLAlchemy or PeeWee), or another database you happen to like.
Contrary to Django, where the admin interface is focused in the apps/models, Flask-Admin is focused in page/models. You cannot (without some heavy coding) load a whole blueprint (the Flask equivalent of a Django app) into the admin interface, but you can create a page for your blueprint and register the blueprint models with it. One advantage of this approach is that you may pick where all your models will be listed with ease.
In our previous example, we created two models to hold our user and role information, so, let's create a simple admin interface for these two models. We make sure our dependency is installed:
pip install flask-admin
And then:
# coding:utf-8 from flask import Flask from flask.ext.admin import Admin from flask.ext.admin.contrib.sqla import ModelView from flask.ext.login import UserMixin from flask.ext.sqlalchemy import SQLAlchemy db = SQLAlchemy() class User(db.Model, UserMixin): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) active = db.Column(db.Boolean, default=False) username = db.Column(db.String(60), unique=True, nullable=False) password = db.Column(db.String(20), nullable=False) roles = db.relationship( 'Role', backref='roles', lazy='dynamic') def __unicode__(self): return self.username # flask login expects an is_active method in your user model # you usually inactivate a user account if you don't want it # to have access to the system anymore def is_active(self): """ Tells flask-login if the user account is active """ return self.active class Role(db.Model): """ Holds our user roles """ __tablename__ = 'roles' name = db.Column(db.String(60), primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id')) def __unicode__(self): return self.name # Flask and Flask-SQLAlchemy initialization here admin = Admin() admin.add_view(ModelView(User, db.session, category='Profile')) admin.add_view(ModelView(Role, db.session, category='Profile')) def app_factory(name=__name__): app = Flask(name) app.debug = True app.config['SECRET_KEY'] = 'secret' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/ex04.db' db.init_app(app) admin.init_app(app) return app if __name__ == '__main__': app = app_factory() # we need to use a context here, otherwise we'll get a runtime error with app.test_request_context(): db.drop_all() db.create_all() app.run()
In this example, we create and initialize the admin
extension and then register our models with it using ModelView
, a special class that creates a CRUD for our model. Run this code and try to access http://127.0.0.1:5000/admin/
; you'll see a nice administrative interface with a Home link at the top followed by a Profile drop-down with two links, User and Role, that point to our model CRUDs. That's a very basic example that does not amount to much, as you cannot have an administrative interface like that, open to all users.
One way to add authentication and permission verification to our admin views is by extending ModelView
and IndexView
. We'll also use a cool design pattern called mixin
:
# coding:utf-8 # permissions.py from flask.ext.principal import RoleNeed, UserNeed, Permission from flask.ext.principal import Principal principal = Principal() # admin permission role admin_permission = Permission(RoleNeed('admin')) # END of FILE # coding:utf-8 # admin.py from flask import g from flask.ext.login import current_user, login_required from flask.ext.admin import Admin, AdminIndexView, expose from flask.ext.admin.contrib.sqla import ModelView from permissions import * class AuthMixinView(object): def is_accessible(self): has_auth = current_user.is_authenticated() has_perm = admin_permission.allows(g.identity) return has_auth and has_perm class AuthModelView(AuthMixinView, ModelView): @expose() @login_required def index_view(self): return super(ModelView, self).index_view() class AuthAdminIndexView(AuthMixinView, AdminIndexView): @expose() @login_required def index_view(self): return super(AdminIndexView, self).index_view() admin = Admin(name='Administrative Interface', index_view=AuthAdminIndexView())
What are we doing here? We overwrite the is_accessible
method, so that users without permission will receive a forbidden-access message, and overwrite the index_view
for AdminIndexView
and ModelView
, adding the login_required
decorator that will redirect unauthenticated users to the login page. admin_permission
verifies that the given identity has the required set of permissions—RoleNeed('admin')
, in our case.
If you're wondering what a mixin is, try this link http://stackoverflow.com/questions/533631/what-is-a-mixin-and-why-are-they-useful.
As our model already has Create, Read, Update, Delete (CRUD) and permission control access, how could we modify our CRUD to show just certain fields, or prevent the addition of other fields?
Just like Django Admin, Flask-Admin allows you to change your ModelView behavior through setting class attributes. A few of my personal favorites are these:
can_create
: This allows the user to create the model using CRUD.can_edit
: This allows the user to update the model using CRUD.can_delete
: This allows the user to delete the model using CRUD.list_template
, edit_template
, and create_template
: These are default CRUD templates.list_columns
: This implies thats columns show in the list view.column_editable_list
: This indicates columns that can be edited in the list view.form
: This is the form used by CRUD to edit and create views.form_args
: This is used to pass form field arguments. Use it like this:form_args = {'form_field_name': {'parameter': 'value'}} # parameter could be name, for example
form_overrides
: use it to override a form field like this:form_overrides = {'form_field': wtforms.SomeField}
form_choices
: allow you to define choices for a form field. Use it like this:form_choices = {'form_field': [('value store in db', 'value display in the combo box')]}
An example would look like this:
class AuthModelView(AuthMixinView, ModelView): can_edit= False form = MyAuthForm @expose() @login_required def index_view(self): return super(ModelView, self).index_view()
Now, were you willing to add a custom reports page to your administrative interface, you certainly would not use a model view for the task. For these cases, add a custom BaseView
like this:
# coding:utf-8 from flask import Flask from flask.ext.admin import Admin, BaseView, expose class ReportsView(BaseView): @expose('/') def index(self): # make sure reports.html exists return self.render('reports.html') app = Flask(__name__) admin = Admin(app) admin.add_view(ReportsView(name='Reports Page')) if __name__ == '__main__': app.debug = True app.run()
Now you have an admin interface with a nice Reports Page link at the top. Do not forget to write a reports.html
page in order to make the preceding example work.
Now, what if you don't want the link to be shown in the navigation bar, because you have it somewhere else? Overwrite the BaseView.is_visible
method as it controls whether the view will appear in the navigation bar. Do it like this:
class ReportsView(BaseView): … def is_visible(self): return False
3.145.63.136