Zion Zion - 3 months ago 35
Python Question

flask-admin is_accessible usage

I've checked the docs and It's pretty vague how the is_accessible method should be implemented.

Here is what the docs of flask admin showed

class MicroBlogModelView(sqla.ModelView):

def is_accessible(self):
return login.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))


what I don't get though is how do you call it is it automatically called or do you have to call it yourself like this:

@expose("/", methods=["GET", "POST"])
def home(self):
if self.is_accesible():
return super().index()
else:
return self.login()

def is_accesible(self):
return current_user.is_authenticated and "admin" in current_user.role


because It would be pretty darn repetitive to put a

if self.is_accesible():
return super().index()


check if we had a lot of admin views.
so how exactly do we implement it? the docs showed how to put it in your model but not how to implement it on your views

Answer

See the example code below - most of the code is taken verbatim from Quokka CMS

Define a Roled mixin that handles def is_accessible(self) and def _handle_view(self, name, *args, **kwargs).

In the example I've used a function , def is_accessible(roles_accepted=None, user=None), to handle the logic of determining if a role is acceptable.

Define a view class from sqla ModelView and the Roled mixin, namely:

class AdminView(Roled, ModelView):

    def __init__(self, *args, **kwargs):
        self.roles_accepted = kwargs.pop('roles_accepted', list())
        super(AdminView, self).__init__(*args, **kwargs)

Derive your view models from this class:

class UserView(AdminView):
    form_excluded_columns = ('password')   

class RoleView(AdminView):
    pass

class PostView(AdminView):
    pass

Add your views to the admin class in the normal manner but pass a list of allowed role names via the roles_accepted keyword:

admin.add_view(UserView(model=User, session=db.session, category='Account', name='Users', roles_accepted=['admin']))
admin.add_view(RoleView(model=Role, session=db.session, category='Account', name='Roles', roles_accepted=['admin']))
admin.add_view(PostView(model=Post, session=db.session, category='Blog', name='Posts (Editor Only)', roles_accepted=['editor']))
admin.add_view(PostView(model=Post, session=db.session, category='Blog', name='Posts (Admins & Editors)', endpoint="post_special", roles_accepted=['editor', 'admin']))

See full single file code below. This was tested under Python 2.7.9, Flask 0.10.1, flask-admin 1.1.0, flask-security 1.7.4 and flask-login 0.2.11

from datetime import datetime
from flask import Flask, redirect, current_app
from flask.ext.admin import Admin
from flask.ext.admin.contrib.sqla import ModelView
from flask.ext.admin.menu import MenuLink
from flask.ext.security import (
    current_user,
    url_for_security,
    UserMixin,
    RoleMixin,
    SQLAlchemyUserDatastore,
    Security
)
from flask.ext.security.utils import encrypt_password
from flask.ext.sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'super-secret'
app.config['SECURITY_PASSWORD_HASH'] = 'pbkdf2_sha512'
app.config['SECURITY_PASSWORD_SALT'] = '16a0af319890f662055ba10aecff37e7e033db3fba737e55'
app.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] = 'email'

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)


class Role(db.Model, RoleMixin):
    __tablename__ = 'roles'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Unicode(length=64), unique=True)
    description = db.Column(db.Unicode(length=255), nullable=True)

    def __unicode__(self):
        return u"{name} ({role})".format(name=self.name, role=self.description or 'Role')

user_to_role = db.Table('user_to_role',
    db.Column('user_id', db.Integer(), db.ForeignKey('users.id')),
    db.Column('role_id', db.Integer(), db.ForeignKey('roles.id')))


class User(db.Model, UserMixin):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)

    first_name = db.Column(db.Unicode(length=255), nullable=False)
    last_name = db.Column(db.Unicode(length=255), nullable=False)

    email = db.Column(db.Unicode(length=254), unique=True, nullable=True)
    password = db.Column(db.Unicode(length=255), nullable=False)
    active = db.Column(db.Boolean(), default=False)

    roles = db.relationship('Role', secondary=user_to_role, backref=db.backref('users', lazy='select'))

    def __unicode__(self):
        return u"{first_name} ({last_name})".format(first_name=self.first_name, last_name=self.last_name)


