[TurboGears] Need Expert's Help Trying to Untangle Pylons + Repoze + TG

17 views
Skip to first unread message

robneville73

unread,
Apr 27, 2010, 9:48:32 AM4/27/10
to TurboGears
I'm working on a TG 2.0 project ATM where we are trying to support
both a standard HTML-based UI and an API to a flash client via JSON.
So far things are going well, but I'm frankly struggling with
authentication.

Here's the situation. I want to be able to put a repoze predicate on a
controller method as normal and have the behavior differ depending on
where the request came from. Specifically, I don't want the server to
return a redirect 302 when the request has come from my API client and
the session isn't authenticated, instead, I want the unmolested 401 to
be returned. Conversely, when the request comes from a browser, I do
want the default repoze challenger mechanism to redirect me to the
login form. I'm running into a brick wall trying to understand the
various project documentations and browsing the source code of those
projects to understand how I might do that.

For other reasons, I've already written some WSGI middleware to
distinguish an API request from a normal one, so I have a way of
detecting that in the WSGI environment if that helps. Additionally, I
have figured out that adding:
basic_auth = BasicAuthPlugin('myapp')
base_config.sa_auth.identifiers = [('basic_auth',basic_auth)]
allows me to send HTTP authentication info in headers coming from the
API requests and it appears to be authenticating me.

It seems to me that the answer either lies in deciphering repoze's
request classifier mechanism for which I've found no good examples of
it's use for my purposes, or in perhaps creating my own decorator
(i.e. something similar to @require) that I use on wrapped controller
methods that only the API client uses - although this solution is less
than ideal.

I have a feeling that there's an already existing, elegant solution to
this but I'm not finding it. HELP PLEASE!

Thanks!
-Rob

--
You received this message because you are subscribed to the Google Groups "TurboGears" group.
To post to this group, send email to turbo...@googlegroups.com.
To unsubscribe from this group, send email to turbogears+...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/turbogears?hl=en.

Diez B. Roggisch

unread,
Apr 27, 2010, 10:33:39 AM4/27/10
to turbo...@googlegroups.com
It's not released, but we use it in producetion - my replacement for
the whole repoze.w* stack. Because I couldn't work with it.

http://bitbucket.org/deets/tgextsimpleauthorization

The sole reason it's not released is the lack of docs. I started, but
never finished.

If you want to give it a shot, I'm happy to help. Maybe that gives me
the push to finally release it.



Diez

robneville73

unread,
Apr 27, 2010, 11:00:16 AM4/27/10
to TurboGears
Thanks Diez. I'll take a longer look at it but at first glance, it
looks really good. My only question would be how to make it play nice
with the existing repoze + TG integration so that HTML UI predicates
behave as normal....in my 1 minute glance at your code it looks as
though you're stripping-out the existing repoze stuff completely when
building up the WSGI app stack, no?

If my assessment is correct, any ideas how one might go down that
path?

Diez B. Roggisch

unread,
Apr 27, 2010, 11:54:17 AM4/27/10
to turbo...@googlegroups.com
On Tuesday 27 April 2010 17:00:16 robneville73 wrote:
> Thanks Diez. I'll take a longer look at it but at first glance, it
> looks really good. My only question would be how to make it play nice
> with the existing repoze + TG integration so that HTML UI predicates
> behave as normal....in my 1 minute glance at your code it looks as
> though you're stripping-out the existing repoze stuff completely when
> building up the WSGI app stack, no?
>
> If my assessment is correct, any ideas how one might go down that
> path?


It's created in a way that it has the same predicates as repoze.what, so you
should have no problems except from re-adjusting imports.

robneville73

unread,
Apr 28, 2010, 7:06:46 AM4/28/10
to TurboGears
Right, so I can use the same predicates, but my main problem is, can
it handle....

@require(foo)
def mycontrollermethod(self):
somestuff

an unauthenticated call to mycontrollermethod from a browser that
results in a redirect to the HTML login form
AND
an unauthenticated call to mycontrollermethod from a Flex/Flash
application that results in no redirection, but instead returns an
HTTP 401 so that the Flex/Flash app can deal with it?

It appeared to me that it could handle scenario #2 OK, but not both at
the same time...but I hope I'm wrong because that would be cool :) As
I said before, in the WSGI stack, I've already added middleware to
insert an flag in the environment that distinguishes one from the
other but I'm unclear how to translate that into the behavior I'm
looking for.

Diez B. Roggisch

unread,
Apr 28, 2010, 7:59:44 AM4/28/10
to turbo...@googlegroups.com
On Wednesday 28 April 2010 13:06:46 robneville73 wrote:
> Right, so I can use the same predicates, but my main problem is, can
> it handle....
>
> @require(foo)
> def mycontrollermethod(self):
> somestuff
>
> an unauthenticated call to mycontrollermethod from a browser that
> results in a redirect to the HTML login form
> AND
> an unauthenticated call to mycontrollermethod from a Flex/Flash
> application that results in no redirection, but instead returns an
> HTTP 401 so that the Flex/Flash app can deal with it?
>
> It appeared to me that it could handle scenario #2 OK, but not both at
> the same time...but I hope I'm wrong because that would be cool :) As
> I said before, in the WSGI stack, I've already added middleware to
> insert an flag in the environment that distinguishes one from the
> other but I'm unclear how to translate that into the behavior I'm
> looking for.

