Correct way to protect flask-sentinel oauth management - python

flask-sentinel creates a route to a management UI using app.add_url_rule. Wanting to apply some access control rules I wrapped it like this:
from flask.ext import sentinel
def requires_auth(fn):
#wraps(fn)
def decorated(*args, **kwargs):
roles = []
if 'user' in session and 'roles' in session['user']:
roles = session['user']['roles']
if 'admin' not in roles:
flash("Not enough power")
return redirect('/')
return fn(*args, **kwargs)
return decorated
sentinel.views.management = requires_auth(sentinel.views.management)
sentinel.ResourceOwnerPasswordCredentials(app)
The question is, is this really the way or is there some more appropriate way?
EDIT:
Realized that my question was rather abstract and really just about flask instead of sentinel. Guess what I meant to ask was: "is there any more declarative way to apply security restrictions to paths in Flask, instead of wrapping each registered route one by one?"
After a bit of study, this seems to provide the flexible security control I was looking for.
from flask import Flask, request
app = Flask('bla')
PATH_ROLES = {
'/admin/.*': ['admin']
}
#app.before_request
def before_request():
try:
rule = next(x for x in PATH_ROLES if re.match(x, request.path))
print "path requires: ", PATH_ROLES[rule]
except StopIteration: pass

At quick glance I'd say that your approach is sound. You are essentially applying the common Flask pattern of wrapping a view with a decorator.

Related

control order that routes are matched when using a custom directive/action

I have a catchall route that is catching my request before my call to add_route within an action created with a custom directive. How can I prevent that from happening? Ie. manually place the catchall route at the end of the route matching process or put my action's add_route at the front of the route matching process?
I tried swapping the order of the includes between admin/routes.py and routes.py but that didn't seem to have an affect. A quick solution (and probably good idea thinking about it now) is to filter admin from the pattern in the catchall. But I feel like this is going to come up again where I cannot do that so I'm asking this question.
__init__.py
def main(global_config, **settings):
#...
config.include('.directives')
#...
config.include(".routes")
#...
for k in (".customer", ".admin"):
config.scan(k) # this picks up the admin/admin_routes.py
return config.make_wsgi_app()
admin/routes.py
def includeme(config):
config.add_dispatched_route(
"admin-plants-edit",
"/admin/plants/{id}",
'plant',
)
routes.py
def includeme(config):
#...
config.add_route("page-view", "/*url")
directives.py
from pyramid.config import PHASE0_CONFIG
from pyramid.httpexceptions import HTTPNotFound
def includeme(config):
def add_dispatched_route(config, route_name, route_pattern, dispatch_with):
def route_factory(request):
api = request.find_service(name=dispatch_with)
obj = api.by_id(request.matchdict["id"])
if not obj:
raise HTTPNotFound()
return obj
def route_pregenerator(request, elements, kw):
api = request.find_service(name=dispatch_with)
try:
obj = kw.pop(api.get_pregenerator_kw())
except KeyError:
pass
else:
kw["id"] = obj.id
return elements, kw
def register():
config.add_route(route_name, route_pattern, factory=route_factory, pregenerator=route_pregenerator)
config.action(('dispatched_route', route_name), register, order=PHASE0_CONFIG)
config.add_directive('add_dispatched_route', add_dispatched_route)
The way the phases work in Pyramid doesn't allow you to re-order actions within a phase. Basically imagine that every call to config.add_route gets appended to a list, and then later that list is iterated in order to add the routes. You cannot inject a call to config.add_route before another call to it.
However if you wrap every call to add_route in your own action, then you have an opportunity to defer everything, order things, and then call config.add_route with each route in the order you wish.

Adding auth decorators to flask restx

