Flask-Admin Role Based Access - Modify access based on role

David Gleba picture David Gleba · Mar 25, 2016 · Viewed 9.3k times · Source

I took the Flask-Admin auth example from here and changed it slightly.

I added the following block to the view below, but it doesn't show the export button. I was expecting it to add the export option to the admin views. It does print ---superuser to the console.

        if current_user.has_role('superuser'):
            can_export = True
            print ' ---- superuser '

I have used the export feature many times before. It will work if I put the statement can_export = True just below class MyModelView(sqla.ModelView): I am using this as an example of controlling access to creating/editing/etc based on the user role. For example, I will want to have a readonly role where can_create=False, can_edit=False, etc.

Can someone help? Can someone tell me what I am doing wrong?

==

This is the entire view.

# Create customized model view class
class MyModelView(sqla.ModelView):

    def is_accessible(self):
        if not current_user.is_active or not current_user.is_authenticated:
            return False

        if current_user.has_role('superuser'):
            return True

        return False

    def _handle_view(self, name, **kwargs):
        """
        Override builtin _handle_view in order to redirect users when a view is not accessible.
        """
        if current_user.has_role('superuser'):
            can_export = True
            print ' ---- superuser '

        if not self.is_accessible():
            if current_user.is_authenticated:
                # permission denied
                abort(403)
            else:
                # login
                return redirect(url_for('security.login', next=request.url))

==

For reference: I put the all the code here.

Answer

David Gleba picture David Gleba · Mar 26, 2016

To expand further, I continued with the auth example as a base from above and added some simple role based access control. I hope this may help someone.

The full code is here. If you see something in here that is not a good RBAC practice, I would like to hear about it.

The main app.py file is:

import os
from flask import Flask, url_for, redirect, render_template, request, abort
from flask_sqlalchemy import SQLAlchemy
from flask_security import Security, SQLAlchemyUserDatastore, \
    UserMixin, RoleMixin, login_required, current_user
from flask_security.utils import encrypt_password
import flask_admin
from flask_admin.contrib import sqla
from flask_admin import helpers as admin_helpers

# Create Flask application
app = Flask(__name__)
app.config.from_pyfile('config.py')
db = SQLAlchemy(app)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Define models directly without reflection...
class Customer(db.Model):
    CustomerId = db.Column(db.Integer(), primary_key=True)
    FirstName = db.Column(db.Unicode(40), nullable=False)
    LastName = db.Column(db.String(20), nullable=False)
    City = db.Column(db.Unicode(40))
    Email = db.Column(db.Unicode(60), unique = True)

    def __str__(self):
        return self.CustomerID

class City(db.Model):
    Id = db.Column(db.Integer(), primary_key=True)
    City = db.Column(db.Unicode(40), unique = True)

    def __str__(self):
        return self.ID

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

# Define models
roles_users = db.Table(
    'roles_users',
    db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
    db.Column('role_id', db.Integer(), db.ForeignKey('role.id'))
)

class Role(db.Model, RoleMixin):
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(80), unique=True)
    description = db.Column(db.String(255))

    def __str__(self):
        return self.name

class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    first_name = db.Column(db.String(255))
    last_name = db.Column(db.String(255))
    email = db.Column(db.String(255), unique=True)
    password = db.Column(db.String(255))
    active = db.Column(db.Boolean())
    confirmed_at = db.Column(db.DateTime())
    roles = db.relationship('Role', secondary=roles_users,
                            backref=db.backref('users', lazy='dynamic'))

    def __str__(self):
        return self.email

# Setup Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore)

# Flask views
@app.route('/')
def index():
    return render_template('index.html')


# Create customized model view class
class dgBaseView(sqla.ModelView):

    column_display_pk = True
    page_size = 20
    can_view_details = True
    #can_export = False
    can_export = True

    def _handle_view(self, name, **kwargs):
        """
        Override builtin _handle_view in order to redirect users when a view is not accessible.
        """
        if not self.is_accessible():
            if current_user.is_authenticated:
                # permission denied
                abort(403)
            else:
                # login
                return redirect(url_for('security.login', next=request.url))

class regularRbacView(dgBaseView):

    def is_accessible(self):

        # set accessibility...
        if not current_user.is_active or not current_user.is_authenticated:
            return False

        # roles not tied to ascending permissions...
        if  not current_user.has_role('export'):
            self.can_export = False

        # roles with ascending permissions...
        if current_user.has_role('adminrole'):
            self.can_create = True
            self.can_edit = True
            self.can_delete = True
            self.can_export = True
            return True
        if current_user.has_role('supervisor'):
            self.can_create = True
            self.can_edit = True
            self.can_delete = False
            return True
        if current_user.has_role('user'):
            self.can_create = True
            self.can_edit = True
            self.can_delete = False
            return True
        if current_user.has_role('create'):
            self.can_create = True
            self.can_edit = False
            self.can_delete = False
            return True
        if current_user.has_role('read'):
            self.can_create = False
            self.can_edit = False
            self.can_delete = False
            return True
        return False