Of course it can - it's part of the denial-handler. You can write your own
that does - depending on environ-state - does redirects or returns an status
code.

Just make your custom denial handler either perform a normal redirect (that's
what it usually does, so take a look at the default implementation), or
perform an "internal redirect" to a location that renders the 401.

robneville73

unread,
Apr 28, 2010, 8:06:49 AM4/28/10
to TurboGears
Sweet! Thanks Diez. I'll work with that this week and let you know how
it goes.

Gustavo Narea

unread,
Apr 28, 2010, 3:54:47 PM4/28/10
to TurboGears, robertn...@gmail.com
Hello,

This kind of scenarios are perfectly supported by repoze.who and it's
not hard to get it working:

You basically need to let repoze.who know which plugins should come
into play when the client is a browser, and which plugins should come
into play when it's a flash client. You'd also need to let it know
what a flash client looks like.

So you want something like this:
- Browser:
- User is identified by the friendlyform plugin (TG2 default).
- User is challenged by friendlyform (TG2 default).
- Flash client:
- User is identified by HTTP basic authentication.
- User is challenged by HTTP basic authentication.

You already have the browser part, so you need to add the flash part.
In TG2, you can do it like this:
# ===== yourapp.somewhere
from repoze.who.classifiers import default_request_classifier
from repoze.who.interfaces import IChallenger, IIdentifier
from repoze.who.plugins.basicauth import BasicAuthPlugin

def my_custom_classifier(environ):
if environ says the UA is a Flash client:
return "flash"
return default_request_classifier(environ)

