Pyramid ACL without traversal
Asked Answered
K

1

9

I really have little idea how ACL do work. I know it's pretty cool and could save me lots of time and pain. But currently i'm a bit lost. All example for pyramid do use traversal. I exclusively use URL Dispatch. I'm not sure to understand how I can build a ressource tree structure.

Here is a sample of code:

class QuestionFactory(object):

    def __init__(self, request):
        self.__acl__ = default[:]
        self.uid = authenticated_userid(request)

        self.qid = request.matchdict.get('id')
        if self.qid:
            self.question = request.db.questions.find_one({'_id': ObjectId(self.qid)})
            if str(self.question.get('owner')) == self.uid:
                self.__acl__.append((Allow, userid, 'view'))     

The thing is that, it works. But I do have to define a new factory for every type of ressource. I'm not certain how I'm supposed to know which ressource I'm trying to access trough URL Dispatch and Factory. I'd see something like that

/accounts/{account}   //Owners only but viewable by anyone 
/messages/{message}   //Owners only
/configs/{config}     //Admin only
/pages/{page}         //Admins only but viewable by anyone

That said here I'd have such structure

  Root -\
         +-- account
         +-- message
         +-- config
         +-- page

Each of these factory has it's own special acl. The other thing is that /accounts is the main page. It doesn't have an id or anything. Also /accounts/new is also a special case. It's not an id but the view to create a new item.

I'm using a restful style with GET/PUT/DELETE/POST requirement. I'm not so sure how I'm supposed to match url to a ressource and to the right acl automatically. If I define in my root a special factory like above there is no problems.

edit

I did got it to work with the exception of some things. I finally think I understand what is the purpose of traverse. For example with we have that url: /comments/9494f0eda/new, /comments/{comment}/new

We could have to Node in our ressource tree or even 3 nodes.

The RootFactory will get inspected first, then according to our traversal. It will get the comments attribute of RootFactory, then "comment" of Comment factory and "new" of CommentFactory or of the Object itself

I don't use Factory as dict as in the example of Michael

It looks pretty much like that:

class RessourceFactory(object):
    def __init__(self, parent, name):

        self.__acl__ = []
        self.__name__ = name
        self.__parent__ = parent

        self.uid = parent.uid
        self.locale = parent.locale
        self.db = parent.db
        self.req = parent.req

This is my base ressource object. On every steps it copies information from the parent to the new child.. I could certainly bubble up my attribute.. context.parent._parent_.uid but that is just not that great.

The reason why I'm not using the dict attribute. I add to make it works with

/comments

For some reasons, it did create my CommentFactory but didn't return it as there wasn't need for a key.

So my root Factory pretty much look like this:

class RootFactory(object):

    def __init__(self, request):
        self.__acl__ = default[:]

        self.req = request
        self.db = request.db

        self.uid = authenticated_userid(request)
        self.locale = request.params.get('locale', 'en')

    def __getitem__(self, key):

        if key == 'questions':
            return QuestionFactory(self, 'questions')
        elif key == 'pages':
            return PageFactory(self, 'pages')
        elif key == 'configs':
            return ConfigFactory(self, 'configs')
        elif key == 'accounts':
            return AccountFactory(self, 'accounts')

        return self

if no item is found, RootFactory return itself if not, it return a new Factory. Since I base my code on Michael's code there is a second parameter for Factory constructor. I'm not certain to keep it as a QuestionFactory is well aware to handle "questions" so there is no need to name the factory here. It should already know its name.

class QuestionFactory(RessourceFactory):
    def __init__(self, parent, name):
        RessourceFactory.__init__(self, parent, name)
        self.__acl__.append((Allow, 'g:admin', 'view'))
        self.__acl__.append((Allow, 'g:admin', 'edit'))
        self.__acl__.append((Allow, 'g:admin', 'create'))
        self.__acl__.append((Allow, 'g:admin', 'delete'))
        self.__acl__.append((Allow, Everyone, 'create'))

    def __getitem__(self, key):

        if key=='read':
            return self

        self.qid = key
        self.question = self.db.questions.find_one({'_id': ObjectId(self.qid)})

        if str(self.question.get('owner')) == self.uid:
            log.info('Allowd user %s' % self.uid)
            self.__acl__.append((Allow, self.uid, 'view'))
            self.__acl__.append((Allow, self.uid, 'edit'))
            self.__acl__.append((Allow, self.uid, 'delete'))

        return self

