Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flask Principal granular resource on demand

I've been looking at this post: http://pythonhosted.org/Flask-Principal/#granular-resource-protection

Now while there is nothing wrong with how it is currently working I can't see that it is very usable since at the time of login all posts are read and the EditBlogPostNeed is added to the identity.

Imagine if I have been writing more than the normal number of posts it will long term not be a very good strategy as I would like to check the post as I access the view /posts/<post_id>.

Is there a way to do this check on each request for the view using Flask Principal?

I can of course get around it very easily with a lazy relationship query and filter but I want to use Flask Principal.

like image 534
Asken Avatar asked Aug 19 '13 17:08

Asken


3 Answers

Not sure I understand your question entirely, but this might help. In a Flask app, I'm using Flask-Principal for role permission such as admin and editor, and I'm also using it for granular resource protection as described in the Flask-Principal docs. In my case I'm checking if a user has permission to access a particular account. In each view the identity is loaded and the permission is checked.

In the view:

@login_required
def call_list(id, page=1):

    dept = models.Department.query.get_or_404(id)
    view_permission = auth.ViewAccountPermission(dept.office.account_id)

    if view_permission.can():
        # do something

The custom permission:

ViewAccount = namedtuple('View', ['method', 'value'])
ViewAccountNeed = partial(ViewAccount, 'view')

class ViewAccountPermission(Permission):
    def __init__(self, account_id):
        need = ViewAccountNeed(unicode(account_id))
        super(ViewAccountPermission, self).__init__(need)

And in the identity loader function:

if hasattr(current_user, 'assigned_accounts'):
    for account_id in current_user.assigned_accounts():
        identity.provides.add(auth.ViewAccountNeed(unicode(account_id)))
like image 187
bill__ Avatar answered Oct 29 '22 14:10

bill__


While Flask-Principal is the most popular plugin, it is unnecessary complicated and it just doesn't work in the most cases I need it. I have been trying to force it to work the way I like it, but I have never succeeded. Luckily, I have found an extremely straightforward and lightweight module - permission:

Usage

First you need to define your own rules by subclassing Rule then override check() and deny():

# rules.py
from flask import session, flash, redirect, url_for
from permission import Rule

class UserRule(Rule):
    def check(self):
        """Check if there is a user signed in."""
        return 'user_id' in session

    def deny(self):
        """When no user signed in, redirect to signin page."""
        flash('Sign in first.')
        return redirect(url_for('signin'))

Then you define permissions by subclassing Permission and override rule():

# permissions.py
from permission import Permission
from .rules import UserRule

class UserPermission(Permission):
    """Only signin user has this permission."""
    def rule(self):
        return UserRule()

There are 4 ways to use the UserPermission defined above:

1. Use as view decorator

from .permissions import UserPermission

@app.route('/settings')
@UserPermission()
def settings():
    """User settings page, only accessable for sign-in user."""
    return render_template('settings.html')

2. Use in view codes

from .permissions import UserPermission

@app.route('/settions')
def settings():
    permission = UserPermission()
    if not permission.check()
        return permission.deny()
    return render_template('settings.html')

3. Use in view codes (using with statement)

from .permissions import UserPermission

@app.route('/settions')
def settings():
    with UserPermission():
        return render_template('settings.html')

4. Use in Jinja2 templates

First you need to inject your defined permissions to template context:

from . import permissions

@app.context_processor
def inject_vars():
    return dict(
        permissions=permissions
    )

then in templates:

{% if permissions.UserPermission().check() %}
    <a href="{{ url_for('new') }}">New</a>
{% endif %}
like image 24
Vlad Frolov Avatar answered Oct 29 '22 14:10

Vlad Frolov


Everything I can find on this topic seems overly obtuse. While not what I initially desired, I've decided to simply handle this manually in my view functions. It's more explicit, and it reduces extra queries against the database. Do note that I'm still using flask-security for its out-of-the-box role-based authentication (which is still implemented via flask-principal via its @roles_accepted('role') decorator.

@app.route('/my_accounts/', methods = ['GET'])
@app.route('/my_accounts/<int:id>/', methods = ['GET'])
@roles_accepted('client')
def my_accounts(id=None):

    if id:
        account = Account.query.get_or_404(id)

        if account.owner == current_user:
            return render_template("my_account.html",
                                   title = "Account: {0}".format(account.name),
                                   account = account)
        else:
            abort(403)

    accounts = Account.query.filter_by(owner=current_user).all()

    return render_template("my_accounts.html",
                           title = 'My Accounts',
                           accounts = accounts)
like image 33
Chockomonkey Avatar answered Oct 29 '22 13:10

Chockomonkey