Flask-Principal and Flask-Login (aka Batman and Robin)

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.

  • Identity: This implies the way Flask-Principal identifies a user.
  • IdentityContext: This implies the context of a user tested against Permission. It is used to verify whether the user has the right to do something. It can be used as a decorator (block unauthorized access) or as a context manager (only execute).

    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'])
  • Permission: This is a group of needs that should be satisfied in order to allow something. Interpret it as a guardian of resources.

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.

Tip

Notice we give very broad error messages for this particular form. We do this in order to avoid giving too much info to a possible attacker.

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.

Tip

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.

Admin like a boss

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.

Tip

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()

Custom pages

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
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.63.136