Search code examples
pythonaclpyramid

Pyramid "per object" permissions


I am trying to create a security system for my toolbox of applications that I am building. I previously had an acl, but the problem with that was I couldn't control on an application based level.

I think the picture below describes how I have it setup in the database. Picture of ACL layout in database

Basically, what I think I need is to pass the application GUID to the groupfinder() function, and I am not sure how I should be going about this. Anybody have any ideas?


Solution

  • The groupfinder and the context factory in Pyramid are intended to operate separately and in isolation. The former is used to define the "current user" and the latter is used to define the "current resource". These two items are finally combined with a permission via the authorization policy's permits function to determine whether the "current user" has "permission" to operate on this "resource".

    1. The groupfinder is responsible for identifying the user. This means converting them into a set of principals that can be used later.

    2. The context factory determines the current resource (context)... so this should normally be different per url (remember the R in URL is resource) so the url normally defines the resource.

    3. The view attached to a url defines the permission needed (for the operation being performed by the view).

    Step 2 here is the hardest for people to grok when coming to Pyramid. How do I define this resource for each URL? It is covered in the url dispatch tutorial [1] but I will explain quickly here.

    As I said above, each route usually represents a resource so we can put that into code by defining an object for that route. This is done via the factory argument to config.add_route(..., factory=...) and it can do lots of things with the route to determine what the current resource is.

    def page_factory(request):
        # I'm attached to a page so I can grab the matchdict
        pageid = request.matchdict['pageid']
        page = request.db.query(Page).get(pageid)
        if page is None:
            raise HTTPNotFound
        return page
    
    config.add_route('page', '/pages/{pageid}', factory=page_factory)
    

    This snippet defines a route with a page factory and every view attached to this route is going to have the page as the context (request.context).

    The Page object here has an ACL defining what users/groups (principals) are allowed to do what (permissions) on it.

    class Page(Base):
        __tablename__ = 'page'
    
        # a bunch of columns
    
        def __acl__(self):
            return [
                (Allow, f'app:{self.app.id} user:{self.owner_id}', 'edit'),
                (Allow, f'app:{self.app.id}', 'read'),
            ]
    

    Assuming this page is attached to app1, we can easily define a groupfinder saying that anyone in app1 can read it:

    def groupfinder(userid, request):
        user = request.db.query(User).get(userid)
        if user is not None:
            principals = []
            for app in user.apps:
                principals += [
                    f'app:{app.id}',
                    f'app:{app.id} user:{user.id}',
                ]
                for group in app.groups:
                    principals += [f'app:{app.id} group:{group.id}']
            return principals
    

    The groupfinder doesn't know about pages but it describes the user with enough granularity that the page can match them up with the operations it allows.

    [1] https://docs.pylonsproject.org/projects/pyramid/en/1.9-branch/tutorials/wiki2/authorization.html