class lookupRbacView(dgBaseView):

    def is_accessible(self):
        # set accessibility...
        if not current_user.is_active or not current_user.is_authenticated:
            return False

        # roles not tied to ascending permissions...
        if  not current_user.has_role('export'):
            self.can_export = False

        # roles with ascending permissions...
        if current_user.has_role('adminrole'):
            self.can_create = True
            self.can_edit = True
            self.can_delete = True
            self.can_export = True
            return True
        if current_user.has_role('supervisor'):
            self.can_create = True
            self.can_edit = True
            self.can_delete = False
            return True
        if current_user.has_role('user'):
            self.can_create = False
            self.can_edit = False
            self.can_delete = False
            return True
        if current_user.has_role('create'):
            self.can_create = False
            self.can_edit = False
            self.can_delete = False
            return True
        if current_user.has_role('read'):
            self.can_create = False
            self.can_edit = False
            self.can_delete = False
            return True
        return False

class SuperView(dgBaseView):

    can_export = True

    def is_accessible(self):
        if not current_user.is_active or not current_user.is_authenticated:
            return False
        if current_user.has_role('adminrole'):
            self.can_create = True
            self.can_edit = True
            self.can_delete = True
            #self.can_export = True
            return True
        return False


# define a context processor for merging flask-admin's template context into the
# flask-security views.
@security.context_processor
def security_context_processor():
    return dict(
        admin_base_template=admin.base_template,
        admin_view=admin.index_view,
        h=admin_helpers,
    )

# Create admin
admin = flask_admin.Admin(
    app, 'Rbac RoleBasedAccess', base_template='my_master.html', template_mode='bootstrap3',
)

class customer_view(regularRbacView):

    column_searchable_list = ['CustomerId', 'City',  'Email', 'FirstName', 'LastName',]
    # make sure the type of your filter matches your hybrid_property
    column_filters = ['FirstName', 'LastName',  'City',  'Email'   ]
    # column_default_sort = ('part_timestamp', True)
    #column_export_list = ['CustomerId', 'City',  'Email', 'FirstName', 'LastName',]

# Add model views
admin.add_view(SuperView(Role, db.session))
admin.add_view(SuperView(User, db.session))
admin.add_view(customer_view(Customer, db.session))
admin.add_view(lookupRbacView(City, db.session))


def build_sample_db():
    """
    Populate a small db with some example entries.
    """
    import string

    #db.drop_all()
    db.create_all()

    with app.app_context():
        read_role = Role(name='read')
        user_role = Role(name='user')
        super_user_role = Role(name='adminrole')
        db.session.add(user_role)
        db.session.add(super_user_role)
        db.session.add(Role(name='read'))
        db.session.add(Role(name='create'))     
        db.session.add(Role(name='supervisor'))
        db.session.add(Role(name='delete'))
        db.session.add(Role(name='export'))
        db.session.commit()

        test_user = user_datastore.create_user(
            first_name='Admin',
            email='admin',
            password=encrypt_password('admin'),
            roles=[user_role, super_user_role]
        )


        first_names = [
            'read', 'create', 'user', 'suser',  'delete',   'Charlie', 'Sophie',  'Mia',
        ]
        last_names = [
            'Brown', 'Smith',  'Patel', 'Jones', 'Williams', 'Johnson', 'Taylor', 'Thomas',
        ]
        roles1 = [
            'read',  'create',  'user', 'supervisor', 'delete', 'read', 'read', 'read',
        ]

        for i in range(len(first_names)):
            tmp_email = first_names[i].lower()
            # initialize the users with simple password...  'a'
            tmp_pass = 'a'
            user_datastore.create_user(
                first_name=first_names[i],
                last_name=last_names[i],
                email=tmp_email,
                password=encrypt_password(tmp_pass),
                roles=[read_role, ]
            )
        db.session.commit()
    return

if __name__ == '__main__':

    # Build a sample db on the fly, if one does not exist yet.
    app_dir = os.path.realpath(os.path.dirname(__file__))
    database_path = os.path.join(app_dir, app.config['DATABASE_FILE'])
    if not os.path.exists(database_path):
        build_sample_db()
    app.run(host='0.0.0.0', port=5000, debug=True)

The config.py is:

# http://stackoverflow.com/questions/5055042/whats-the-best-practice-using-a-settings-file-in-python
import creds

# Create dummy secret key so we can use sessions
SECRET_KEY = creds.cred['secretkey']

# Create in-memory database
DATABASE_FILE = 'fground.sqlite'
SQLALCHEMY_DATABASE_URI = creds.cred['dbspec'] + DATABASE_FILE
SQLALCHEMY_ECHO = True

# Flask-Security config
SECURITY_URL_PREFIX = "/admin"
SECURITY_PASSWORD_HASH = "pbkdf2_sha512"
SECURITY_PASSWORD_SALT = creds.cred['csalt']

# Flask-Security URLs, overridden because they don't put a / at the end
SECURITY_LOGIN_URL = "/login/"
SECURITY_LOGOUT_URL = "/logout/"
SECURITY_REGISTER_URL = "/register/"

SECURITY_POST_LOGIN_VIEW = "/admin/"
SECURITY_POST_LOGOUT_VIEW = "/admin/"
SECURITY_POST_REGISTER_VIEW = "/admin/"

# Flask-Security features
SECURITY_REGISTERABLE = True
SECURITY_SEND_REGISTER_EMAIL = False

The creds.py is:

cred = dict(
    secretkey = '123232323238',
    dbspec    = 'sqlite:///',
    csalt     = "ATGUOHAELKiubaq3fgo8hiughaerGOJAEGj",
    dbu = 'user',
    dbp = 'pass',
)

To run this, I recommend you start with the flask-admin auth example above and then copy these files into that example. Running it should create a database with users and roles. Also, you could get all the code ready to go at the github link.