I have a Flask application using flask-restx and flask-login. I would like all routes by default to require login, and explicitly define public routes that require no authentication. I have started using decorators following the example given in this question:
Best way to make Flask-Login's login_required the default
It works for function endpoints, but not for restx resource endpoints.
I have tried adding the function both as a decorator, and using the method_decorators field. For example:
def public_route(decorated_function):
"""
This is a decorator to specify public endpoints in our flask routes
:param decorated_function:
:return:
"""
decorated_function.is_public = True
return decorated_function
class HelloWorld(ConfigurableResource):
method_decorators = {"get": [public_route]}
#public_route
#api.doc('Welcome message')
def get(self):
return {'hello': 'world'}
And this test passes:
def test_hello_world_is_public():
api = Namespace('health', description='Health related operations')
hello = HelloWorld(api, config=None, logger=None)
is_public_endpoint = getattr(hello.get, 'is_public', False)
assert is_public_endpoint
My challenge is I can't see how to access this attribute in my auth logic:
#app.before_request
def check_route_access():
"""
This function decides whethere access should be granted to an endpoint.
This function runs before all requests.
:return:
"""
is_public_endpoint = getattr(app.view_functions[request.endpoint], 'is_public', False)
if person_authorized_for_path(current_user, request.path, is_public_endpoint):
return
# Otherwise access not granted
return redirect(url_for("auth.index"))
This works for plain function endpoints, but not restx resources.
I understand that restx is wrapping my resource class in a function so that flask can do dispatch, but I can't figure out how to access the decorator from here. So I have some questions:
Is it possible to reach the decorator from the view_function?
Is it possible to know whether the endpoint is a restx resource or a plain rest function?
Is there a better way to do what I'm trying to achieve?
Based on this and this, method_decorators variable should be a list of functions, so you should use it like:
def _perform_auth(method):
is_public_endpoint = getattr(method, 'is_public', False)
# place the validation here
class Resource(flask_restx.Resource):
method_decorators = [_perform_auth]
Is it possible to reach the decorator from the view_function?
Well... it is possible, but I wouldn't recommend it. Here's an example
Is it possible to know whether the endpoint is a restx resource or a
plain rest function?
You probably can inspect the func and figure out if it's from restx, maybe looking at __qualname__, but then again, I`d wouldn't recommend it.
Is there a better way to do what I'm trying to achieve?
I would go one of these solutions:
Explicitly decorate view_funcs and resources that do need authentication, instead of the other way around
Create a blueprint for public endpoints, a blueprint for protected endpoints with the before_request decorator for authorization

How to handle complex URL in a elegant way?

