plone / plone.formwidget.autocomplete

z3c.form widget for Plone using jQuery Autocomplete
https://pypi.org/project/plone.formwidget.autocomplete
2 stars 12 forks source link

User security context is absent from AJAX call #15

Closed davisd50 closed 8 years ago

davisd50 commented 8 years ago

When utilizing the JavaScript AJAX call to a vocabulary that is sensitive to a logged in user's permissions (i.e. a vocabulary whose terms are based on catalog searches to ACL-restricted content), the AJAX call fails due to LookupError (my code). When researching the issue, I found that the request context is the Anonymous user instead of the user that is actually logged in.

The widget works fine (as expected) when not using JavaScript. Here's the URL that is causing the issue

http://localhost:8080/Plone/people/users/aliceheimbach/@@edit/++widget++form.widgets.business/@@autocomplete-search?q=cof&limit=10&timestamp=1448484747073

Why, when going to this view, would my logged in user become 'Anonymous' instead of what they actually are authenticated as?

davisagli commented 8 years ago

Your code must be running during traversal; the security manager is activated with the correct user after traversal is complete.

Maybe you are accessing the vocabulary in the autocomplete-search view's __init__ method? If so maybe you can change it to happen in the __call__ method instead.

davisagli commented 8 years ago

Oops, I missed that that view comes from this package, and it's clearly already doing things in __call__. Can you provide a traceback for the LookupError?

davisd50 commented 8 years ago

Here's the traceback...

/Users/ddavis/git/b2bserver/eggs/Zope2-2.13.22-py2.7.egg/ZServer/PubCore/ZServerPublisher.py(31)init() -> response=b) /Users/ddavis/git/b2bserver/eggs/Zope2-2.13.22-py2.7.egg/ZPublisher/Publish.py(455)publish_module() -> environ, debug, request, response) /Users/ddavis/git/b2bserver/eggs/Zope2-2.13.22-py2.7.egg/ZPublisher/Publish.py(249)publish_module_standard() -> response = publish(request, module_name, after_list, debug=debug)

(7)_patched_publish() (59)publish() /Users/ddavis/git/b2bserver/eggs/Zope2-2.13.22-py2.7.egg/ZPublisher/BaseRequest.py(502)traverse() -> subobject = self.traverseName(object, entry_name) /Users/ddavis/git/b2bserver/eggs/Zope2-2.13.22-py2.7.egg/ZPublisher/BaseRequest.py(326)traverseName() -> ob2 = namespaceLookup(ns, nm, ob, self) /Users/ddavis/git/b2bserver/eggs/zope.traversing-3.13.2-py2.7.egg/zope/traversing/namespace.py(112)namespaceLookup() -> return traverser.traverse(name, ()) /Users/ddavis/git/b2bserver/eggs/plone.z3cform-0.8.0-py2.7.egg/plone/z3cform/traversal.py(60)traverse() -> form.update() /Users/ddavis/git/b2bserver/eggs/plone.dexterity-2.2.4-py2.7.egg/plone/dexterity/browser/edit.py(62)update() -> super(DefaultEditForm, self).update() /Users/ddavis/git/b2bserver/eggs/plone.z3cform-0.8.0-py2.7.egg/plone/z3cform/fieldsets/extensible.py(59)update() -> super(ExtensibleForm, self).update() /Users/ddavis/git/b2bserver/eggs/plone.z3cform-0.8.0-py2.7.egg/plone/z3cform/patch.py(30)GroupForm_update() -> _original_GroupForm_update(self) /Users/ddavis/git/b2bserver/eggs/z3c.form-3.2.3-py2.7.egg/z3c/form/group.py(132)update() -> self.updateWidgets() /Users/ddavis/git/b2bserver/eggs/z3c.form-3.2.3-py2.7.egg/z3c/form/form.py(135)updateWidgets() -> self.widgets.update() /Users/ddavis/git/b2bserver/eggs/z3c.form-3.2.3-py2.7.egg/z3c/form/field.py(277)update() -> widget.update() /Users/ddavis/git/b2bserver/eggs/z3c.formwidget.query-0.11-py2.7.egg/z3c/formwidget/query/widget.py(162)update() -> terms.append(source.getTerm(value)) > /Users/ddavis/git/b2bserver/src/b2b.collaboration/b2b/collaboration/registrations/registry/vocabularies/registrations.py(83)getTerm() > -> raise LookupError("expected to find term related to value {} providing at least one of {}".format(value, self.b2b_principal_types)) when debugging the issue (via Python debugger), I found that a call to plone.api.user.get_current() returns the 'Anonymous' user instead of the expected 'admin' user I was logged in as. This is with Plone 4.3.6.
davisd50 commented 8 years ago

Seems like PAS is not getting it's hook here. take a look at this debug session....

/Users/ddavis/git/b2bserver/src/b2b.collaboration/b2b/collaboration/registrations/registry/vocabularies/registrations.py(83)getTerm() -> raise LookupError("expected to find term related to value {} providing at least one of {}".format(value, self.b2b_principal_types)) (Pdb) from plone import api (Pdb) api.user.get_current() <SpecialUser 'Anonymous User'> (Pdb) from plone import api (Pdb) portal = api.portal.get() (Pdb) request = portal.REQUEST (Pdb) portal.acl_users.credentials_cookie_auth.extractCredentials(request) {'remote_address': '127.0.0.1', 'remote_host': '', 'password': 'admin', 'login': 'admin'}

It seems the session cookie is valid...it just hasn't been processed into this view call.

davisagli commented 8 years ago

Yeah, your problem is that the vocabulary is being used during traversal, which happens before the security manager is activated (because knowing which user folder to use depends on knowing where the traversed object is in the site hierarchy).

I'm not sure what you should do to solve or work around this. You probably need to find a way to delay the form's 'update' method from being called until the form view is called. But, 'update' is what constructs widgets, so it's not possible to traverse to widgets until it has been called.

davisd50 commented 8 years ago

Thanks for the help. For reference, I ended up using the following code to find the currently logged-in user. It's a bit hackish, but I think it'll handle my use-cases.

class QueryablePrincipalRegistrationSource(object):
    """A source that can be queried by the autocomplete widget"""
    # ...I left out most of the implementation for brevity's sake
    def _returnVerifiedUser(self):
        """Return the current request user based on cookie credentials
        """
        if api.user.is_anonymous():
            portal = api.portal.get()
            app = portal.__parent__
            request = portal.REQUEST
            creds = portal.acl_users.credentials_cookie_auth.extractCredentials(request)
            user = None
            if 'login' in creds and creds['login']:
                # first try the portal (non-admin accounts)
                user = portal.acl_users.authenticate(creds['login'], creds['password'], request)
                if not user:
                    # now try the app (i.e. the admin account)
                    user = app.acl_users.authenticate(creds['login'], creds['password'], request)
            return user
        else:
            return api.user.get_current()

    def search(self, query_string):
        if query_string:
            with api.env.adopt_user(user = self._returnVerifiedUser()):
                # ...do catalog search and return results based on cookied user creds