class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(255))
    content = db.Column(db.Text, nullable=False)

    def __unicode__(self):
        return self.title


def get_current_user():
    from flask.ext.security import current_user
    try:
        return User.objects.get(id=current_user.id)
    except Exception as e:
        # logger.warning("No user found: %s", str(e))
        return current_user


def is_accessible(roles_accepted=None, user=None):
    user = user or get_current_user()
    # uncomment if "admin" has access to everything
    # if user.has_role('admin'):
    #     return True
    if roles_accepted:
        accessible = any(
            [user.has_role(role) for role in roles_accepted]
        )
        return accessible
    return True


class Roled(object):

    def is_accessible(self):
        roles_accepted = getattr(self, 'roles_accepted', None)
        return is_accessible(roles_accepted=roles_accepted, user=current_user)

    def _handle_view(self, name, *args, **kwargs):
        if not current_user.is_authenticated():
            return redirect(url_for_security('login', next="/admin"))
        if not self.is_accessible():
            # return self.render("admin/denied.html")
            return "<p>Access denied</p>"

class AdminView(Roled, ModelView):

    def __init__(self, *args, **kwargs):
        self.roles_accepted = kwargs.pop('roles_accepted', list())
        super(AdminView, self).__init__(*args, **kwargs)


class UserView(AdminView):
    form_excluded_columns = ('password')


class RoleView(AdminView):
    pass


class PostView(AdminView):
    pass


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

@app.route('/')
def index():
    _login = url_for_security('login', next="/admin")
    _logout = url_for_security('logout', next="/admin")
    return '''
        <a href="/admin/">Click me to get to Admin!</a><br>
        <a href="{login}">Click me to get to login!</a><br>
        <a href="{logout}">Click me to get to logout!</a>
        '''.format(login=_login, logout=_logout)


# Create admin
admin = Admin(app, name='Admin')
admin.add_view(UserView(model=User, session=db.session, category='Account', name='Users', roles_accepted=['admin']))
admin.add_view(RoleView(model=Role, session=db.session, category='Account', name='Roles', roles_accepted=['admin']))
admin.add_view(PostView(model=Post, session=db.session, category='Blog', name='Posts (Editor Only)', roles_accepted=['editor']))
admin.add_view(PostView(model=Post, session=db.session, category='Blog', name='Posts (Admins & Editors)', endpoint="post_special", roles_accepted=['editor', 'admin']))
admin.add_link(MenuLink(name='Public Website', category='', url='/'))


def build_db():
    users = [
        {
            'first_name': 'Super',
            'last_name': 'User',
            'email': 'admin@example.com',
            'active': True,
            'password' : encrypt_password('password'),
            'roles': ['admin']
        },
        {
            'first_name': u'Post',
            'last_name': u'Editor',
            'email': 'editor@example.com',
            'active': True,
            'password': encrypt_password('password'),
            'roles': ['editor']
        },

    ]

    posts = [
        {
            'title': "de Finibus Bonorum et Malorum - Part I",
            'content': "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut"
        },
        {
            'title': "de Finibus Bonorum et Malorum - Part II",
            'content': "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque"
        },
        {
            'title': "de Finibus Bonorum et Malorum - Part III",
            'content': "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium"
        }
    ]

    db.drop_all()
    db.create_all()

    security = current_app.extensions.get('security')

    security.datastore.create_role(name=u"admin", description=u'Administers the system')
    security.datastore.create_role(name=u"editor", description=u'Can edit posts')

    for user in users:
        roles = user.pop('roles')
        user_db = security.datastore.create_user(**user)
        for role_name in roles:
            role_from_db = security.datastore.find_role(role_name)
            security.datastore.add_role_to_user(user_db, role_from_db)
        security.datastore.activate_user(user_db)
        user_db.confirmed_at = datetime.now()

    security.datastore.commit()

    for row in posts:
        post = Post(**row)
        db.session.add(post)

    db.session.commit()


@app.before_first_request
def create_user():
    build_db()

if __name__ == '__main__':
    app.run(debug=True)
Comments