21: Protecting Resources With Authorization¶
Assign security statements to resources describing the permissions required to perform an operation.
Background¶
Our application has URLs that allow people to add/edit/delete content via a web
browser. Time to add security to the application. Let's protect our add/edit
views to require a login (username of editor
and password of editor
).
We will allow the other views to continue working without a password.
Objectives¶
- Introduce the Pyramid concepts of authentication, authorization, permissions, and access control lists (ACLs).
- Make a root factory that returns an instance of our class for the top of the application.
- Assign security statements to our root resource.
- Add a permissions predicate on a view.
- Provide a Forbidden view to handle visiting a URL without adequate permissions.
Steps¶
We are going to use the authentication step as our starting point:
$ cd ..; cp -r authentication authorization; cd authorization $ $VENV/bin/python setup.py develop
Start by changing
authorization/tutorial/__init__.py
to specify a root factory to the configurator:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy from pyramid.config import Configurator from .security import groupfinder def main(global_config, **settings): config = Configurator(settings=settings, root_factory='.resources.Root') config.include('pyramid_chameleon') # Security policies authn_policy = AuthTktAuthenticationPolicy( settings['tutorial.secret'], callback=groupfinder, hashalg='sha512') authz_policy = ACLAuthorizationPolicy() config.set_authentication_policy(authn_policy) config.set_authorization_policy(authz_policy) config.add_route('home', '/') config.add_route('hello', '/howdy') config.add_route('login', '/login') config.add_route('logout', '/logout') config.scan('.views') return config.make_wsgi_app()
That means we need to implement
authorization/tutorial/resources.py
:1 2 3 4 5 6 7 8 9
from pyramid.security import Allow, Everyone class Root(object): __acl__ = [(Allow, Everyone, 'view'), (Allow, 'group:editors', 'edit')] def __init__(self, request): pass
Change
authorization/tutorial/views.py
to require theedit
permission on thehello
view and implement the forbidden view:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
from pyramid.httpexceptions import HTTPFound from pyramid.security import ( remember, forget, ) from pyramid.view import ( view_config, view_defaults, forbidden_view_config ) from .security import ( USERS, check_password ) @view_defaults(renderer='home.pt') class TutorialViews: def __init__(self, request): self.request = request self.logged_in = request.authenticated_userid @view_config(route_name='home') def home(self): return {'name': 'Home View'} @view_config(route_name='hello', permission='edit') def hello(self): return {'name': 'Hello View'} @view_config(route_name='login', renderer='login.pt') @forbidden_view_config(renderer='login.pt') def login(self): request = self.request login_url = request.route_url('login') referrer = request.url if referrer == login_url: referrer = '/' # never use login form itself as came_from came_from = request.params.get('came_from', referrer) message = '' login = '' password = '' if 'form.submitted' in request.params: login = request.params['login'] password = request.params['password'] if check_password(password, USERS.get(login)): headers = remember(request, login) return HTTPFound(location=came_from, headers=headers) message = 'Failed login' return dict( name='Login', message=message, url=request.application_url + '/login', came_from=came_from, login=login, password=password, ) @view_config(route_name='logout') def logout(self): request = self.request headers = forget(request) url = request.route_url('home') return HTTPFound(location=url, headers=headers)
Run your Pyramid application with:
$ $VENV/bin/pserve development.ini --reload
Open http://localhost:6543/ in a browser.
If you are still logged in, click the "Log Out" link.
Visit http://localhost:6543/howdy in a browser. You should be asked to login.
Analysis¶
This simple tutorial step can be boiled down to the following:
- A view can require a permission (
edit
). - The context for our view (the
Root
) has an access control list (ACL). - This ACL says that the
edit
permission is available onRoot
to thegroup:editors
principal. - The registered
groupfinder
answers whether a particular user (editor
) has a particular group (group:editors
).
In summary, hello
wants edit
permission, Root
says
group:editors
has edit
permission.
Of course, this only applies on Root
. Some other part of the site (a.k.a.
context) might have a different ACL.
If you are not logged in and visit /howdy
, you need to get shown the login
screen. How does Pyramid know what is the login page to use? We explicitly told
Pyramid that the login
view should be used by decorating the view with
@forbidden_view_config
.
Extra credit¶
- Do I have to put a
renderer
in my@forbidden_view_config
decorator? - Perhaps you would like the experience of not having enough permissions (forbidden) to be richer. How could you change this?
- Perhaps we want to store security statements in a database and allow editing via a browser. How might this be done?
- What if we want different security statements on different kinds of objects? Or on the same kinds of objects, but in different parts of a URL hierarchy?