So that is where almost all logic will go. In the init I set acl that will work for /questions in the getitem it will work for /questions/{id}/*

Since I return itself, any getitem past this RessourceFactory will point to itself unless I return a new Factory for some special case. The reason why doing so is that my context isn't just an object in database or an object.

My context handles multiple things like user id, locale and so on... when the acl is done, I have a fresh context object ready to use. It removes most of the logic in the views.

I could probably set events to query locale and uid but it really fits here. If I need anything new, I just have to edit my RootFactory and RessourceFactory to copy them to child factory.

That way if something has to change accross all views, there is no redundancy at all.

Kodok answered 24/10, 2011 at 18:21 Comment(0)
A
6

It looks like you're interested in some object/row-level security features to allow only owners of accounts to view their data. I would refer you to my previous SO answer on this topic, as well as the tutorial I've been working on for auth in URL Dispatch which is built around this answer. Specifically you might want to look at the 2.object_security demo in the linked github project as well as the docs explaining resource trees as part of the rendered html on my site.

Pyramid authorization for stored items

https://github.com/mmerickel/pyramid_auth_demo

http://michael.merickel.org/projects/pyramid_auth_demo/

If you have any questions understanding those resources I'd be happy to elaborate further here.

Anaerobe answered 24/10, 2011 at 20:14 Comment(15)
I don't know, I already all read that. As I said in the title... I'm trying to achieve this without traversal. I tried once and it messed my routes and nothing worked. I don't believe that I have any tree hierachy too. since I have pages, messages and so on but no objects really relate to one other. So such thing /foo/bar/baz doesn't speak much to me.Jeramyjerba
I did some test and it might work after all... I'll write something tomorrow I have to sleep before my exam tomorrow.Jeramyjerba
The links I gave you are using URL dispatch... pyramid's auth system works hierarchically by traversing a tree of objects. This is how the ACLAuthorizationPolicy works. I'd be surprised if you "already read all that" because I just re-wrote that demo last night and am actively working on it. ;-)Anaerobe
Your mechanism of simply using a one-node tree (the QuestionFactory) is valid, and if that makes the most sense to you, then go ahead and use it.Anaerobe
Well I did probably read most of it since I just read it. Tough it works. I got traverse to works and it simpler than I thought.Jeramyjerba
I posted my solution, it might look a bit like yours. Except that I don't return RessourceObject and don't use dict.Jeramyjerba
So you've setup a global root factory and then on the individual routes are you setting the "traverse" parameter? Your solution is fine and I'm glad it works for you. Remember that a resource should have entries for a user whether it's that user trying to access it or not, thus passing authenticated_userid into the QuestionFactory is actually incorrect, you could've generated those entries without it. Later when auth happens the userid will be a principal that is compared to the entries you added.Anaerobe
Yes, the traverse parameter, since I have a "route" builder. It's not much of a big task. rest_route(request, location, ressource_name), If you were familiar with rest_controller. I'm not sure to understand what you meant by incorrect. and "resource should have entries for a user whether it's that user trying to access it or not"Jeramyjerba
It might be a bit misleading but Question is a received question. It's not supposed to be visible publicly. Only the owner and admin can do anything on it. All other users are denied. On the other hand any users or unauth user can create a question and give ownership to an other user. (send a question)Jeramyjerba
Think of it this way... A resource has an ACL describing who should access it. That access list shouldn't change based on who is trying to access it, right? Whoever is trying to access it is then compared to the ACL and either allowed or not later on by Pyramid. You are effectively doing an authorization yourself in the resource which is redundant.Anaerobe
so you mean that instead of adding self.uid I should add: question.ownerJeramyjerba
Yes I'm effectively saying that you probably shouldn't be setting self.uid = authenticated_userid(request) in your root factory, and the "if str(self.question.get('owner')) == self.uid:" is not necessary, your ACL should contain the entries for question.owner always for that Question.Anaerobe
Yeah I agree testing ownership isn't needed as the ressource already knows who can access it. But the self.uid is needed as the factory is going to be my context. I might strecth word definition but context to me is a request context and not a ressource context. It makes sense to put the uid there. I don't know where else would fit to be honest but certainly not the request object.Jeramyjerba
Well the uid represents the user initiating the current "request" to your application. However you want to think about it is fine with me but that's definitely not how Pyramid defines a resource or a context. Pyramid would call the "request" object the thing that tells you about the current request.Anaerobe
let us continue this discussion in chatJeramyjerba

© 2022 - 2024 — McMap. All rights reserved.