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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With