Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I restrict permissions based on the single page ID in the URL?

I'm trying to implement Pyramid's Security features in my website but I'm having some trouble figuring out how to use it.

I've been reading over this tutorial and this example, as well as the Pyramid docs, and I can't figure out how to implement an authorization policy for single page IDs.

For example, I have the following URL scheme:

/pages
/pages/12

/pages obviously lists the available pages and /pages/:id is where you can read/comment on the page.

The documentation/examples I've read have shown that you can implement group level ACS's by providing a groupfinder callback with a list of groups. Such as editor, admin, etc.

How can I not use a group for permissions and instead rights based on the page id?

In my URL scheme above, when the user browses to /pages they must be logged in. When they browse to /pages/:id, they must have been given access to view that particular id. Or, they must be the owner of that page.

Same as comments. On the /page/:id page, they may have been given access to view the page but not comment on it.

like image 426
Kane Avatar asked Apr 22 '12 09:04

Kane


2 Answers

The basic principle here is that Pyramid's security machinery checks the ACL on the current context. In this case your page would be the logical context to use. The first step is to setup a context factory for a page. Assuming you are using SQLAlchemy and URL dispatch this is simple to do. Register your route like this:

config.add_route('page', '/pages/{id:\d+}', factory=page_factory)

There is a little trick in the path for the route that makes pyramid check the page id must be a number so you do not have to check that yourself. Note the reference to a *page_factory* method. Lets define that now:

def page_factory(request):
    return DBSession.query(Page).get(int(request.matchdict['id']))

This takes the page id from the route and uses that to lookup the page in your database. Notice that we do not check if the id can be converted to an integer here: we can get away with that since the route already checks that directly.

The next step is to setup the ACL on the page. The simplest way is to add a acl property to you Page class:

from pyramid import security

class Page(BaseObject):
    @property
    def __acl__(self):
        return [(security.Allow, self.userid, 'view')]

This ACL tells pyramid that only the user with the id stored in page.userid is allowed to view that page. What is important to realise here is that the ACL is different for every page: it is generated for every page separately based on the information in your database; in this case using self.userid.

You can now use the view permission on your view:

@view_config(route_name='page', context=Page, permission='view')
def page_view(context, request):
    return 'I can see!'

This example has a very minimal ACL for a page, but you can extend that to fit your needs.

Also note the context=Page parameter for view_config: this tells pyramid that this view should only be used of the context is a Page. If the context factory (page_factory in this example) did not find a matching page it will return None instead of a Page instance, so this view will not be used by pyramid. As a result pyramid will automatically produce a not-found error.

like image 186
Wichert Akkerman Avatar answered Nov 15 '22 08:11

Wichert Akkerman


For the purposes of this discussion, I'm going to assume you are using SQLAlchemy to interface with your DB.

If you have config.add_route('pages', '/pages/{id}') in your __init__.py, you can add a custom factory to replace/supplement your default ACL. For example:

Your current ACL may look like this:

class RootFactory(object):
    __acl__ = [
        (Allow, Everyone, 'view'),
        (Allow, Authenticated, 'auth'),
    ]

    def __init__(self, request):
        self.request = request

This would allow Authenticated users to access any view with a permission of 'auth', and anyone that visits your site to access any view with a permission of 'view'.

By using a custom factory, you can either bypass your RootFactory, or supplement it.

To bypass, change your original config.add_route to--> config.add_route('pages', '/pages/{id}', factory=PageFactory) and create a PageFactory class like this:

class PageFactory(object):
    __acl__ = [
        (Allow, Everyone, 'view'),
        (Allow, Authenticated, 'auth'),
    ]

    def __init__(self, request):
        self.request = request

    from pyramid.security import authenticated_userid
    user_id = authenticated_userid(self.request)

    thispage = DBSession.query(Page).filter(Page.id==self.request.matchdict['id']).first()

    if thispage.user_id == user_id:
        ## Pyramid allows Everyone, Authenticated, and authenticated_userid
        ## (each of these is known as a Principal) to be in the second
        ## position of the ACL tuple
        acl.append((Allow, user_id, 'edit'))

This is assuming your view has permission='edit' as one of its parameters.

Now, if you would like to use the RootFactory and supplement it with your custom factory, so you don't have to repeat yourself, simply leave you RootFactory as I've shown at the beginning of this post, and inherit from the RootFactory class, as such:

class PageFactory(RootFactory):
    @property
    def __acl__(self):
        acl = super(PageFactory, self).__acl__[:] ##[:] creates a copy

        from pyramid.security import authenticated_userid
        user_id = authenticated_userid(self.request)

        thispage = DBSession.query(Page).filter(Page.id==self.request.matchdict['id']).first()

        if thispage.user_id == user_id:
            acl.append((Allow, user_id, 'edit'))

        return acl

The groupfinder is very useful, by the way, because then you can simply place users in groups, such as 'admin', and all those in the admin group can access views with permission='whatever' or permission='whateverelse' that you may want, and no Factory needed, only a groupfinder that returns a grouplist for the current user. Alas, I digress, as that's not what you were looking to do. Hope this answers your question.

like image 28
Raj Avatar answered Nov 15 '22 10:11

Raj