FLASH_AUTHN_PLUGIN = BasicAuthPlugin("Rob's secret for Flash
clients")
# Optional:
FLASH_AUTHN_PLUGIN.classifications = {
IIdentifier: ["flash"],
IChallenger: ["flash"],
}

# ===== yourapp.config.app_cfg
from yourapp.somewhere import my_custom_classifier,
FLASH_AUTHN_PLUGIN
base_config.sa_auth.request_classifier = my_custom_classifier
base_config.sa_auth.identifiers = [FLASH_AUTHN_PLUGIN]
base_config.sa_auth.challengers = [FLASH_AUTHN_PLUGIN]

That's it.

HTH,

- Gustavo.

PS: I've just released a new version of repoze.who-friendlyform just
to make it come into play when the client is a browser. This way it
won't get in your way:
http://code.gustavonarea.net/repoze.who-friendlyform/News.html#version-1-0-6-2010-04-28

Robert Neville

unread,
Apr 28, 2010, 10:32:46 PM4/28/10
to Gustavo Narea, TurboGears
I wondered about using repoze classifier's, thanks for the example. This looks very close to what I envisioned the answer to be....I think I'm still confused though how this works....sorry I'm being dense I guess but I have had a heck of time understanding the documentation and source code for this for some reason...(its me though, not the fault of the documentation)

By setting :
   base_config.sa_auth.request_classifier = my_custom_classifier
   base_config.sa_auth.identifiers = [FLASH_AUTHN_PLUGIN]
   base_config.sa_auth.challengers = [FLASH_AUTHN_PLUGIN]

Am I not overwriting the default behavior that way? How does repoze know that requests classified as "flash" use one challenger and should otherwise use something else? Since it looks like identifiers is an array, doesn't there need to be an entry for the default set of identifiers and challengers or are those baked in somehow, i.e. something like:
   base_config.sa_auth.challengers = [FLASH_AUTHN_PLUGIN, DEFAULT_THINGGY]
or something like that?

Again, thank you for your patience with my thick-headedness on this...

Gustavo Narea

unread,
Apr 29, 2010, 4:54:57 PM4/29/10
to Robert Neville, TurboGears
Hello, Robert!

On 29/04/10 03:32, Robert Neville wrote:
> base_config.sa_auth.request_classifier = my_custom_classifier
> base_config.sa_auth.identifiers = [FLASH_AUTHN_PLUGIN]
> base_config.sa_auth.challengers = [FLASH_AUTHN_PLUGIN]
>
> Am I not overwriting the default behavior that way? How does repoze
> know that requests classified as "flash" use one challenger and should
> otherwise use something else? Since it looks like identifiers is an
> array, doesn't there need to be an entry for the default set of
> identifiers and challengers or are those baked in somehow, i.e.
> something like:
> base_config.sa_auth.challengers = [FLASH_AUTHN_PLUGIN, DEFAULT_THINGGY]
> or something like that?
>

TG2 uses repoze.what-quickstart [1], which is a plugin that sets up
repoze.who and repoze.what in one go. It configures the repoze.who
FriendlyFormPlugin [2] as the first challenger and first identifier, so
there's no need to add it explicitly in TG2. Then, any identifiers or
challengers set in app_cfg.py will be appended.

FriendlyFormPlugin advertises itself as a browser-specific plugin, so
you don't have to say that it's a plugin for browsers.

That way, you're not changing anything in the default behavior. You're
just adding support for Flash clients. People using browsers won't
notice any difference.

HTH,

[1] http://code.gustavonarea.net/repoze.what-quickstart/
[2] http://code.gustavonarea.net/repoze.who-friendlyform/

--
Gustavo Narea <xri://=Gustavo>.

robneville73

unread,
May 4, 2010, 8:13:23 AM5/4/10
to TurboGears
Gustavo, I finally got back to this but I'm having some trouble that
perhaps you could shed some light on?

So, this is what I've done as per your instructions:

in file <myapp.config.repoze_cfg.py>:
from repoze.who.classifiers import default_request_classifier
from repoze.who.interfaces import IChallenger, IIdentifier
from repoze.who.plugins.basicauth import BasicAuthPlugin

def api_identifier(environ):
if environ.get('myapp.flex_detected',False): #<-- I have WSGI
middleware defined that is setting that flag.
return "api"
return default_request_classifier(environ)

API_AUTH_PLUGIN = BasicAuthPlugin('myapp')

API_AUTH_PLUGIN.classifications = {
IIdentifier: ["api"],
IChallenger: ["api"],
}

in file <myapp.config.app_cfg.py>:
from myapp.config.repoze_cfg import api_identifier, API_AUTH_PLUGIN
...
#enable authentication via http Basic Auth
base_config.sa_auth.classifier = api_identifier
base_config.sa_auth.identifiers = [API_AUTH_PLUGIN.classifications]
#<-- trouble here...
base_config.sa_auth.challengers = [API_AUTH_PLUGIN.classifications]
#<-- trouble here...

At first, I tried setting base_config.sa_auth.identifiers as you had
outlined below with:

base_config.sa_auth.identifiers = [API_AUTH_PLUGIN]

but upon starting up TG, I would get:

"TypeError: 'BasicAuthPlugin' object is not iterable"

which was failing at repoze.who.middleware.py in the make_registries
function. So, I then tried changing it as I've outlined above thinking
that's what the "for name, value in supplied" loop was actually
looking for...when I do that, I now get the following exception
thrown....

repoze/who/middleware.py", line 416, in make_registries
raise ValueError(str(name) + ': ' + why)
ValueError: <InterfaceClass repoze.who.interfaces.IIdentifier>: An
object has failed to implement interface <InterfaceClass
repoze.who.interfaces.IIdentifier>

The identify attribute was not provided.

When I looked at repoze.who.interfaces at the IIdentifier interface,
it looked to me like you might need to specify an identify, remember
and forget function for it to pass this test. Am I on the right track?
If so, I'm not clear on what I'm supposed to be doing in those
functions.

In your example, you marked the section that said:
API_AUTH_PLUGIN.classifications = {
IIdentifier: ["api"],
IChallenger: ["api"],
}
as optional. Why is that optional? The reason I ask is that if I
comment out like this:

base_config.sa_auth.classifier = api_identifier
#base_config.sa_auth.identifiers = [API_AUTH_PLUGIN.classifications]
#base_config.sa_auth.challengers = [API_AUTH_PLUGIN.classifications]

then the application starts up with no errors, but I'm not sure that
I'm going to get the desired effect or if I'm missing something vital
by doing that. Can you elaborate on what the
API_AUTH_PLUGIN.classifications is trying to accomplish or point me to
relevant documentation?

Thanks!

robneville73

unread,
May 4, 2010, 8:22:23 AM5/4/10
to TurboGears
Oh, one more thing, you're original post said to do:
base_config.sa_auth.request_classifier = my_custom_classifier

but that complained that request_classifier wasn't an attribute of
sa_auth. Upon inspection, it looked like the right value instead was:
base_config.sa_auth.classifier = my_custom_classifier

I thought I would include that for completeness in case someone else
finds this thread later with the same issue...

robneville73

unread,
May 6, 2010, 8:54:57 AM5/6/10
to TurboGears
Okay, I think I got this working finally. Gustavo, thanks a ton for
putting me on the right path. We needed to run through the debugger a
few times to see what we were missing but you're example was very very
close to what we needed. For the benefit of the group, here's what now
appears to be working:

in file <myapp.config.repoze_cfg.py>, the eventual version I ended up
with was this:
http://pastebin.com/wQ9BnBcQ

in my app_cfg.py file, I ended up with this:
http://pastebin.com/j4CSWG5V

The key difference between what I originally had and now was the
section in the app_cfg that now says:
base_config.sa_auth.identifiers =
[('mynametoregisteras',FLASH_AUTHN_PLUGIN)]
base_config.sa_auth.challengers =
[('mynametoregisteras',FLASH_AUTHN_PLUGIN)]

and not:
base_config.sa_auth.identifiers = [FLASH_AUTHN_PLUGIN]
base_config.sa_auth.challengers = [FLASH_AUTHN_PLUGIN]

Gustavo Narea

unread,
May 7, 2010, 3:05:03 PM5/7/10
to TurboGears
Hello, Robert.

I'm glad to hear it's working now! It was a pleasure to help you
out :)

- Gustavo.
Reply all
Reply to author
Forward
0 new messages