pallets-eco / flask-principal

Identity management for Flask applications
MIT License
496 stars 89 forks source link

Consider adding ability to lazily load data for permission objects #6

Open mattupstate opened 12 years ago

mattupstate commented 12 years ago

Considering the hypothetical example in the current documentation for granular resource protection, what if a user has hundreds, even thousands of posts or some other sort of attribute. Perhaps adding a way to lazily load some sort of condition in the permission object can avoid loading all that data and inflating the identity on every request.

wshrdryr commented 11 years ago

about to crack a million permissioned resources here, so yes i'd vote for this

mattupstate commented 11 years ago

Any thoughts on how to implement this? I did something at one point in another project of mine where the permission object is created on the fly in a decorator. I used a class property that matches the url parameter. For example:

from collections import namedtuple
from functools import partial

from flask.ext.principal import Permission as _Permission

BlogPostNeed = namedtuple('BlogPostNeed', ['method', 'value'])
EditBlogPostNeed = partial(BlogPostNeed, 'edit')

class Permission(_Permission):
    name = None
    need = None

class BlogPostPermission(Permission):
    uri_parameter_key = 'blog_post_id'

class EditBlogPostPermission(BlogPostPermission):
    name = 'edit'
    need = EditBlogPostNeed

    def __init__(self, blog_post_id):
        super(EditBlogPostPermission, self).__init__(self.need(unicode(blog_post_id)))

# Decorator...
def accepted_permissions(*perms):
    def wrapper(fn):
        @wraps(fn)
        def decorated(*args, **kwargs):
            for clazz in perms:
                if clazz(kwargs[clazz.uri_parameter_key]).can():
                    return fn(*args, **kwargs)
            abort(403)
        return decorated
    return wrapper

# In action:
@accepted_permissions(EditBlogPostNeed)
@app.route('/posts/<blog_post_id>', methods=['PUT', 'PATCH'])
def edit_blog_post(blog_post_id):
    # .... do stuff ....
    return render_template(...)

But all the needs are loaded before each request still. I'm still not sure how I would lazily check for one resource.

wshrdryr commented 11 years ago

Yes, on reflection it seems like a fundamental redesign is required to get there. Perhaps so fundamental that it ought to be a different extension?

I'm thinking this needs base classes for Principal and Resource that have some SQLA cascading magic so the decorators have some data structures to check at runtime. I'd be happy to hear of a clever way to avoid going that far but if you plan to have granular permissions, you need to define a storage layer too, right?

mattupstate commented 11 years ago

I'm not convinced a fundamental redesign is required. Flask-Principal is already very minimal in its design.

wshrdryr commented 11 years ago

Okay, given that,

Are we talking about an ACL-based system?

I have assumed so up to this point but perhaps you see a different way to do this.

mattupstate commented 11 years ago

I would say Flask-Principal gives you to the tools to build an ACL system already. Permissions are attached to the identity at the developer's discretion.

The problem I'd like to solve here is how and when to check the permissions for a given resource to the identity. The easiest way is to do this add all the permissions to the identity in a app.before_request handler but obviously this is not scalable if there are thousands of resources a user may be able to access.

So when in the context of a request should the permission(s) for a resource be created and checked? The obvious approach to me is a decorator of sorts like I have above but I developed that in a vacuum and I'm not sure it will serve the use case of anyone else.

metatoaster commented 11 years ago

I don't think a redesign is needed at all. Granular access control on resources generally requires an understanding of the associated mission specific implementation of data structures, which flask-principal, being a generic framework, would be completely agnostic about them. That said, this generic implementation allows the overriding of these classes for suitable purposes, even for lazily loaded permission checks.

The current Permission class should be a subclass of a generic implementation (say PermissionBase) with the allows method that raises NotImplementedError, then giving a clearer indication to integrators to create their own permission items.

This also allows the implementation of a more clear set of operators on permission objects, where by default a & operation on permission objects will yield a new permission object that requires both preceding ones, and then a set of | to or a set of permissions that specifies any of those permissions.

Now to solve that original problem posed here: I have an example that sort of addresses this.

class BlogEntryEditPermission(Permission):
    """
    Lazily loaded permission check.
    """

    def allows(self, identity):
        # current entry is already in the request
        blog_id = request.view_args.get('blog_id')
        blog = blogs.get(blog_id)
        if not blog:
            # free sanity check.
            abort(404)

        result = blog.identity == identity.id
        return result

blog_edit = BlogEntryEditPermission()

# ...

@app.route('/blog/edit/<int:blog_id>')
@blogger.require()
@blog_edit.require()
def blog_edit(blog_id):
    return 'Editing blog entry: %s' % blog_id

Since the permission checks happen within a flask request context, the view has all the values needed to do pin-point checks on what needs to be done. I have the example posted as a gist. Once you started the server, have your client open to the /login/${userid} endpoint to log in as the user, specified at the role_map dictionary for the roles needed. Yes it is possible to have millions of blog entries without having to bog down checks when a smarter Permission class is built to guard a view.

mattupstate commented 11 years ago

@metatoaster Thanks for your input. This is certainly one way of going about it and should work for many implementations. I've started working on a simple ACL/ACE system for Flask-Security that, once I'm done with it, I could provide an example, along with yours, in the documentation on how to use Flask-Principal in more "advanced" ways.

lyschoening commented 10 years ago

+1

DerekDomino commented 10 years ago

@mattupstate Do you have any update on an example of ACL with Flask-Principal? Having examples would greatly help in understanding concepts of Flask-Principal. Thanks.

mattupstate commented 10 years ago

@DerekDomino unfortunately I do not. I am, however, experimenting with a different approach to an ACL system over here

frol commented 9 years ago

Just for your info, looking for a solution of simple permissions handling in my Flask app, I have found a really simple, lightweight and powerful module permission! Even though it is mentioned that it is intended for Flask, it is a general purpose Python module with zero dependencies. It is much easier than Flask-Principal, which I couldn't get right (it works, but I tried to wrap it to make it simple and good-looking to maintain permissions in future) after a day of hardwork.