Skip to content

Instantly share code, notes, and snippets.

@mcdonc
Created April 28, 2015 23:08
Show Gist options
  • Save mcdonc/15096411bf36a71718f7 to your computer and use it in GitHub Desktop.
Save mcdonc/15096411bf36a71718f7 to your computer and use it in GitHub Desktop.
example of security plus interface-based view lookup
from wsgiref.simple_server import make_server
from zope.interface import (
Interface,
alsoProvides,
)
from pyramid.config import Configurator
from pyramid.httpexceptions import HTTPFound
from pyramid.response import Response
from pyramid.security import Allow, remember, forget
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.view import (
view_config,
forbidden_view_config,
)
class ICase(Interface): pass
class IFooCase(ICase): pass
class IBarCase(ICase): pass
cases = {
'1':{'name':'Fred', 'type':'foo'},
'2':{'name':'Jane', 'type':'bar'},
}
type_to_interface = {
'foo':IFooCase,
'bar':IBarCase,
}
users = {
'allcases':{
'name':'All Cases',
'groups':('g:foocase_viewer', 'g:barcase_viewer'),
},
'foocases':{
'name':'Foo Cases',
'groups':('g:foocase_viewer', ),
}
}
def groupfinder(userid, request):
user = users.get(userid)
if user is not None:
return user['groups']
class CaseContext(object):
def __init__(self, request):
case_id = request.matchdict['id']
self.case = cases[case_id]
self.case_type = self.case['type']
iface = type_to_interface[self.case_type]
alsoProvides(self, iface) # either IFooCase or IBarCase
def __acl__(self):
return [
(Allow, 'g:%scase_viewer' % self.case_type, 'view')
]
@view_config(
route_name='root',
)
def root(request):
body = '''
<html>
<head>
</head>
<body>
<p> Logged in as: %s </p>
<p> Intent:
<ul>
<li>A user who is not logged in will be shown the forbidden page for both /cases/1 and /cases/2 </li>
<li>Someone logged in as 'foocases' will be be shown the show_foo_case view when they visit /cases/1 (a Foo case), but will be shown the forbidden_case view when they visit /cases/2 (a Bar case) </li>
<li>Someone logged in as 'allcases' will be be shown the show_foo_case view when they visit /cases/1 (a Foo case), they will be shown the show_bar_case view when they visit the /cases/2 (a Bar case) </li>
</ul>
</p>
<ul>
<li> <a href="/login?userid=foocases">Log In As foocases</a> </li>
<li> <a href="/login?userid=allcases">Log In As allcases</a> </li>
<li> <a href="/logout">Log Out</a> </li>
</ul>
<ul>
<li> <a href="/cases/1">/cases/1 (a Foo case)</a> </li>
<li> <a href="/cases/2">/cases/2 (a Bar case)</a> </li>
</ul>
</body>
</html>''' % request.authenticated_userid
return Response(
content_type='text/html',
body = body,
)
@view_config(
route_name='show_case',
context=IFooCase,
permission='view',
renderer='string'
)
def show_foo_case(context, request):
return 'show_foo_case: This foo case is named %(name)s' % context.case
@view_config(
route_name='show_case',
context=IBarCase,
permission='view',
renderer='string'
)
def show_bar_case(context, request):
return 'show_bar_case: This bar case is named %(name)s' % context.case
@forbidden_view_config(
renderer='string',
route_name='show_case',
)
def forbidden_case(request):
# cant use ``context=`` in the view configuration here, as the context
# will be an HTTPForbidden exception object when we get here. However,
# request.context will be the original CaseContext instance, so we
# can perform some sort of dispatch on that as necessary.
return ('forbidden_case: You are forbidden to see the case of '
'type %(type)s named %(name)s' % request.context.case)
@view_config(
route_name='login',
)
def login_view(request):
userid = request.GET.get('userid')
user = users.get(userid)
if user is not None:
headers = remember(request, userid)
return HTTPFound(
location=request.route_url('root'),
headers=headers
)
return HTTPFound(location=request.route_url('login'))
@view_config(
route_name='logout',
)
def logout_view(request):
headers = forget(request)
return HTTPFound(location=request.route_url('root'), headers=headers)
if __name__ == '__main__':
authz_policy = ACLAuthorizationPolicy()
authn_policy = AuthTktAuthenticationPolicy(
'sosecret',
callback=groupfinder
)
config = Configurator(
authentication_policy=authn_policy,
authorization_policy=authz_policy,
)
config.add_route('root', '/')
config.add_route('login', '/login')
config.add_route('logout', '/logout')
config.add_route('show_case', '/cases/{id}', factory=CaseContext)
config.scan()
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever()
@gcarothers
Copy link

How would a generic forbidden no_login at all interact with the specific forbidden_view_config?

@gcarothers
Copy link

contains a pyramid app that will listen on 8080 if you run it with any python interpreter that has pyramid installed

connect on http://localhost:8080/

@mcdonc
Copy link
Author

mcdonc commented Apr 29, 2015

The forbidden view configured in the above app only applies to forbidden errors generated when the route_name is "show_case", the other generic one you already have will fire when that is not true.

@gcarothers
Copy link

Ugh, so yeah if a non signed in user goes back to a one of those pages they would see the you should buy page... and not get redirected back to the login page with the correct target_url set
Current forbidden_view

@view_config(context=HTTPForbidden,
             permission=NO_PERMISSION_REQUIRED,
             xhr=False)
def forbidden_view(request):
    if Authenticated not in effective_principals(request) or not request.user:
        request.session['target_url'] = request.url
        return HTTPFound(request.route_url('login'))
    if 'group:confirmed' not in effective_principals(request):
        with transaction.manager:
            send_confirmation_email(request=request, user=request.user,
                                    login_attempt=True)
        error_msg = '<strong>Unable to Sign in</strong>: Must confirm email address \
        and accept Terms of Use to log in.<br />Your confirmation email has \
        been re-sent.'
        request.session.flash(error_msg, 'error', allow_duplicate=False)
        request.response.headerlist.extend(
            forget(request),
        )
        return HTTPFound(request.route_url('login'))
    if 'group:confirmed' in effective_principals(request):
        # Attempt to access page available only to logged out users.
        return HTTPFound(request.route_url('landing'))

@npilon
Copy link

npilon commented Apr 29, 2015

Isn't our permissions case actually pretty specific here? Like @k9 pointed out, it's more like we're manifesting different contexts based on the user's package, and then rendering a different template based on that context. Which feels kind of like...

@view_config(
    route_name='show_case',
    context=IPermittedCase,
    permission='view',
    renderer='fullcase.mako'
    )
@view_config(
    route_name='show_case',
    context=IBannedCase,
    permission='view',
    renderer='upsellcase.mako'
    )
def show_case(context, request):
    return {}  # Maybe including some specific info depending on context?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment