/authorization.rst
ReStructuredText | 280 lines | 215 code | 65 blank | 0 comment | 0 complexity | 703019131ff19d9e3037bd7322dc9168 MD5 | raw file
1.. _wiki2_adding_authorization: 2 3==================== 4Adding Authorization 5==================== 6 7Our application currently allows anyone with access to the server to 8view, edit, and add pages to our wiki. For purposes of demonstration 9we'll change our application to allow only people whom possess a 10specific username (`editor`) to add and edit wiki pages but we'll 11continue allowing anyone with access to the server to view pages. 12:mod:`repoze.bfg` provides facilities for *authorization* and 13*authentication*. We'll make use of both features to provide security 14to our application. 15 16The source code for this tutorial stage can be browsed at 17`docs.repoze.org 18<http://docs.repoze.org/bfgwiki2-1.3/authorization>`_. 19 20Adding A Root Factory 21--------------------- 22 23We're going to start to use a custom :term:`root factory` within our 24``run.py`` file. The objects generated by the root factory will be 25used as the :term:`context` of each request to our application. In 26order for :mod:`repoze.bfg` declarative security to work properly, the 27context object generated during a request must be decorated with 28security declarations; when we begin to use a custom root factory to 29generate our contexts, we can begin to make use of the declarative 30security features of :mod:`repoze.bfg`. 31 32Let's modify our ``run.py``, passing in a :term:`root factory` to our 33:term:`Configurator` constructor. We'll point it at a new class we 34create inside our ``models.py`` file. Add the following statements to 35your ``models.py`` file: 36 37.. code-block:: python 38 39 from repoze.bfg.security import Allow 40 from repoze.bfg.security import Everyone 41 42 class RootFactory(object): 43 __acl__ = [ (Allow, Everyone, 'view'), 44 (Allow, 'group:editors', 'edit') ] 45 def __init__(self, request): 46 self.__dict__.update(request.matchdict) 47 48The ``RootFactory`` class we've just added will be used by 49:mod:`repoze.bfg` to construct a ``context`` object. The context is 50attached to the request object passed to our view callables as the 51``context`` attribute. 52 53All of our context objects will possess an ``__acl__`` attribute that 54allows :data:`repoze.bfg.security.Everyone` (a special principal) to 55view all pages, while allowing only a :term:`principal` named 56``group:editors`` to edit and add pages. The ``__acl__`` attribute 57attached to a context is interpreted specially by :mod:`repoze.bfg` as 58an access control list during view callable execution. See 59:ref:`assigning_acls` for more information about what an :term:`ACL` 60represents. 61 62.. note: Although we don't use the functionality here, the ``factory`` 63 used to create route contexts may differ per-route as opposed to 64 globally. See the ``factory`` attribute in 65 :ref:`route_zcml_directive` for more info. 66 67We'll pass the ``RootFactory`` we created in the step above in as the 68``root_factory`` argument to a :term:`Configurator`. When we're done, 69your application's ``run.py`` will look like this. 70 71.. literalinclude:: src/authorization/tutorial/run.py 72 :linenos: 73 :language: python 74 75Configuring a ``repoze.bfg`` Authorization Policy 76------------------------------------------------- 77 78For any :mod:`repoze.bfg` application to perform authorization, we 79need to add a ``security.py`` module and we'll need to change our 80``configure.zcml`` file to add an :term:`authentication policy` and an 81:term:`authorization policy`. 82 83Changing ``configure.zcml`` 84~~~~~~~~~~~~~~~~~~~~~~~~~~~ 85 86We'll change our ``configure.zcml`` file to enable an 87``AuthTktAuthenticationPolicy`` and an ``ACLAuthorizationPolicy`` to 88enable declarative security checking. We'll also change 89``configure.zcml`` to add a view stanza which points at our ``login`` 90:term:`view callable`, also known as a :term:`forbidden view`. This 91configures our newly created login view to show up when 92:mod:`repoze.bfg` detects that a view invocation can not be 93authorized. Also, we'll add ``view_permission`` attributes with the 94value ``edit`` to the ``edit_page`` and ``add_page`` route 95declarations. This indicates that the view callables which these 96routes reference cannot be invoked without the authenticated user 97possessing the ``edit`` permission with respect to the current 98context. 99 100This makes the assertion that only users who possess the effective 101``edit`` permission at the time of the request may invoke those two 102views. We've granted the ``group:editors`` principal the ``edit`` 103permission at the root model via its ACL, so only the a user whom is a 104member of the group named ``group:editors`` will able to invoke the 105views associated with the ``add_page`` or ``edit_page`` routes. 106 107When you're done, your ``configure.zcml`` will look like so 108 109.. literalinclude:: src/authorization/tutorial/configure.zcml 110 :linenos: 111 :language: xml 112 113Note that the ``authtktauthenticationpolicy`` tag has two attributes: 114``secret`` and ``callback``. ``secret`` is a string representing an 115encryption key used by the "authentication ticket" machinery 116represented by this policy: it is required. The ``callback`` is a 117string, representing a :term:`dotted Python name`, which points at the 118``groupfinder`` function in the current directory's ``security.py`` 119file. We haven't added that module yet, but we're about to. 120 121Adding ``security.py`` 122~~~~~~~~~~~~~~~~~~~~~~ 123 124Add a ``security.py`` module within your package (in the same 125directory as "run.py", "views.py", etc) with the following content: 126 127.. literalinclude:: src/authorization/tutorial/security.py 128 :linenos: 129 :language: python 130 131The groupfinder defined here is an :term:`authentication policy` 132"callback"; it is a callable that accepts a userid and a request. If 133the userid exists in the system, the callback will return a sequence 134of group identifiers (or an empty sequence if the user isn't a member 135of any groups). If the userid *does not* exist in the system, the 136callback will return ``None``. In a production system, user and group 137data will most often come from a database, but here we use "dummy" 138data to represent user and groups sources. Note that the ``editor`` 139user is a member of the ``group:editors`` group in our dummy group 140data (the ``GROUPS`` data structure). 141 142We've given the ``editor`` user membership to the ``group:editors`` by 143mapping him to this group in the ``GROUPS`` data structure (``GROUPS = 144{'editor':['group:editors']}``). Since the ``groupfinder`` function 145consults the ``GROUPS`` data structure, this will mean that, as a 146result of the ACL attached to the root returned by the root factory, 147and the permission associated with the ``add_page`` and ``edit_page`` 148views, the ``editor`` user should be able to add and edit pages. 149 150Adding Login and Logout Views 151~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 152 153We'll add a ``login`` view callable which renders a login form and 154processes the post from the login form, checking credentials. 155 156We'll also add a ``logout`` view callable to our application and 157provide a link to it. This view will clear the credentials of the 158logged in user and redirect back to the front page. 159 160We'll add a different file (for presentation convenience) to add login 161and logout view callables. Add a file named ``login.py`` to your 162application (in the same directory as ``views.py``) with the following 163content: 164 165.. literalinclude:: src/authorization/tutorial/login.py 166 :linenos: 167 :language: python 168 169Changing Existing Views 170~~~~~~~~~~~~~~~~~~~~~~~ 171 172Then we need to change each of our ``view_page``, ``edit_page`` and 173``add_page`` views in ``views.py`` to pass a "logged in" parameter to 174its template. We'll add something like this to each view body: 175 176.. ignore-next-block 177.. code-block:: python 178 :linenos: 179 180 from repoze.bfg.security import authenticated_userid 181 logged_in = authenticated_userid(request) 182 183We'll then change the return value of these views to pass the 184`resulting `logged_in`` value to the template, e.g.: 185 186.. ignore-next-block 187.. code-block:: python 188 :linenos: 189 190 return dict(page = context, 191 content = content, 192 logged_in = logged_in, 193 edit_url = edit_url) 194 195Adding the ``login.pt`` Template 196~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 197 198Add a ``login.pt`` template to your templates directory. It's 199referred to within the login view we just added to ``login.py``. 200 201.. literalinclude:: src/authorization/tutorial/templates/login.pt 202 :linenos: 203 :language: xml 204 205Change ``view.pt`` and ``edit.pt`` 206~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 207 208We'll also need to change our ``edit.pt`` and ``view.pt`` templates to 209display a "Logout" link if someone is logged in. This link will 210invoke the logout view. 211 212To do so we'll add this to both templates within the ``<div 213class="main_content">`` div: 214 215.. code-block:: xml 216 :linenos: 217 218 <span tal:condition="logged_in"> 219 <a href="${request.application_url}/logout">Logout</a> 220 </span> 221 222Viewing the Application in a Browser 223------------------------------------ 224 225We can finally examine our application in a browser. The views we'll 226try are as follows: 227 228- Visiting ``http://localhost:6543/`` in a browser invokes the 229 ``view_wiki`` view. This always redirects to the ``view_page`` view 230 of the FrontPage page object. It is executable by any user. 231 232- Visiting ``http://localhost:6543/FrontPage`` in a browser invokes 233 the ``view_page`` view of the FrontPage page object. 234 235- Visiting ``http://localhost:6543/FrontPage/edit_page`` in a browser 236 invokes the edit view for the FrontPage object. It is executable by 237 only the ``editor`` user. If a different user (or the anonymous 238 user) invokes it, a login form will be displayed. Supplying the 239 credentials with the username ``editor``, password ``editor`` will 240 display the edit page form. 241 242- Visiting ``http://localhost:6543/add_page/SomePageName`` in a 243 browser invokes the add view for a page. It is executable by only 244 the ``editor`` user. If a different user (or the anonymous user) 245 invokes it, a login form will be displayed. Supplying the 246 credentials with the username ``editor``, password ``editor`` will 247 display the edit page form. 248 249Seeing Our Changes To ``views.py`` and our Templates 250---------------------------------------------------- 251 252Our ``views.py`` module will look something like this when we're done: 253 254.. literalinclude:: src/authorization/tutorial/views.py 255 :linenos: 256 :language: python 257 258Our ``edit.pt`` template will look something like this when we're done: 259 260.. literalinclude:: src/authorization/tutorial/templates/edit.pt 261 :linenos: 262 :language: xml 263 264Our ``view.pt`` template will look something like this when we're done: 265 266.. literalinclude:: src/authorization/tutorial/templates/view.pt 267 :linenos: 268 :language: xml 269 270Revisiting the Application 271--------------------------- 272 273When we revisit the application in a browser, and log in (as a result 274of hitting an edit or add page and submitting the login form with the 275``editor`` credentials), we'll see a Logout link in the upper right 276hand corner. When we click it, we're logged out, and redirected back 277to the front page. 278 279 280