I'm writing a admin website which control several websites with same program and database schema but different content. The URL I designed like this:
http://example.com/site A list of all sites which under control
http://example.com/site/{id} A brief overview of select site with ID id
http://example.com/site/{id}/user User list of target site
http://example.com/site/{id}/item A list of items sold on target site
http://example.com/site/{id}/item/{iid} Item detailed information
# ...... something similar
As you can see, nearly all URL are need the site_id. And in almost all views, I have to do some common jobs like query Site model against database with the site_id. Also, I have to pass site_id whenever I invoke request.route_path.
So... is there anyway for me to make my life easier?
It might be useful for you to use a hybrid approach to get the site loaded.
def groupfinder(userid, request):
user = request.db.query(User).filter_by(id=userid).first()
if user is not None:
# somehow get the list of sites they are members
sites = user.allowed_sites
return ['site:%d' % s.id for s in sites]
class SiteFactory(object):
def __init__(self, request):
self.request = request
def __getitem__(self, key):
site = self.request.db.query(Site).filter_by(id=key).first()
if site is None:
raise KeyError
site.__parent__ = self
site.__name__ = key
site.__acl__ = [
(Allow, 'site:%d' % site.id, 'view'),
]
return site
We'll use the groupfinder to map users to principals. We've chosen here to only map them to the sites which they have a membership within. Our simple traversal only requires a root object. It updates the loaded site with an __acl__ that uses the same principals the groupfinder is setup to create.
You'll need to setup the request.db given patterns in the Pyramid Cookbook.
def site_pregenerator(request, elements, kw):
# request.route_url(route_name, *elements, **kw)
from pyramid.traversal import find_interface
# we use find_interface in case we improve our hybrid traversal process
# to take us deeper into the hierarchy, where Site might be context.__parent__
site = find_interface(request.context, Site)
if site is not None:
kw['site_id'] = site.id
return elements, kw
Pregenerator can find the site_id and add it to URLs for you automatically.
def add_site_route(config, name, pattern, **kw):
kw['traverse'] = '/{site_id}'
kw['factory'] = SiteFactory
kw['pregenerator'] = site_pregenerator
if pattern.startswith('/'):
pattern = pattern[1:]
config.add_route(name, '/site/{site_id}/' + pattern, **kw)
def main(global_conf, **settings):
config = Configurator(settings=settings)
authn_policy = AuthTktAuthenticationPolicy('seekrit', callback=groupfinder)
config.set_authentication_policy(authn_policy)
config.set_authorization_policy(ACLAuthorizationPolicy())
config.add_directive(add_site_route, 'add_site_route')
config.include(site_routes)
config.scan()
return config.make_wsgi_app()
def site_routes(config):
config.add_site_route('site_users', '/user')
config.add_site_route('site_items', '/items')
We setup our application here. We also moved the routes into an includable function which can allow us to more easily test the routes.
#view_config(route_name='site_users', permission='view')
def users_view(request):
site = request.context
Our views are then simplified. They are only invoked if the user has permission to access the site, and the site object is already loaded for us.
Hybrid Traversal
A custom directive add_site_route is added to enhance your config object with a wrapper around add_route which will automatically add traversal support to the route. When that route is matched, it will take the {site_id} placeholder from the route pattern and use that as your traversal path (/{site_id} is path we define based on how our traversal tree is structured).
Traversal happens on the path /{site_id} where the first step is finding the root of the tree (/). The route is setup to perform traversal using the SiteFactory as the root of the traversal path. This class is instantiated as the root, and the __getitem__ is invoked with the key which is the next segment in the path ({site_id}). We then find a site object matching that key and load it if possible. The site object is then updated with a __parent__ and __name__ to allow the find_interface to work. It is also enhanced with an __acl__ providing permissions mentioned later.
Pregenerator
Each route is updated with a pregenerator that attempts to find the instance of Site in the traversal hierarchy for a request. This could fail if the current request did not resolve to a site-based URL. The pregenerator then updates the keywords sent to route_url with the site id.
Authentication
The example shows how you can have an authentication policy which maps a user into principals indicating that this user is in the "site:" group. The site (request.context) is then updated to have an ACL saying that if site.id == 1 someone in the "site:1" group should have the "view" permission. The users_view is then updated to require the "view" permission. This will raise an HTTPForbidden exception if the user is denied access to the view. You can write an exception view to conditionally translate this into a 404 if you want.
The purpose of my answer is just to show how a hybrid approach can make your views a little nicer by handling common parts of a URL in the background. HTH.
For the views, you could use a class so that common jobs can be carried out in the __init__ method (docs):
from pyramid.view import view_config
class SiteView(object):
def __init__(self, request):
self.request = request
self.id = self.request.matchdict['id']
# Do any common jobs here
#view_config(route_name='site_overview')
def site_overview(self):
# ...
#view_config(route_name='site_users')
def site_users(self):
# ...
def route_site_url(self, name, **kw):
return self.request.route_url(name, id=self.id, **kw)
And you could use a route prefix to handle the URLs (docs). Not sure whether this would be helpful for your situation or not.
from pyramid.config import Configurator
def site_include(config):
config.add_route('site_overview', '')
config.add_route('site_users', '/user')
config.add_route('site_items', '/item')
# ...
def main(global_config, **settings):
config = Configurator()
config.include(site_include, route_prefix='/site/{id}')

What is the minimum amount of boilerplate for negotiating content in Django?

In a Django application, I have more than a handful of views which return JSON, with an invocation similar to:
return HttpResponse(json.dumps(content), mimetype="application/json")
I want to start creating views that return either HTML or JSON depending on the Accept headers from the request. Possibly other types, too, but those are the main ones. I also want to get multiple URLs routed to this view; the file extensions ".html" and ".json" help tell clients which types they should Accept when making their request, and I want to avoid the "?format=json" antipattern.
What's the correct, blessed way to do this in Django with a minimum of boilerplate or repeated code?
(Edit: Rephrase in order to better follow SO's community guidelines.)
I think a class-based view mixin (django 1.3+) is the easiest way to do this. All your views would inherit from a base class that contains logic to respond with the appropriate content.
I think I may not be seeing your big picture here but this is what I would do:
Have a html template that you render when html is requested and keep your json.dumps(content) for when json is requested. Seems to be obvious but I thought i should mention it anyway.
Set your URLs to send you "json" or 'html'. :
(r'^some/path/(?P<url_path>.*)\.(?P<extension>html|json)$', 'some.redirect.view'),
(r'^/(?P<extension>html|json)/AppName', include(MyApp)),
# etc etc
and your view:
def myRedirectView(request, url_path, extension):
view, args, kwargs = resolve("/" + extension + "/" + urlPath)
kwargs['request'] = request
return view(*args, **kwargs)
I know this is a bit vague because I haven't fully thought it through but its where I would start.
I have addressed this by creating a generic view class, based on Django's own generic.View class, that defines a decorator 'accept_types'. This modifies the view to which it is applied so that it returns None if the indicated content-type is not in the Accept header. Then, the get() method (which is called by the generic.View dispatcher) looks like this:
def get(self, request):
self.request = request # For clarity: generic.View does this anyway
resultdata = { 'result': data, etc. }
return (
self.render_uri_list(resultdata) or
self.render_html(resultdata) or
self.error(self.error406values())
)
The actual view renderers are decorated thus:
#ContentNegotiationView.accept_types(["text/uri-list"])
def render_uri_list(self, resultdata):
resp = HttpResponse(status=200, content_type="text/uri-list")
# use resp.write(...) to assemble rendered response body
return resp
#ContentNegotiationView.accept_types(["text/html", "application/html", "default_type"])
def render_html(self, resultdata):
template = loader.get_template('rovserver_home.html')
context = RequestContext(self.request, resultdata)
return HttpResponse(template.render(context))
The (one-off) generic view class that declares the decorator looks like this:
class ContentNegotiationView(generic.View):
"""
Generic view class with content negotiation decorators and generic error value methods
Note: generic.View dispatcher assigns HTTPRequest object to self.request.
"""
#staticmethod
def accept_types(types):
"""
Decorator to use associated function to render the indicated content types
"""
def decorator(func):
def guard(self, values):
accept_header = self.request.META.get('HTTP_ACCEPT',"default_type")
accept_types = [ a.split(';')[0].strip().lower()
for a in accept_header.split(',') ]
for t in types:
if t in accept_types:
return func(self, values)
return None
return guard
return decorator
(The parameter handling in the decorator should be generalized - this code works, but is still in development as I write this. The actual code is in GitHub at https://github.com/wf4ever/ro-manager/tree/develop/src/roverlay/rovweb/rovserver, but in due course should be separated to a separate package. HTH.)

Python+Pyramid+Mako: What is the difference between the context in event, context in view and context in template?

I've been trying hard to understand this, but can't quite put a finger on a precise documentation about it. I am quite confused about the different meaning of context in this Python Pyramid+Mako setup.
Here is some code snippets (tell me if you need more context):
class Root(object):
request = None
def __init__(self, request):
self.request = request
#events.subscriber(events.BeforeRender)
def add_renderer_globals(event):
event[u'c'] = event[u'request'].tmpl_context
print u"add_renderer_globals(): request.tmpl_context={0}".format(event[u'request'].tmpl_context)
print u"add_renderer_globals(): context={0}".format(event[u'context'])
#view.view_config(route_name='login', request_method='GET', renderer='login.mako')
def login_get(context, request):
print u"login_get(): context={0}".format(context)
return {}
[...]
cfg = config.Configurator(root_factory=Root,
package=MyPKG,
settings=settings,
session_factory=pyramid_beaker.session_factory_from_settings(settings),
)
cfg.add_route(name='login', pattern='/login')
cfg.scan()
and in my mako template, just to have an example, I only have:
Mako template context=${context}
So I would make a request and I get the following outputs from console or browser:
login_get(): context=<MyPKG.Root object at 0x1523c90>
add_renderer_globals(): request.tmpl_context=<pyramid.request.TemplateContext object at 0x12fbc50>
add_renderer_globals(): context=<MyPKG.Root object at 0x1523c90>
Mako template context=<mako.runtime.Context object at 0x15a4950>
My question is: What are the differences, and what do you use them for? I'm also confused why semantically, I declared root_factory=MyPKG.Root and it becomes context=MyPKG.Root in my view and my subscriber.
Thanks for any hint to help me understand.
First, ignore request.tmpl_context. This is just a dictionary on the request object that you can add stuff to and is not normally used in Pyramid applications at all. It's a step-child from the Pylons merge.
There are two context objects when using Mako. The first (mako.runtime.Context) is supplied by Mako: http://docs.makotemplates.org/en/latest/runtime.html#context
Pyramid typically exposes the traversal context (MyPKG.Root) as context in your templates. However, Mako already has a variable using that name. :-( Thus, Pyramid's context is actually named _context.

Categories