summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt24
-rw-r--r--CONTRIBUTORS.txt4
-rw-r--r--HISTORY.txt6
-rw-r--r--README.rst8
-rw-r--r--docs/_static/pyramid_request_processing.graffle244
-rw-r--r--docs/_static/pyramid_request_processing.pngbin130688 -> 135179 bytes
-rw-r--r--docs/_static/pyramid_request_processing.svg2
-rw-r--r--docs/api/config.rst1
-rw-r--r--docs/api/csrf.rst23
-rw-r--r--docs/api/interfaces.rst3
-rw-r--r--docs/api/session.rst4
-rw-r--r--docs/glossary.rst5
-rw-r--r--docs/narr/hooks.rst2
-rw-r--r--docs/narr/logging.rst2
-rw-r--r--docs/narr/myproject/README.txt2
-rw-r--r--docs/narr/project.rst2
-rw-r--r--docs/narr/security.rst216
-rw-r--r--docs/narr/sessions.rst183
-rw-r--r--docs/narr/templates.rst4
-rw-r--r--docs/quick_tour.rst4
-rw-r--r--docs/quick_tour/logging/README.txt2
-rw-r--r--docs/quick_tour/package/README.txt2
-rw-r--r--docs/quick_tour/sessions/README.txt2
-rw-r--r--docs/quick_tour/sqla_demo/README.txt2
-rw-r--r--docs/quick_tutorial/cookiecutters.rst2
-rw-r--r--docs/quick_tutorial/cookiecutters/README.txt2
-rw-r--r--docs/tutorials/modwsgi/index.rst2
-rw-r--r--docs/tutorials/wiki/installation.rst2
-rw-r--r--docs/tutorials/wiki/src/authorization/README.txt2
-rw-r--r--docs/tutorials/wiki/src/basiclayout/README.txt2
-rw-r--r--docs/tutorials/wiki/src/installation/README.txt2
-rw-r--r--docs/tutorials/wiki/src/models/README.txt2
-rw-r--r--docs/tutorials/wiki/src/tests/README.txt2
-rw-r--r--docs/tutorials/wiki/src/views/README.txt2
-rw-r--r--docs/tutorials/wiki2/installation.rst2
-rw-r--r--docs/tutorials/wiki2/src/authentication/README.txt2
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/default.py6
-rw-r--r--docs/tutorials/wiki2/src/authorization/README.txt2
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views/default.py6
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/README.txt2
-rw-r--r--docs/tutorials/wiki2/src/installation/README.txt2
-rw-r--r--docs/tutorials/wiki2/src/models/README.txt2
-rw-r--r--docs/tutorials/wiki2/src/tests/README.txt2
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/views/default.py6
-rw-r--r--docs/tutorials/wiki2/src/views/README.txt2
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/views/default.py6
-rw-r--r--docs/whatsnew-1.8.rst7
-rw-r--r--pyramid/config/__init__.py52
-rw-r--r--pyramid/config/security.py36
-rw-r--r--pyramid/config/views.py24
-rw-r--r--pyramid/csrf.py332
-rw-r--r--pyramid/interfaces.py43
-rw-r--r--pyramid/predicates.py2
-rw-r--r--pyramid/renderers.py5
-rw-r--r--pyramid/session.py169
-rw-r--r--pyramid/testing.py1
-rw-r--r--pyramid/tests/test_config/test_init.py26
-rw-r--r--pyramid/tests/test_config/test_views.py3
-rw-r--r--pyramid/tests/test_csrf.py406
-rw-r--r--pyramid/tests/test_renderers.py8
-rw-r--r--pyramid/tests/test_session.py138
-rw-r--r--pyramid/tests/test_util.py6
-rw-r--r--pyramid/tests/test_viewderivers.py1
-rw-r--r--pyramid/viewderivers.py2
64 files changed, 1456 insertions, 612 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 59196626d..075d3ffd9 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -36,6 +36,26 @@ Features
requirement that the server is being run in this format so it may fail.
See https://github.com/Pylons/pyramid/pull/2984
+- The threadlocals are now available inside any function invoked via
+ ``config.include``. This means the only config-time code that cannot rely
+ on threadlocals is code executed from non-actions inside the main. This
+ can be alleviated by invoking ``config.begin()`` and ``config.end()``
+ appropriately. See https://github.com/Pylons/pyramid/pull/2989
+
+- A new CSRF implementation, ``pyramid.csrf.SessionCSRFStoragePolicy``,
+ has been added which delegates all CSRF generation to the current session,
+ following the old API for this. A ``pyramid.csrf.get_csrf_token()`` api is now
+ available in template global scope, to make it easy for template developers
+ to get the current CSRF token without adding it to Python code.
+ See https://github.com/Pylons/pyramid/pull/2854 and
+ https://github.com/Pylons/pyramid/pull/3019
+
+- The ``pyramid.config.Configurator`` can now be used as a context manager
+ which will automatically push/pop threadlocals (similar to
+ ``config.begin()`` and ``config.end()``). It will also automatically perform
+ a ``config.commit()`` and thus it is only recommended to be used at the
+ top-level of your app. See https://github.com/Pylons/pyramid/pull/2874
+
Bug Fixes
---------
@@ -62,3 +82,7 @@ Backward Incompatibilities
Documentation Changes
---------------------
+
+- Retrieving CSRF token from the session has been deprecated, in favor of
+ equivalent methods in :mod:`pyramid.csrf`.
+ See https://github.com/Pylons/pyramid/pull/2854
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 566e91195..ca1f56f51 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -291,6 +291,10 @@ Contributors
- Mikko Ohtamaa, 2016/12/6
+- Jure Cerjak, 2016/12/7
+
- Martin Frlin, 2016/12/7
- Kirill Kuzminykh, 2017/03/01
+
+- Jeremy(Ching-Rui) Chen, 2017/04/19
diff --git a/HISTORY.txt b/HISTORY.txt
index c10747af4..c69d9514e 100644
--- a/HISTORY.txt
+++ b/HISTORY.txt
@@ -198,6 +198,12 @@ Features
See https://github.com/Pylons/pyramid/pull/2873
+- Added a new ``callback`` option to ``config.set_default_csrf_options`` which
+ can be used to determine per-request whether CSRF checking should be enabled
+ to allow for a mix authentication methods. Only cookie-based methods
+ generally require CSRF checking.
+ See https://github.com/Pylons/pyramid/pull/2778
+
Bug Fixes
---------
diff --git a/README.rst b/README.rst
index f05dd8bbf..302590fe1 100644
--- a/README.rst
+++ b/README.rst
@@ -31,10 +31,10 @@ and deployment more fun, more predictable, and more productive.
return Response('Hello %(name)s!' % request.matchdict)
if __name__ == '__main__':
- config = Configurator()
- config.add_route('hello', '/hello/{name}')
- config.add_view(hello_world, route_name='hello')
- app = config.make_wsgi_app()
+ with Configurator() as config:
+ config.add_route('hello', '/hello/{name}')
+ config.add_view(hello_world, route_name='hello')
+ app = config.make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever()
diff --git a/docs/_static/pyramid_request_processing.graffle b/docs/_static/pyramid_request_processing.graffle
index 56e4e13f2..5ccbf4ea4 100644
--- a/docs/_static/pyramid_request_processing.graffle
+++ b/docs/_static/pyramid_request_processing.graffle
@@ -59,8 +59,128 @@
<key>GraphicsList</key>
<array>
<dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <key>FontInfo</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>w</key>
+ <string>0</string>
+ </dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169378</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169517</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Points</key>
+ <array>
+ <string>{155.00000381469727, 109.82143211364746}</string>
+ <string>{155.00000254313238, 133.68303707668386}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.0980392</string>
+ <key>g</key>
+ <string>0.0980392</string>
+ <key>r</key>
+ <string>0.0980392</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169516</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{102.16667175292969, 87.276790618896484}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169516</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.999208</string>
+ <key>g</key>
+ <string>0.811343</string>
+ <key>r</key>
+ <string>0.644457</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs20 \cf0 execution policy}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
<key>Bounds</key>
- <string>{{238.74999618530273, 294.65604172230951}, {105.66668701171875, 18.656048080136394}}</string>
+ <string>{{238.74999618530273, 325.15604172230951}, {105.66668701171875, 18.656048080136394}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>FontInfo</key>
@@ -146,8 +266,8 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{154.9999760464211, 209.11365574251681}</string>
- <string>{239.8333613077798, 209.14732074737549}</string>
+ <string>{154.99997604642331, 241.21656873817005}</string>
+ <string>{239.8333613077798, 240.64732074737549}</string>
</array>
<key>Style</key>
<dict>
@@ -182,7 +302,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{239.83336130777977, 197.875}, {105.66666412353516, 22.544641494750977}}</string>
+ <string>{{239.83336130777977, 229.375}, {105.66666412353516, 22.544641494750977}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>ID</key>
@@ -259,8 +379,8 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{344.41668319702148, 411.88506673894034}</string>
- <string>{375.5, 411.77232108797347}</string>
+ <string>{344.41668319702148, 442.38506673894034}</string>
+ <string>{375.5, 442.27232108797347}</string>
</array>
<key>Style</key>
<dict>
@@ -317,8 +437,8 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{155.00000254313238, 459.27667544230695}</string>
- <string>{238.5002713470962, 456.52468399152298}</string>
+ <string>{155.00000254313238, 489.77667544230695}</string>
+ <string>{238.5002713470962, 487.02468399152298}</string>
</array>
<key>Style</key>
<dict>
@@ -377,8 +497,8 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{155.00000254313238, 482.12574895537085}</string>
- <string>{238.52297468463752, 508.35839132916635}</string>
+ <string>{155.00000254313238, 512.6257489553708}</string>
+ <string>{238.52297468463752, 538.85839132916635}</string>
</array>
<key>Style</key>
<dict>
@@ -413,7 +533,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{238.74999618530273, 275.99999999999994}, {105.75002924601222, 18.656048080136394}}</string>
+ <string>{{238.74999618530273, 306.49999999999994}, {105.75002924601222, 18.656048080136394}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>FontInfo</key>
@@ -475,7 +595,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{238.74999618530273, 421.15071036499205}, {105.66668701171875, 18.656048080136394}}</string>
+ <string>{{238.74999618530273, 451.65071036499205}, {105.66668701171875, 18.656048080136394}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>FontInfo</key>
@@ -537,7 +657,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{238.74999618530273, 312.65604172230951}, {105.66668701171875, 18.656048080136394}}</string>
+ <string>{{238.74999618530273, 343.15604172230951}, {105.66668701171875, 18.656048080136394}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>FontInfo</key>
@@ -599,7 +719,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{238.74999618530273, 402.55704269887212}, {105.66668701171875, 18.656048080136394}}</string>
+ <string>{{238.74999618530273, 433.05704269887212}, {105.66668701171875, 18.656048080136394}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>FontInfo</key>
@@ -659,7 +779,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{238.74999618530273, 383.90099016834085}, {105.66668701171875, 18.656048080136394}}</string>
+ <string>{{238.74999618530273, 414.40099016834085}, {105.66668701171875, 18.656048080136394}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>FontInfo</key>
@@ -719,7 +839,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{238.74999618530273, 350.36561209044055}, {105.66668701171875, 33.089282989501953}}</string>
+ <string>{{238.74999618530273, 380.86561209044055}, {105.66668701171875, 33.089282989501953}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>FontInfo</key>
@@ -779,7 +899,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{238.74999618530273, 331.26348241170439}, {105.66668701171875, 18.656048080136394}}</string>
+ <string>{{238.74999618530273, 361.76348241170439}, {105.66668701171875, 18.656048080136394}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>FontInfo</key>
@@ -865,8 +985,8 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{155.00000254313238, 470.25295298442387}</string>
- <string>{238.33861159880226, 482.4262543949045}</string>
+ <string>{155.00000254313238, 500.75295298442387}</string>
+ <string>{238.33861159880226, 512.9262543949045}</string>
</array>
<key>Style</key>
<dict>
@@ -901,7 +1021,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{238.83336130777977, 471.22620192028251}, {105.66666412353516, 22.544641494750977}}</string>
+ <string>{{238.83336130777977, 501.72620192028251}, {105.66666412353516, 22.544641494750977}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>ID</key>
@@ -980,8 +1100,8 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{154.99998733539806, 128.68025330008533}</string>
- <string>{239.83340199788393, 128.59152244387357}</string>
+ <string>{154.99998724282403, 165.00131810958592}</string>
+ <string>{239.83340199788393, 164.59152244387357}</string>
</array>
<key>Style</key>
<dict>
@@ -1016,7 +1136,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{239.83340199788395, 117.31920169649808}, {105.66666412353516, 22.544641494750977}}</string>
+ <string>{{239.83340199788395, 153.31920169649808}, {105.66666412353516, 22.544641494750977}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>ID</key>
@@ -1074,7 +1194,7 @@
<array>
<dict>
<key>Bounds</key>
- <string>{{102.1666056315114, 148.28868579864499}, {105.66669464111328, 33.08929443359375}}</string>
+ <string>{{102.1666056315114, 181.78868579864499}, {105.66669464111328, 33.08929443359375}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>ID</key>
@@ -1125,7 +1245,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{102.1666056315114, 181.37798023223874}, {105.66669464111328, 17.244049072265625}}</string>
+ <string>{{102.1666056315114, 214.87798023223874}, {105.66669464111328, 17.244049072265625}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>ID</key>
@@ -1193,7 +1313,7 @@
<array>
<dict>
<key>Bounds</key>
- <string>{{102.16666158040482, 272}, {105.66666412353516, 33.08929443359375}}</string>
+ <string>{{102.16666158040482, 302.5}, {105.66666412353516, 33.08929443359375}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>ID</key>
@@ -1244,7 +1364,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{102.16666158040482, 305.08929443359375}, {105.66666412353516, 17.244049072265625}}</string>
+ <string>{{102.16666158040482, 335.58929443359375}, {105.66666412353516, 17.244049072265625}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>ID</key>
@@ -1321,8 +1441,8 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{238.74999618530282, 439.80675844512831}</string>
- <string>{207.66666666666765, 385.656005859375}</string>
+ <string>{238.74999618530282, 470.30675844512831}</string>
+ <string>{207.66666666666765, 416.156005859375}</string>
</array>
<key>Style</key>
<dict>
@@ -1371,8 +1491,8 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{239.25039065750093, 276.57837549845181}</string>
- <string>{207.66666666666777, 353.07514659563753}</string>
+ <string>{239.25039065750093, 307.07837549845181}</string>
+ <string>{207.66666666666777, 383.57514659563753}</string>
</array>
<key>Style</key>
<dict>
@@ -1436,8 +1556,8 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{155.00000254313238, 386.66442959065108}</string>
- <string>{155.00000254313238, 422.21209462483216}</string>
+ <string>{155.00000254313238, 417.16442959065108}</string>
+ <string>{155.00000254313238, 452.71209462483216}</string>
</array>
<key>Style</key>
<dict>
@@ -1472,7 +1592,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{102.16667048136482, 353.07514659563753}, {105.66666412353516, 33.089282989501953}}</string>
+ <string>{{102.16667048136482, 383.57514659563753}, {105.66666412353516, 33.089282989501953}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>ID</key>
@@ -1555,8 +1675,8 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{154.9999936421724, 258.44082431579938}</string>
- <string>{238.8333613077798, 258.45536063967575}</string>
+ <string>{154.9999936421724, 288.94082431579938}</string>
+ <string>{238.8333613077798, 288.95536063967575}</string>
</array>
<key>Style</key>
<dict>
@@ -1969,7 +2089,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{375.5, 400.5}, {105.66666412353516, 22.544642175946908}}</string>
+ <string>{{375.5, 431}, {105.66666412353516, 22.544642175946908}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>ID</key>
@@ -2053,8 +2173,8 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{155.00000170434049, 119.22767858295661}</string>
- <string>{154.99995295206804, 148.28868579864499}</string>
+ <string>{155.00000157307935, 156.72767858475962}</string>
+ <string>{154.99995295206804, 181.78868579864499}</string>
</array>
<key>Style</key>
<dict>
@@ -2110,7 +2230,9 @@
<key>Head</key>
<dict>
<key>ID</key>
- <integer>169378</integer>
+ <integer>169516</integer>
+ <key>Info</key>
+ <integer>2</integer>
</dict>
<key>ID</key>
<integer>169385</integer>
@@ -2119,7 +2241,7 @@
<key>Points</key>
<array>
<string>{155.00000254313238, 67.727678571434836}</string>
- <string>{155.00000254313238, 96.18303707668386}</string>
+ <string>{155.00000381469727, 87.276790618896484}</string>
</array>
<key>Style</key>
<dict>
@@ -2156,7 +2278,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{102.16667048136482, 509.6179466247504}, {105.66666412353516, 22.544641494750977}}</string>
+ <string>{{102.16667048136482, 540.1179466247504}, {105.66666412353516, 22.544641494750977}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>ID</key>
@@ -2209,7 +2331,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{239, 497.23589324949899}, {105.66666412353516, 22.544641494750977}}</string>
+ <string>{{239, 527.73589324949899}, {105.66666412353516, 22.544641494750977}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>ID</key>
@@ -2262,7 +2384,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{239, 445.23589324949717}, {105.66666412353516, 22.544641494750977}}</string>
+ <string>{{239, 475.73589324949717}, {105.66666412353516, 22.544641494750977}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>ID</key>
@@ -2315,7 +2437,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{102.16667048136482, 422.21209462483216}, {105.66666412353516, 22.544641494750977}}</string>
+ <string>{{102.16667048136482, 452.71209462483216}, {105.66666412353516, 22.544641494750977}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>ID</key>
@@ -2368,7 +2490,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{238.83336130777977, 247.18303989230026}, {105.66666412353516, 22.544641494750977}}</string>
+ <string>{{238.83336130777977, 277.68303989230026}, {105.66666412353516, 22.544641494750977}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>ID</key>
@@ -2421,7 +2543,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{102.16667048136482, 222.18303707668389}, {105.66666412353516, 22.544641494750977}}</string>
+ <string>{{102.16667048136482, 252.68303707668389}, {105.66666412353516, 22.544641494750977}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>ID</key>
@@ -2474,7 +2596,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{102.16667048136482, 96.18303707668386}, {105.66666412353516, 22.544641494750977}}</string>
+ <string>{{102.16667048136482, 133.68303707668386}, {105.66666412353516, 22.544641494750977}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>ID</key>
@@ -2609,8 +2731,8 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{154.99995295206804, 198.62202930450437}</string>
- <string>{155.00000254313238, 222.18303707668389}</string>
+ <string>{154.99995295206804, 232.12202930450437}</string>
+ <string>{155.00000254313238, 252.68303707668389}</string>
</array>
<key>Style</key>
<dict>
@@ -2678,8 +2800,8 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{154.9999936421724, 245.22767856643924}</string>
- <string>{154.9999936421724, 272}</string>
+ <string>{154.9999936421724, 275.72767856643924}</string>
+ <string>{154.9999936421724, 302.5}</string>
</array>
<key>Style</key>
<dict>
@@ -2743,8 +2865,8 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{154.9999936421724, 322.33334350585938}</string>
- <string>{155.00000254313238, 353.07514659563753}</string>
+ <string>{154.9999936421724, 352.83334350585938}</string>
+ <string>{155.00000254313238, 383.57514659563753}</string>
</array>
<key>Style</key>
<dict>
@@ -2812,8 +2934,8 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{155.00000254313238, 444.75673611958314}</string>
- <string>{155.00000254313238, 509.6179466247504}</string>
+ <string>{155.00000254313238, 475.25673611958314}</string>
+ <string>{155.00000254313238, 540.1179466247504}</string>
</array>
<key>Style</key>
<dict>
@@ -9868,7 +9990,7 @@
<key>MasterSheets</key>
<array/>
<key>ModificationDate</key>
- <string>2016-04-13 08:32:47 +0000</string>
+ <string>2017-04-10 09:33:14 +0000</string>
<key>Modifier</key>
<string>Steve Piercy</string>
<key>NotesVisible</key>
@@ -9949,7 +10071,7 @@
</dict>
</array>
<key>Frame</key>
- <string>{{35, 93}, {2284, 1325}}</string>
+ <string>{{35, 93}, {1632, 1325}}</string>
<key>ListView</key>
<true/>
<key>OutlineWidth</key>
@@ -9963,15 +10085,15 @@
<key>SidebarWidth</key>
<integer>163</integer>
<key>VisibleRegion</key>
- <string>{{110.125, 77.875}, {239.125, 146.375}}</string>
+ <string>{{-27, 33.5}, {630.5, 593}}</string>
<key>Zoom</key>
- <real>8</real>
+ <real>2</real>
<key>ZoomValues</key>
<array>
<array>
<string>Request Processing</string>
+ <real>2</real>
<real>8</real>
- <real>4</real>
</array>
</array>
</dict>
diff --git a/docs/_static/pyramid_request_processing.png b/docs/_static/pyramid_request_processing.png
index 2f44f4824..d62b172b0 100644
--- a/docs/_static/pyramid_request_processing.png
+++ b/docs/_static/pyramid_request_processing.png
Binary files differ
diff --git a/docs/_static/pyramid_request_processing.svg b/docs/_static/pyramid_request_processing.svg
index 03f6d56fa..38fa7348d 100644
--- a/docs/_static/pyramid_request_processing.svg
+++ b/docs/_static/pyramid_request_processing.svg
@@ -1,3 +1,3 @@
<?xml version="1.0"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="91 11 424 533" width="424pt" height="533pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2016-04-13 08:32Z</dc:date><!-- Produced by OmniGraffle Professional 5.4.4 --></metadata><defs><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="SharpArrow_Marker" viewBox="-4 -4 10 8" markerWidth="10" markerHeight="8" color="#191919"><g><path d="M 5 0 L -3 -3 L 0 0 L 0 0 L -3 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Request Processing</title><rect fill="white" width="576" height="733"/><g><title>no exceptions</title><path d="M 155 444.75674 C 155 450.64061 155 486.2592 155 502.71617" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99999 322.33334 C 154.99999 327.72413 155 337.74646 155 346.1775" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99999 245.22768 C 154.99999 250.5417 154.99999 257.93189 154.99999 265.10145" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99995 198.62203 C 154.99995 203.74682 154.99998 209.1909 154.99999 215.28222" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="102.16667" y="45.183037" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="45.183037" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 50.455358)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="4.7596016" y="10" textLength="88.92578">middleware ingress </tspan></text><rect x="102.16667" y="96.183037" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="96.183037" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 101.45536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.983723" y="10" textLength="61.69922">tween ingress</tspan></text><rect x="102.16667" y="222.18304" width="105.666664" height="22.544641" fill="#d2ffd0"/><rect x="102.16667" y="222.18304" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 227.45536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="28.660969" y="10" textLength="38.344727">traversal</tspan></text><rect x="238.83336" y="247.18304" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="238.83336" y="247.18304" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.83336 252.45536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.424641" y="10" textLength="62.817383">ContextFound</tspan></text><rect x="102.16667" y="422.2121" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="422.2121" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 427.48442)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="18.094563" y="10" textLength="59.47754">tween egress</tspan></text><rect x="239" y="445.2359" width="105.666664" height="22.544641" fill="#fed153"/><rect x="239" y="445.2359" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244 450.50821)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.3113594" y="10" textLength="85.043945">response callbacks</tspan></text><rect x="239" y="497.2359" width="105.666664" height="22.544641" fill="#fed153"/><rect x="239" y="497.2359" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244 502.5082)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="8.6463203" y="10" textLength="5">fi</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="13.64632" y="10" textLength="73.374023">nished callbacks</tspan></text><rect x="102.16667" y="509.61795" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="509.61795" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 514.89027)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.8704414" y="10" textLength="83.92578">middleware egress</tspan></text><path d="M 155 67.72768 C 155 73.048893 155 81.55558 155 89.2853" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 155 119.22768 C 155 124.62026 154.99997 133.48763 154.99996 141.38632" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="375.5" y="400.5" width="105.666664" height="22.544642" fill="#dfbeff"/><rect x="375.5" y="400.5" width="105.666664" height="22.544642" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(380.5 405.77232)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.702961" y="10" textLength="62.260742">BeforeRender</tspan></text><text transform="translate(233.5 20)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="bold" x=".31445312" y="11" textLength="115.371094">Request Processing</tspan></text><path d="M 375.99995 42.910746 L 498.66662 42.910746 C 501.42805 42.910746 503.66662 45.149323 503.66662 47.910746 L 503.66662 222 C 503.66662 224.76142 501.42805 227 498.66662 227 L 375.99995 227 C 373.23853 227 370.99995 224.76142 370.99995 222 L 370.99995 47.910746 C 370.99995 45.149323 373.23853 42.910746 375.99995 42.910746 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(375.99995 42.910746)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="0" y="10" textLength="35.55664">Legend</tspan></text><rect x="383.66662" y="63.908513" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="383.66662" y="63.908513" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 69.180834)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="35.601887" y="10" textLength="24.46289">event</tspan></text><rect x="383.66662" y="186.58226" width="105.666664" height="22.544641" fill="#fed153"/><rect x="383.66662" y="186.58226" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 191.85458)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="29.769367" y="10" textLength="36.12793">callback</tspan></text><rect x="383.66662" y="158.54998" width="105.666664" height="22.544641" fill="#ffff6c"/><rect x="383.66662" y="158.54998" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 163.8223)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="21.158527" y="10" textLength="53.34961">view deriver</tspan></text><rect x="383.66662" y="91.94079" width="105.666664" height="33.089283" fill="#a4cfff"/><rect x="383.66662" y="91.94079" width="105.666664" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 96.48543)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="11.148762" y="10" textLength="76.14746">external process </tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="2.8162422" y="22" textLength="90.03418">(middleware, tween)</tspan></text><rect x="383.66662" y="130.51771" width="105.666664" height="22.544641" fill="#d2ffd0"/><rect x="383.66662" y="130.51771" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 135.79003)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="12.537922" y="10" textLength="70.59082">internal process</tspan></text><line x1="154.99999" y1="258.44082" x2="238.83336" y2="258.45536" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="102.16667" y="353.07515" width="105.666664" height="33.089283" fill="#ffff6c"/><rect x="102.16667" y="353.07515" width="105.666664" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 363.61979)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.205402" y="10" textLength="57.25586">view pipeline</tspan></text><path d="M 155 386.66443 C 155 392.17252 155 405.5052 155 415.30935" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="239.25039" y1="276.57838" x2="207.66667" y2="353.07515" stroke="#c1c1c1" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,3"/><line x1="238.75" y1="439.80676" x2="207.66667" y2="385.656" stroke="#c1c1c1" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,3"/><rect x="102.16666" y="305.0893" width="105.666664" height="17.244049" fill="#d2ffd0"/><rect x="102.16666" y="305.0893" width="105.666664" height="17.244049" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16666 307.71132)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="24.764484" y="10" textLength="46.137695">predicates</tspan></text><rect x="102.16666" y="272" width="105.666664" height="33.089294" fill="#d2ffd0"/><rect x="102.16666" y="272" width="105.666664" height="33.089294" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16666 282.54465)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="21.707844" y="10" textLength="52.250977">view lookup</tspan></text><rect x="102.166606" y="181.37798" width="105.666695" height="17.244049" fill="#d2ffd0"/><rect x="102.166606" y="181.37798" width="105.666695" height="17.244049" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.166606 184)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="11.978855" y="10" textLength="71.708984">route predicates</tspan></text><rect x="102.166606" y="148.28869" width="105.666695" height="33.089294" fill="#d2ffd0"/><rect x="102.166606" y="148.28869" width="105.666695" height="33.089294" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.166606 158.83333)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="18.001804" y="10" textLength="20.004883">URL</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="37.640476" y="10" textLength="40.024414"> dispatch</tspan></text><rect x="239.8334" y="117.3192" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="239.8334" y="117.3192" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244.8334 122.59152)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.207844" y="10" textLength="57.250977">NewRequest</tspan></text><line x1="154.99999" y1="128.68025" x2="239.8334" y2="128.59152" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.83336" y="471.2262" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="238.83336" y="471.2262" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.83336 476.49852)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="15.316242" y="10" textLength="65.03418">NewResponse</tspan></text><line x1="155" y1="470.25295" x2="238.33861" y2="482.42625" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.75" y="331.26348" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="331.26348" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 334.5915)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="1.9812927" y="10" textLength="91.7041">view mapper ingress</tspan></text><rect x="238.75" y="350.36561" width="105.66669" height="33.089283" fill="#ffff6c"/><rect x="238.75" y="350.36561" width="105.66669" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 360.91025)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="37.830902" y="10" textLength="20.004883">view</tspan></text><rect x="238.75" y="383.901" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="383.901" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 387.22901)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="3.0921326" y="10" textLength="89.48242">view mapper egress</tspan></text><rect x="238.75" y="402.55704" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="402.55704" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 405.88507)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="8.917328" y="10" textLength="77.83203">response adapter</tspan></text><rect x="238.75" y="312.65604" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="312.65604" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 315.98407)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="6.7029724" y="10" textLength="82.26074">decorators ingress</tspan></text><rect x="238.75" y="421.1507" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="421.1507" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 424.47873)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="7.8138123" y="10" textLength="80.039062">decorators egress</tspan></text><rect x="238.75" y="276" width="105.75003" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="276" width="105.75003" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 279.32802)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.244644" y="10" textLength="57.260742">authorization</tspan></text><line x1="155" y1="482.12575" x2="238.52297" y2="508.3584" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><line x1="155" y1="459.27668" x2="238.50027" y2="456.52468" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><line x1="344.41668" y1="411.88507" x2="375.5" y2="411.77232" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="239.83336" y="197.875" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="239.83336" y="197.875" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244.83336 203.14732)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="12.44759" y="10" textLength="35.57129">BeforeT</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="47.652668" y="10" textLength="35.566406">raversal</tspan></text><line x1="154.99998" y1="209.11366" x2="239.83336" y2="209.14732" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.75" y="294.65604" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="294.65604" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 297.98407)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="17.27182" y="10" textLength="61.123047">CSRF checks</tspan></text></g></g></svg>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="91 11 424 563" width="424pt" height="563pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2017-04-10 09:33Z</dc:date><!-- Produced by OmniGraffle Professional 5.4.4 --></metadata><defs><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="SharpArrow_Marker" viewBox="-4 -4 10 8" markerWidth="10" markerHeight="8" color="#191919"><g><path d="M 5 0 L -3 -3 L 0 0 L 0 0 L -3 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Request Processing</title><rect fill="white" width="576" height="733"/><g><title>no exceptions</title><path d="M 155 475.25674 C 155 481.14061 155 516.7592 155 533.21617" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99999 352.83334 C 154.99999 358.22413 155 368.24646 155 376.6775" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99999 275.72768 C 154.99999 281.0417 154.99999 288.43189 154.99999 295.60145" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99995 232.12203 C 154.99995 237.07358 154.99998 240.81523 154.99999 245.78103" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="102.16667" y="45.183037" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="45.183037" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 50.455358)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="4.7596016" y="10" textLength="88.92578">middleware ingress </tspan></text><rect x="102.16667" y="133.68304" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="133.68304" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 138.95536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.983723" y="10" textLength="61.69922">tween ingress</tspan></text><rect x="102.16667" y="252.68304" width="105.666664" height="22.544641" fill="#d2ffd0"/><rect x="102.16667" y="252.68304" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 257.95536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="28.660969" y="10" textLength="38.344727">traversal</tspan></text><rect x="238.83336" y="277.68304" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="238.83336" y="277.68304" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.83336 282.95536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.424641" y="10" textLength="62.817383">ContextFound</tspan></text><rect x="102.16667" y="452.7121" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="452.7121" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 457.98442)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="18.094563" y="10" textLength="59.47754">tween egress</tspan></text><rect x="239" y="475.7359" width="105.666664" height="22.544641" fill="#fed153"/><rect x="239" y="475.7359" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244 481.0082)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.3113594" y="10" textLength="85.043945">response callbacks</tspan></text><rect x="239" y="527.7359" width="105.666664" height="22.544641" fill="#fed153"/><rect x="239" y="527.7359" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244 533.0082)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="8.6463203" y="10" textLength="5">fi</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="13.64632" y="10" textLength="73.374023">nished callbacks</tspan></text><rect x="102.16667" y="540.11795" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="540.11795" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 545.39027)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.8704414" y="10" textLength="83.92578">middleware egress</tspan></text><path d="M 155 67.72768 C 155 72.60557 155 75.81567 155 80.374542" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 155 156.72768 C 155 161.9728 154.999975 168.29494 154.99996 174.89142" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="375.5" y="431" width="105.666664" height="22.544642" fill="#dfbeff"/><rect x="375.5" y="431" width="105.666664" height="22.544642" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(380.5 436.27232)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.702961" y="10" textLength="62.260742">BeforeRender</tspan></text><text transform="translate(233.5 20)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="bold" x=".31445312" y="11" textLength="115.371094">Request Processing</tspan></text><path d="M 375.99995 42.910746 L 498.66662 42.910746 C 501.42805 42.910746 503.66662 45.149323 503.66662 47.910746 L 503.66662 222 C 503.66662 224.76142 501.42805 227 498.66662 227 L 375.99995 227 C 373.23853 227 370.99995 224.76142 370.99995 222 L 370.99995 47.910746 C 370.99995 45.149323 373.23853 42.910746 375.99995 42.910746 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(375.99995 42.910746)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="0" y="10" textLength="35.55664">Legend</tspan></text><rect x="383.66662" y="63.908513" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="383.66662" y="63.908513" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 69.180834)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="35.601887" y="10" textLength="24.46289">event</tspan></text><rect x="383.66662" y="186.58226" width="105.666664" height="22.544641" fill="#fed153"/><rect x="383.66662" y="186.58226" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 191.85458)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="29.769367" y="10" textLength="36.12793">callback</tspan></text><rect x="383.66662" y="158.54998" width="105.666664" height="22.544641" fill="#ffff6c"/><rect x="383.66662" y="158.54998" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 163.8223)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="21.158527" y="10" textLength="53.34961">view deriver</tspan></text><rect x="383.66662" y="91.94079" width="105.666664" height="33.089283" fill="#a4cfff"/><rect x="383.66662" y="91.94079" width="105.666664" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 96.48543)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="11.148762" y="10" textLength="76.14746">external process </tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="2.8162422" y="22" textLength="90.03418">(middleware, tween)</tspan></text><rect x="383.66662" y="130.51771" width="105.666664" height="22.544641" fill="#d2ffd0"/><rect x="383.66662" y="130.51771" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 135.79003)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="12.537922" y="10" textLength="70.59082">internal process</tspan></text><line x1="154.99999" y1="288.94082" x2="238.83336" y2="288.95536" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="102.16667" y="383.57515" width="105.666664" height="33.089283" fill="#ffff6c"/><rect x="102.16667" y="383.57515" width="105.666664" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 394.11979)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.205402" y="10" textLength="57.25586">view pipeline</tspan></text><path d="M 155 417.16443 C 155 422.67252 155 436.0052 155 445.80935" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="239.25039" y1="307.07838" x2="207.66667" y2="383.57515" stroke="#c1c1c1" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,3"/><line x1="238.75" y1="470.30676" x2="207.66667" y2="416.156" stroke="#c1c1c1" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,3"/><rect x="102.16666" y="335.5893" width="105.666664" height="17.244049" fill="#d2ffd0"/><rect x="102.16666" y="335.5893" width="105.666664" height="17.244049" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16666 338.21132)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="24.764484" y="10" textLength="46.137695">predicates</tspan></text><rect x="102.16666" y="302.5" width="105.666664" height="33.089294" fill="#d2ffd0"/><rect x="102.16666" y="302.5" width="105.666664" height="33.089294" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16666 313.04465)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="21.707844" y="10" textLength="52.250977">view lookup</tspan></text><rect x="102.166606" y="214.87798" width="105.666695" height="17.244049" fill="#d2ffd0"/><rect x="102.166606" y="214.87798" width="105.666695" height="17.244049" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.166606 217.5)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="11.978855" y="10" textLength="71.708984">route predicates</tspan></text><rect x="102.166606" y="181.78869" width="105.666695" height="33.089294" fill="#d2ffd0"/><rect x="102.166606" y="181.78869" width="105.666695" height="33.089294" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.166606 192.33333)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="18.001804" y="10" textLength="20.004883">URL</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="37.640476" y="10" textLength="40.024414"> dispatch</tspan></text><rect x="239.8334" y="153.3192" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="239.8334" y="153.3192" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244.8334 158.59152)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.207844" y="10" textLength="57.250977">NewRequest</tspan></text><line x1="154.99999" y1="165.00132" x2="239.8334" y2="164.59152" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.83336" y="501.7262" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="238.83336" y="501.7262" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.83336 506.99852)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="15.316242" y="10" textLength="65.03418">NewResponse</tspan></text><line x1="155" y1="500.75295" x2="238.33861" y2="512.92625" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.75" y="361.76348" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="361.76348" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 365.0915)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="1.9812927" y="10" textLength="91.7041">view mapper ingress</tspan></text><rect x="238.75" y="380.86561" width="105.66669" height="33.089283" fill="#ffff6c"/><rect x="238.75" y="380.86561" width="105.66669" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 391.41025)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="37.830902" y="10" textLength="20.004883">view</tspan></text><rect x="238.75" y="414.401" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="414.401" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 417.72901)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="3.0921326" y="10" textLength="89.48242">view mapper egress</tspan></text><rect x="238.75" y="433.05704" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="433.05704" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 436.38507)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="8.917328" y="10" textLength="77.83203">response adapter</tspan></text><rect x="238.75" y="343.15604" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="343.15604" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 346.48407)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="6.7029724" y="10" textLength="82.26074">decorators ingress</tspan></text><rect x="238.75" y="451.6507" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="451.6507" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 454.97873)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="7.8138123" y="10" textLength="80.039062">decorators egress</tspan></text><rect x="238.75" y="306.5" width="105.75003" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="306.5" width="105.75003" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 309.82802)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.244644" y="10" textLength="57.260742">authorization</tspan></text><line x1="155" y1="512.62575" x2="238.52297" y2="538.8584" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><line x1="155" y1="489.77668" x2="238.50027" y2="487.02468" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><line x1="344.41668" y1="442.38507" x2="375.5" y2="442.27232" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="239.83336" y="229.375" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="239.83336" y="229.375" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244.83336 234.64732)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="12.44759" y="10" textLength="35.57129">BeforeT</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="47.652668" y="10" textLength="35.566406">raversal</tspan></text><line x1="154.99998" y1="241.21657" x2="239.83336" y2="240.64732" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.75" y="325.15604" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="325.15604" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 328.48407)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="17.27182" y="10" textLength="61.123047">CSRF checks</tspan></text><rect x="102.16667" y="87.27679" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="87.27679" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 92.54911)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="12.25716" y="10" textLength="71.152344">execution policy</tspan></text><path d="M 155 109.82143 C 155 114.961226 155 120.58499 155 126.78393" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g></g></svg>
diff --git a/docs/api/config.rst b/docs/api/config.rst
index c76d3d5ff..a785b64ad 100644
--- a/docs/api/config.rst
+++ b/docs/api/config.rst
@@ -37,6 +37,7 @@
.. automethod:: set_authentication_policy
.. automethod:: set_authorization_policy
.. automethod:: set_default_csrf_options
+ .. automethod:: set_csrf_storage_policy
.. automethod:: set_default_permission
.. automethod:: add_permission
diff --git a/docs/api/csrf.rst b/docs/api/csrf.rst
new file mode 100644
index 000000000..38501546e
--- /dev/null
+++ b/docs/api/csrf.rst
@@ -0,0 +1,23 @@
+.. _csrf_module:
+
+:mod:`pyramid.csrf`
+-------------------
+
+.. automodule:: pyramid.csrf
+
+ .. autoclass:: LegacySessionCSRFStoragePolicy
+ :members:
+
+ .. autoclass:: SessionCSRFStoragePolicy
+ :members:
+
+ .. autoclass:: CookieCSRFStoragePolicy
+ :members:
+
+ .. autofunction:: get_csrf_token
+
+ .. autofunction:: new_csrf_token
+
+ .. autofunction:: check_csrf_origin
+
+ .. autofunction:: check_csrf_token
diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst
index a212ba7a9..e542a6be0 100644
--- a/docs/api/interfaces.rst
+++ b/docs/api/interfaces.rst
@@ -44,6 +44,9 @@ Other Interfaces
.. autointerface:: IRoutePregenerator
:members:
+ .. autointerface:: ICSRFStoragePolicy
+ :members:
+
.. autointerface:: ISession
:members:
diff --git a/docs/api/session.rst b/docs/api/session.rst
index 56c4f52d7..53bae7c52 100644
--- a/docs/api/session.rst
+++ b/docs/api/session.rst
@@ -9,10 +9,6 @@
.. autofunction:: signed_deserialize
- .. autofunction:: check_csrf_origin
-
- .. autofunction:: check_csrf_token
-
.. autofunction:: SignedCookieSessionFactory
.. autofunction:: UnencryptedCookieSessionFactoryConfig
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 8f7ea70a1..2e5276554 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -899,6 +899,11 @@ Glossary
:meth:`pyramid.config.Configurator.set_session_factory` for more
information.
+ CSRF storage policy
+ A utility that implements :class:`pyramid.interfaces.ICSRFStoragePolicy`
+ which is responsible for allocating CSRF tokens to a user and verifying
+ that a provided token is acceptable.
+
Mako
`Mako <http://www.makotemplates.org/>`_ is a template language
which refines the familiar ideas of componentized layout and inheritance
diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst
index 63279027a..81c8e81d7 100644
--- a/docs/narr/hooks.rst
+++ b/docs/narr/hooks.rst
@@ -1569,7 +1569,7 @@ event type.
def __call__(self, event):
return event.request.path.startswith(self.val)
-Once you've created a subscriber predicate, it may registered via
+Once you've created a subscriber predicate, it may be registered via
:meth:`pyramid.config.Configurator.add_subscriber_predicate`. For example:
.. code-block:: python
diff --git a/docs/narr/logging.rst b/docs/narr/logging.rst
index 87682158b..9cc5b4ed8 100644
--- a/docs/narr/logging.rst
+++ b/docs/narr/logging.rst
@@ -16,7 +16,7 @@ to send log messages to loggers that you've configured.
cookiecutter which does not create these files, the configuration information in
this chapter may not be applicable.
-.. index:
+.. index::
pair: settings; logging
pair: .ini; logging
pair: logging; configuration
diff --git a/docs/narr/myproject/README.txt b/docs/narr/myproject/README.txt
index 41ef0ff91..2ffc0acba 100644
--- a/docs/narr/myproject/README.txt
+++ b/docs/narr/myproject/README.txt
@@ -1,5 +1,5 @@
MyProject
-===============================
+=========
Getting Started
---------------
diff --git a/docs/narr/project.rst b/docs/narr/project.rst
index ce7e90793..9c44d4f16 100644
--- a/docs/narr/project.rst
+++ b/docs/narr/project.rst
@@ -94,7 +94,7 @@ If prompted for the first item, accept the default ``yes`` by hitting return.
You've cloned ~/.cookiecutters/pyramid-cookiecutter-starter before.
Is it okay to delete and re-clone it? [yes]: yes
project_name [Pyramid Scaffold]: myproject
- repo_name [scaffold]: myproject
+ repo_name [myproject]: myproject
Select template_language:
1 - jinja2
2 - chameleon
diff --git a/docs/narr/security.rst b/docs/narr/security.rst
index 77e7fd707..3a6bfa5e5 100644
--- a/docs/narr/security.rst
+++ b/docs/narr/security.rst
@@ -146,7 +146,7 @@ For example, the following view declaration protects the view named
# config is an instance of pyramid.config.Configurator
config.add_view('mypackage.views.blog_entry_add_view',
- name='add_entry.html',
+ name='add_entry.html',
context='mypackage.resources.Blog',
permission='add')
@@ -725,7 +725,7 @@ object that implements the following interface:
""" Return ``True`` if any of the ``principals`` is allowed the
``permission`` in the current ``context``, else return ``False``
"""
-
+
def principals_allowed_by_permission(self, context, permission):
""" Return a set of principal identifiers allowed by the
``permission`` in ``context``. This behavior is optional; if you
@@ -765,3 +765,215 @@ which would allow the attacker to control the content of the payload. Re-using
a secret across two different subsystems might drop the security of signing to
zero. Keys should not be re-used across different contexts where an attacker
has the possibility of providing a chosen plaintext.
+
+.. index::
+ single: preventing cross-site request forgery attacks
+ single: cross-site request forgery attacks, prevention
+
+Preventing Cross-Site Request Forgery Attacks
+---------------------------------------------
+
+`Cross-site request forgery
+<https://en.wikipedia.org/wiki/Cross-site_request_forgery>`_ attacks are a
+phenomenon whereby a user who is logged in to your website might inadvertantly
+load a URL because it is linked from, or embedded in, an attacker's website.
+If the URL is one that may modify or delete data, the consequences can be dire.
+
+You can avoid most of these attacks by issuing a unique token to the browser
+and then requiring that it be present in all potentially unsafe requests.
+:app:`Pyramid` provides facilities to create and check CSRF tokens.
+
+By default :app:`Pyramid` comes with a session-based CSRF implementation
+:class:`pyramid.csrf.SessionCSRFStoragePolicy`. To use it, you must first enable
+a :term:`session factory` as described in
+:ref:`using_the_default_session_factory` or
+:ref:`using_alternate_session_factories`. Alternatively, you can use
+a cookie-based implementation :class:`pyramid.csrf.CookieCSRFStoragePolicy` which gives
+some additional flexibility as it does not require a session for each user.
+You can also define your own implementation of
+:class:`pyramid.interfaces.ICSRFStoragePolicy` and register it with the
+:meth:`pyramid.config.Configurator.set_csrf_storage_policy` directive.
+
+For example:
+
+.. code-block:: python
+
+ from pyramid.config import Configurator
+
+ config = Configurator()
+ config.set_csrf_storage_policy(MyCustomCSRFPolicy())
+
+.. index::
+ single: csrf.get_csrf_token
+
+Using the ``csrf.get_csrf_token`` Method
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To get the current CSRF token, use the
+:data:`pyramid.csrf.get_csrf_token` method.
+
+.. code-block:: python
+
+ from pyramid.csrf import get_csrf_token
+ token = get_csrf_token(request)
+
+The ``get_csrf_token()`` method accepts a single argument: the request. It
+returns a CSRF *token* string. If ``get_csrf_token()`` or ``new_csrf_token()``
+was invoked previously for this user, then the existing token will be returned.
+If no CSRF token previously existed for this user, then a new token will be set
+into the session and returned. The newly created token will be opaque and
+randomized.
+
+.. _get_csrf_token_in_templates:
+
+Using the ``get_csrf_token`` global in templates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Templates have a ``get_csrf_token()`` method inserted into their globals, which
+allows you to get the current token without modifying the view code. This
+method takes no arguments and returns a CSRF token string. You can use the
+returned token as the value of a hidden field in a form that posts to a method
+that requires elevated privileges, or supply it as a request header in AJAX
+requests.
+
+For example, include the CSRF token as a hidden field:
+
+.. code-block:: html
+
+ <form method="post" action="/myview">
+ <input type="hidden" name="csrf_token" value="${get_csrf_token()}">
+ <input type="submit" value="Delete Everything">
+ </form>
+
+Or include it as a header in a jQuery AJAX request:
+
+.. code-block:: javascript
+
+ var csrfToken = "${get_csrf_token()}";
+ $.ajax({
+ type: "POST",
+ url: "/myview",
+ headers: { 'X-CSRF-Token': csrfToken }
+ }).done(function() {
+ alert("Deleted");
+ });
+
+The handler for the URL that receives the request should then require that the
+correct CSRF token is supplied.
+
+.. index::
+ single: csrf.new_csrf_token
+
+Using the ``csrf.new_csrf_token`` Method
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To explicitly create a new CSRF token, use the ``csrf.new_csrf_token()``
+method. This differs only from ``csrf.get_csrf_token()`` inasmuch as it
+clears any existing CSRF token, creates a new CSRF token, sets the token into
+the user, and returns the token.
+
+.. code-block:: python
+
+ from pyramid.csrf import get_csrf_token
+ token = new_csrf_token()
+
+.. note::
+
+ It is not possible to force a new CSRF token from a template. If you
+ want to regenerate your CSRF token then do it in the view code and return
+ the new token as part of the context.
+
+Checking CSRF Tokens Manually
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In request handling code, you can check the presence and validity of a CSRF
+token with :func:`pyramid.csrf.check_csrf_token`. If the token is valid, it
+will return ``True``, otherwise it will raise ``HTTPBadRequest``. Optionally,
+you can specify ``raises=False`` to have the check return ``False`` instead of
+raising an exception.
+
+By default, it checks for a POST parameter named ``csrf_token`` or a header
+named ``X-CSRF-Token``.
+
+.. code-block:: python
+
+ from pyramid.csrf import check_csrf_token
+
+ def myview(request):
+ # Require CSRF Token
+ check_csrf_token(request)
+
+ # ...
+
+.. _auto_csrf_checking:
+
+Checking CSRF Tokens Automatically
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.7
+
+:app:`Pyramid` supports automatically checking CSRF tokens on requests with an
+unsafe method as defined by RFC2616. Any other request may be checked manually.
+This feature can be turned on globally for an application using the
+:meth:`pyramid.config.Configurator.set_default_csrf_options` directive.
+For example:
+
+.. code-block:: python
+
+ from pyramid.config import Configurator
+
+ config = Configurator()
+ config.set_default_csrf_options(require_csrf=True)
+
+CSRF checking may be explicitly enabled or disabled on a per-view basis using
+the ``require_csrf`` view option. A value of ``True`` or ``False`` will
+override the default set by ``set_default_csrf_options``. For example:
+
+.. code-block:: python
+
+ @view_config(route_name='hello', require_csrf=False)
+ def myview(request):
+ # ...
+
+When CSRF checking is active, the token and header used to find the
+supplied CSRF token will be ``csrf_token`` and ``X-CSRF-Token``, respectively,
+unless otherwise overridden by ``set_default_csrf_options``. The token is
+checked against the value in ``request.POST`` which is the submitted form body.
+If this value is not present, then the header will be checked.
+
+In addition to token based CSRF checks, if the request is using HTTPS then the
+automatic CSRF checking will also check the referrer of the request to ensure
+that it matches one of the trusted origins. By default the only trusted origin
+is the current host, however additional origins may be configured by setting
+``pyramid.csrf_trusted_origins`` to a list of domain names (and ports if they
+are non-standard). If a host in the list of domains starts with a ``.`` then
+that will allow all subdomains as well as the domain without the ``.``.
+
+If CSRF checks fail then a :class:`pyramid.exceptions.BadCSRFToken` or
+:class:`pyramid.exceptions.BadCSRFOrigin` exception will be raised. This
+exception may be caught and handled by an :term:`exception view` but, by
+default, will result in a ``400 Bad Request`` response being sent to the
+client.
+
+Checking CSRF Tokens with a View Predicate
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 1.7
+ Use the ``require_csrf`` option or read :ref:`auto_csrf_checking` instead
+ to have :class:`pyramid.exceptions.BadCSRFToken` exceptions raised.
+
+A convenient way to require a valid CSRF token for a particular view is to
+include ``check_csrf=True`` as a view predicate. See
+:meth:`pyramid.config.Configurator.add_view`.
+
+.. code-block:: python
+
+ @view_config(request_method='POST', check_csrf=True, ...)
+ def myview(request):
+ ...
+
+.. note::
+ A mismatch of a CSRF token is treated like any other predicate miss, and the
+ predicate system, when it doesn't find a view, raises ``HTTPNotFound``
+ instead of ``HTTPBadRequest``, so ``check_csrf=True`` behavior is different
+ from calling :func:`pyramid.csrf.check_csrf_token`.
diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst
index 5b24201a9..7e2469d54 100644
--- a/docs/narr/sessions.rst
+++ b/docs/narr/sessions.rst
@@ -12,8 +12,7 @@ application.
This chapter describes how to configure sessions, what session implementations
:app:`Pyramid` provides out of the box, how to store and retrieve data from
-sessions, and two session-specific features: flash messages, and cross-site
-request forgery attack prevention.
+sessions, and a session-specific feature: flash messages.
.. index::
single: session factory (default)
@@ -316,183 +315,3 @@ flash storage.
['info message']
>>> request.session.peek_flash()
[]
-
-.. index::
- single: preventing cross-site request forgery attacks
- single: cross-site request forgery attacks, prevention
-
-Preventing Cross-Site Request Forgery Attacks
----------------------------------------------
-
-`Cross-site request forgery
-<https://en.wikipedia.org/wiki/Cross-site_request_forgery>`_ attacks are a
-phenomenon whereby a user who is logged in to your website might inadvertantly
-load a URL because it is linked from, or embedded in, an attacker's website.
-If the URL is one that may modify or delete data, the consequences can be dire.
-
-You can avoid most of these attacks by issuing a unique token to the browser
-and then requiring that it be present in all potentially unsafe requests.
-:app:`Pyramid` sessions provide facilities to create and check CSRF tokens.
-
-To use CSRF tokens, you must first enable a :term:`session factory` as
-described in :ref:`using_the_default_session_factory` or
-:ref:`using_alternate_session_factories`.
-
-.. index::
- single: session.get_csrf_token
-
-Using the ``session.get_csrf_token`` Method
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-To get the current CSRF token from the session, use the
-``session.get_csrf_token()`` method.
-
-.. code-block:: python
-
- token = request.session.get_csrf_token()
-
-The ``session.get_csrf_token()`` method accepts no arguments. It returns a
-CSRF *token* string. If ``session.get_csrf_token()`` or
-``session.new_csrf_token()`` was invoked previously for this session, then the
-existing token will be returned. If no CSRF token previously existed for this
-session, then a new token will be set into the session and returned. The newly
-created token will be opaque and randomized.
-
-You can use the returned token as the value of a hidden field in a form that
-posts to a method that requires elevated privileges, or supply it as a request
-header in AJAX requests.
-
-For example, include the CSRF token as a hidden field:
-
-.. code-block:: html
-
- <form method="post" action="/myview">
- <input type="hidden" name="csrf_token" value="${request.session.get_csrf_token()}">
- <input type="submit" value="Delete Everything">
- </form>
-
-Or include it as a header in a jQuery AJAX request:
-
-.. code-block:: javascript
-
- var csrfToken = ${request.session.get_csrf_token()};
- $.ajax({
- type: "POST",
- url: "/myview",
- headers: { 'X-CSRF-Token': csrfToken }
- }).done(function() {
- alert("Deleted");
- });
-
-The handler for the URL that receives the request should then require that the
-correct CSRF token is supplied.
-
-.. index::
- single: session.new_csrf_token
-
-Using the ``session.new_csrf_token`` Method
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-To explicitly create a new CSRF token, use the ``session.new_csrf_token()``
-method. This differs only from ``session.get_csrf_token()`` inasmuch as it
-clears any existing CSRF token, creates a new CSRF token, sets the token into
-the session, and returns the token.
-
-.. code-block:: python
-
- token = request.session.new_csrf_token()
-
-Checking CSRF Tokens Manually
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-In request handling code, you can check the presence and validity of a CSRF
-token with :func:`pyramid.session.check_csrf_token`. If the token is valid, it
-will return ``True``, otherwise it will raise ``HTTPBadRequest``. Optionally,
-you can specify ``raises=False`` to have the check return ``False`` instead of
-raising an exception.
-
-By default, it checks for a POST parameter named ``csrf_token`` or a header
-named ``X-CSRF-Token``.
-
-.. code-block:: python
-
- from pyramid.session import check_csrf_token
-
- def myview(request):
- # Require CSRF Token
- check_csrf_token(request)
-
- # ...
-
-.. _auto_csrf_checking:
-
-Checking CSRF Tokens Automatically
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. versionadded:: 1.7
-
-:app:`Pyramid` supports automatically checking CSRF tokens on requests with an
-unsafe method as defined by RFC2616. Any other request may be checked manually.
-This feature can be turned on globally for an application using the
-:meth:`pyramid.config.Configurator.set_default_csrf_options` directive.
-For example:
-
-.. code-block:: python
-
- from pyramid.config import Configurator
-
- config = Configurator()
- config.set_default_csrf_options(require_csrf=True)
-
-CSRF checking may be explicitly enabled or disabled on a per-view basis using
-the ``require_csrf`` view option. A value of ``True`` or ``False`` will
-override the default set by ``set_default_csrf_options``. For example:
-
-.. code-block:: python
-
- @view_config(route_name='hello', require_csrf=False)
- def myview(request):
- # ...
-
-When CSRF checking is active, the token and header used to find the
-supplied CSRF token will be ``csrf_token`` and ``X-CSRF-Token``, respectively,
-unless otherwise overridden by ``set_default_csrf_options``. The token is
-checked against the value in ``request.POST`` which is the submitted form body.
-If this value is not present, then the header will be checked.
-
-In addition to token based CSRF checks, if the request is using HTTPS then the
-automatic CSRF checking will also check the referrer of the request to ensure
-that it matches one of the trusted origins. By default the only trusted origin
-is the current host, however additional origins may be configured by setting
-``pyramid.csrf_trusted_origins`` to a list of domain names (and ports if they
-are non standard). If a host in the list of domains starts with a ``.`` then
-that will allow all subdomains as well as the domain without the ``.``.
-
-If CSRF checks fail then a :class:`pyramid.exceptions.BadCSRFToken` or
-:class:`pyramid.exceptions.BadCSRFOrigin` exception will be raised. This
-exception may be caught and handled by an :term:`exception view` but, by
-default, will result in a ``400 Bad Request`` response being sent to the
-client.
-
-Checking CSRF Tokens with a View Predicate
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. deprecated:: 1.7
- Use the ``require_csrf`` option or read :ref:`auto_csrf_checking` instead
- to have :class:`pyramid.exceptions.BadCSRFToken` exceptions raised.
-
-A convenient way to require a valid CSRF token for a particular view is to
-include ``check_csrf=True`` as a view predicate. See
-:meth:`pyramid.config.Configurator.add_view`.
-
-.. code-block:: python
-
- @view_config(request_method='POST', check_csrf=True, ...)
- def myview(request):
- ...
-
-.. note::
- A mismatch of a CSRF token is treated like any other predicate miss, and the
- predicate system, when it doesn't find a view, raises ``HTTPNotFound``
- instead of ``HTTPBadRequest``, so ``check_csrf=True`` behavior is different
- from calling :func:`pyramid.session.check_csrf_token`.
diff --git a/docs/narr/templates.rst b/docs/narr/templates.rst
index 6b3b5fcce..4eadbd2f0 100644
--- a/docs/narr/templates.rst
+++ b/docs/narr/templates.rst
@@ -228,6 +228,10 @@ These values are provided to the template:
provided if the template is rendered as the result of a ``renderer=``
argument to the view configuration being used.
+``get_csrf_token()``
+ A convenience function to access the current CSRF token. See
+ :ref:`get_csrf_token_in_templates` for more information.
+
``renderer_name``
The renderer name used to perform the rendering, e.g.,
``mypackage:templates/foo.pt``.
diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst
index 02c3ff811..571dfb356 100644
--- a/docs/quick_tour.rst
+++ b/docs/quick_tour.rst
@@ -519,7 +519,7 @@ If prompted for the first item, accept the default ``yes`` by hitting return.
You've cloned ~/.cookiecutters/pyramid-cookiecutter-starter before.
Is it okay to delete and re-clone it? [yes]: yes
project_name [Pyramid Scaffold]: hello_world
- repo_name [scaffold]: hello_world
+ repo_name [hello_world]: hello_world
Select template_language:
1 - jinja2
2 - chameleon
@@ -875,7 +875,7 @@ If prompted for the first item, accept the default ``yes`` by hitting return.
You've cloned ~/.cookiecutters/pyramid-cookiecutter-alchemy before.
Is it okay to delete and re-clone it? [yes]: yes
project_name [Pyramid Scaffold]: sqla_demo
- repo_name [scaffold]: sqla_demo
+ repo_name [sqla_demo]: sqla_demo
We then run through the following commands as before.
diff --git a/docs/quick_tour/logging/README.txt b/docs/quick_tour/logging/README.txt
index fb7bde0a7..ff70a1354 100644
--- a/docs/quick_tour/logging/README.txt
+++ b/docs/quick_tour/logging/README.txt
@@ -1,5 +1,5 @@
hello_world
-===============================
+===========
Getting Started
---------------
diff --git a/docs/quick_tour/package/README.txt b/docs/quick_tour/package/README.txt
index fb7bde0a7..ff70a1354 100644
--- a/docs/quick_tour/package/README.txt
+++ b/docs/quick_tour/package/README.txt
@@ -1,5 +1,5 @@
hello_world
-===============================
+===========
Getting Started
---------------
diff --git a/docs/quick_tour/sessions/README.txt b/docs/quick_tour/sessions/README.txt
index fb7bde0a7..ff70a1354 100644
--- a/docs/quick_tour/sessions/README.txt
+++ b/docs/quick_tour/sessions/README.txt
@@ -1,5 +1,5 @@
hello_world
-===============================
+===========
Getting Started
---------------
diff --git a/docs/quick_tour/sqla_demo/README.txt b/docs/quick_tour/sqla_demo/README.txt
index 1659e47ab..27bbff5a7 100644
--- a/docs/quick_tour/sqla_demo/README.txt
+++ b/docs/quick_tour/sqla_demo/README.txt
@@ -1,5 +1,5 @@
sqla_demo
-===============================
+=========
Getting Started
---------------
diff --git a/docs/quick_tutorial/cookiecutters.rst b/docs/quick_tutorial/cookiecutters.rst
index edfd8cd69..337a5c535 100644
--- a/docs/quick_tutorial/cookiecutters.rst
+++ b/docs/quick_tutorial/cookiecutters.rst
@@ -37,7 +37,7 @@ Steps
You've cloned ~/.cookiecutters/pyramid-cookiecutter-starter before.
Is it okay to delete and re-clone it? [yes]: yes
project_name [Pyramid Scaffold]: cc_starter
- repo_name [scaffold]: cc_starter
+ repo_name [cc_starter]: cc_starter
Select template_language:
1 - jinja2
2 - chameleon
diff --git a/docs/quick_tutorial/cookiecutters/README.txt b/docs/quick_tutorial/cookiecutters/README.txt
index 4b1f31bf3..55c5dcec6 100644
--- a/docs/quick_tutorial/cookiecutters/README.txt
+++ b/docs/quick_tutorial/cookiecutters/README.txt
@@ -1,5 +1,5 @@
cc_starter
-===============================
+==========
Getting Started
---------------
diff --git a/docs/tutorials/modwsgi/index.rst b/docs/tutorials/modwsgi/index.rst
index 690266586..170f2ebc8 100644
--- a/docs/tutorials/modwsgi/index.rst
+++ b/docs/tutorials/modwsgi/index.rst
@@ -48,7 +48,7 @@ specific path information for commands and files.
You've cloned ~/.cookiecutters/pyramid-cookiecutter-starter before.
Is it okay to delete and re-clone it? [yes]: yes
project_name [Pyramid Scaffold]: myproject
- repo_name [scaffold]: myproject
+ repo_name [myproject]: myproject
Select template_language:
1 - jinja2
2 - chameleon
diff --git a/docs/tutorials/wiki/installation.rst b/docs/tutorials/wiki/installation.rst
index 6be826395..de057b1cc 100644
--- a/docs/tutorials/wiki/installation.rst
+++ b/docs/tutorials/wiki/installation.rst
@@ -50,7 +50,7 @@ If prompted for the first item, accept the default ``yes`` by hitting return.
You've cloned ~/.cookiecutters/pyramid-cookiecutter-zodb before.
Is it okay to delete and re-clone it? [yes]: yes
project_name [Pyramid Scaffold]: myproj
- repo_name [scaffold]: tutorial
+ repo_name [myproj]: tutorial
Change directory into your newly created project
------------------------------------------------
diff --git a/docs/tutorials/wiki/src/authorization/README.txt b/docs/tutorials/wiki/src/authorization/README.txt
index 98683bf8c..5ec53bf9d 100644
--- a/docs/tutorials/wiki/src/authorization/README.txt
+++ b/docs/tutorials/wiki/src/authorization/README.txt
@@ -1,5 +1,5 @@
myproj
-===============================
+======
Getting Started
---------------
diff --git a/docs/tutorials/wiki/src/basiclayout/README.txt b/docs/tutorials/wiki/src/basiclayout/README.txt
index 98683bf8c..5ec53bf9d 100644
--- a/docs/tutorials/wiki/src/basiclayout/README.txt
+++ b/docs/tutorials/wiki/src/basiclayout/README.txt
@@ -1,5 +1,5 @@
myproj
-===============================
+======
Getting Started
---------------
diff --git a/docs/tutorials/wiki/src/installation/README.txt b/docs/tutorials/wiki/src/installation/README.txt
index 98683bf8c..5ec53bf9d 100644
--- a/docs/tutorials/wiki/src/installation/README.txt
+++ b/docs/tutorials/wiki/src/installation/README.txt
@@ -1,5 +1,5 @@
myproj
-===============================
+======
Getting Started
---------------
diff --git a/docs/tutorials/wiki/src/models/README.txt b/docs/tutorials/wiki/src/models/README.txt
index 98683bf8c..5ec53bf9d 100644
--- a/docs/tutorials/wiki/src/models/README.txt
+++ b/docs/tutorials/wiki/src/models/README.txt
@@ -1,5 +1,5 @@
myproj
-===============================
+======
Getting Started
---------------
diff --git a/docs/tutorials/wiki/src/tests/README.txt b/docs/tutorials/wiki/src/tests/README.txt
index 98683bf8c..5ec53bf9d 100644
--- a/docs/tutorials/wiki/src/tests/README.txt
+++ b/docs/tutorials/wiki/src/tests/README.txt
@@ -1,5 +1,5 @@
myproj
-===============================
+======
Getting Started
---------------
diff --git a/docs/tutorials/wiki/src/views/README.txt b/docs/tutorials/wiki/src/views/README.txt
index 98683bf8c..5ec53bf9d 100644
--- a/docs/tutorials/wiki/src/views/README.txt
+++ b/docs/tutorials/wiki/src/views/README.txt
@@ -1,5 +1,5 @@
myproj
-===============================
+======
Getting Started
---------------
diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst
index 9eeb1711d..c61d4360d 100644
--- a/docs/tutorials/wiki2/installation.rst
+++ b/docs/tutorials/wiki2/installation.rst
@@ -62,7 +62,7 @@ If prompted for the first item, accept the default ``yes`` by hitting return.
You've cloned ~/.cookiecutters/pyramid-cookiecutter-alchemy before.
Is it okay to delete and re-clone it? [yes]: yes
project_name [Pyramid Scaffold]: myproj
- repo_name [scaffold]: tutorial
+ repo_name [myproj]: tutorial
Change directory into your newly created project
------------------------------------------------
diff --git a/docs/tutorials/wiki2/src/authentication/README.txt b/docs/tutorials/wiki2/src/authentication/README.txt
index 5e21b8aa4..81102a869 100644
--- a/docs/tutorials/wiki2/src/authentication/README.txt
+++ b/docs/tutorials/wiki2/src/authentication/README.txt
@@ -1,5 +1,5 @@
myproj
-===============================
+======
Getting Started
---------------
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py
index 1b071434c..2d058d874 100644
--- a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py
@@ -1,4 +1,4 @@
-import cgi
+from pyramid.compat import escape
import re
from docutils.core import publish_parts
@@ -32,10 +32,10 @@ def view_page(request):
exists = request.dbsession.query(Page).filter_by(name=word).all()
if exists:
view_url = request.route_url('view_page', pagename=word)
- return '<a href="%s">%s</a>' % (view_url, cgi.escape(word))
+ return '<a href="%s">%s</a>' % (view_url, escape(word))
else:
add_url = request.route_url('add_page', pagename=word)
- return '<a href="%s">%s</a>' % (add_url, cgi.escape(word))
+ return '<a href="%s">%s</a>' % (add_url, escape(word))
content = publish_parts(page.data, writer_name='html')['html_body']
content = wikiwords.sub(add_link, content)
diff --git a/docs/tutorials/wiki2/src/authorization/README.txt b/docs/tutorials/wiki2/src/authorization/README.txt
index 5e21b8aa4..81102a869 100644
--- a/docs/tutorials/wiki2/src/authorization/README.txt
+++ b/docs/tutorials/wiki2/src/authorization/README.txt
@@ -1,5 +1,5 @@
myproj
-===============================
+======
Getting Started
---------------
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py
index 9358993ea..65c12ed3b 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py
@@ -1,4 +1,4 @@
-import cgi
+from pyramid.compat import escape
import re
from docutils.core import publish_parts
@@ -25,10 +25,10 @@ def view_page(request):
exists = request.dbsession.query(Page).filter_by(name=word).all()
if exists:
view_url = request.route_url('view_page', pagename=word)
- return '<a href="%s">%s</a>' % (view_url, cgi.escape(word))
+ return '<a href="%s">%s</a>' % (view_url, escape(word))
else:
add_url = request.route_url('add_page', pagename=word)
- return '<a href="%s">%s</a>' % (add_url, cgi.escape(word))
+ return '<a href="%s">%s</a>' % (add_url, escape(word))
content = publish_parts(page.data, writer_name='html')['html_body']
content = wikiwords.sub(add_link, content)
diff --git a/docs/tutorials/wiki2/src/basiclayout/README.txt b/docs/tutorials/wiki2/src/basiclayout/README.txt
index 5e21b8aa4..81102a869 100644
--- a/docs/tutorials/wiki2/src/basiclayout/README.txt
+++ b/docs/tutorials/wiki2/src/basiclayout/README.txt
@@ -1,5 +1,5 @@
myproj
-===============================
+======
Getting Started
---------------
diff --git a/docs/tutorials/wiki2/src/installation/README.txt b/docs/tutorials/wiki2/src/installation/README.txt
index 5e21b8aa4..81102a869 100644
--- a/docs/tutorials/wiki2/src/installation/README.txt
+++ b/docs/tutorials/wiki2/src/installation/README.txt
@@ -1,5 +1,5 @@
myproj
-===============================
+======
Getting Started
---------------
diff --git a/docs/tutorials/wiki2/src/models/README.txt b/docs/tutorials/wiki2/src/models/README.txt
index 5e21b8aa4..81102a869 100644
--- a/docs/tutorials/wiki2/src/models/README.txt
+++ b/docs/tutorials/wiki2/src/models/README.txt
@@ -1,5 +1,5 @@
myproj
-===============================
+======
Getting Started
---------------
diff --git a/docs/tutorials/wiki2/src/tests/README.txt b/docs/tutorials/wiki2/src/tests/README.txt
index 5e21b8aa4..81102a869 100644
--- a/docs/tutorials/wiki2/src/tests/README.txt
+++ b/docs/tutorials/wiki2/src/tests/README.txt
@@ -1,5 +1,5 @@
myproj
-===============================
+======
Getting Started
---------------
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py
index 9358993ea..65c12ed3b 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py
@@ -1,4 +1,4 @@
-import cgi
+from pyramid.compat import escape
import re
from docutils.core import publish_parts
@@ -25,10 +25,10 @@ def view_page(request):
exists = request.dbsession.query(Page).filter_by(name=word).all()
if exists:
view_url = request.route_url('view_page', pagename=word)
- return '<a href="%s">%s</a>' % (view_url, cgi.escape(word))
+ return '<a href="%s">%s</a>' % (view_url, escape(word))
else:
add_url = request.route_url('add_page', pagename=word)
- return '<a href="%s">%s</a>' % (add_url, cgi.escape(word))
+ return '<a href="%s">%s</a>' % (add_url, escape(word))
content = publish_parts(page.data, writer_name='html')['html_body']
content = wikiwords.sub(add_link, content)
diff --git a/docs/tutorials/wiki2/src/views/README.txt b/docs/tutorials/wiki2/src/views/README.txt
index 5e21b8aa4..81102a869 100644
--- a/docs/tutorials/wiki2/src/views/README.txt
+++ b/docs/tutorials/wiki2/src/views/README.txt
@@ -1,5 +1,5 @@
myproj
-===============================
+======
Getting Started
---------------
diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py
index bb6300b75..3b95e0f59 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py
@@ -1,4 +1,4 @@
-import cgi
+from pyramid.compat import escape
import re
from docutils.core import publish_parts
@@ -31,10 +31,10 @@ def view_page(request):
exists = request.dbsession.query(Page).filter_by(name=word).all()
if exists:
view_url = request.route_url('view_page', pagename=word)
- return '<a href="%s">%s</a>' % (view_url, cgi.escape(word))
+ return '<a href="%s">%s</a>' % (view_url, escape(word))
else:
add_url = request.route_url('add_page', pagename=word)
- return '<a href="%s">%s</a>' % (add_url, cgi.escape(word))
+ return '<a href="%s">%s</a>' % (add_url, escape(word))
content = publish_parts(page.data, writer_name='html')['html_body']
content = wikiwords.sub(add_link, content)
diff --git a/docs/whatsnew-1.8.rst b/docs/whatsnew-1.8.rst
index adc60b34b..ff16c1a4b 100644
--- a/docs/whatsnew-1.8.rst
+++ b/docs/whatsnew-1.8.rst
@@ -114,6 +114,13 @@ Minor Feature Additions
later calls to place translation directories at a higher priority then
earlier calls. See https://github.com/Pylons/pyramid/pull/2902
+- Added a new ``callback`` option to
+ :meth:`pyramid.config.Configurator.set_default_csrf_options`` which
+ can be used to determine per-request whether CSRF checking should be enabled
+ to allow for a mix authentication methods. Only cookie-based methods
+ generally require CSRF checking.
+ See https://github.com/Pylons/pyramid/pull/2778
+
Backwards Incompatibilities
---------------------------
diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py
index 304d3a85e..a34f0b4db 100644
--- a/pyramid/config/__init__.py
+++ b/pyramid/config/__init__.py
@@ -110,6 +110,17 @@ class Configurator(
A Configurator is used to configure a :app:`Pyramid`
:term:`application registry`.
+ The Configurator lifecycle can be managed by using a context manager to
+ automatically handle calling :meth:`pyramid.config.Configurator.begin` and
+ :meth:`pyramid.config.Configurator.end` as well as
+ :meth:`pyramid.config.Configurator.commit`.
+
+ .. code-block:: python
+
+ with Configurator(settings=settings) as config:
+ config.add_route('home', '/')
+ app = config.make_wsgi_app()
+
If the ``registry`` argument is not ``None``, it must
be an instance of the :class:`pyramid.registry.Registry` class
representing the registry to configure. If ``registry`` is ``None``, the
@@ -265,6 +276,11 @@ class Configurator(
.. versionadded:: 1.6
The ``root_package`` argument.
The ``response_factory`` argument.
+
+ .. versionadded:: 1.9
+ The ability to use the configurator as a context manager with the
+ ``with``-statement to make threadlocal configuration available for
+ further configuration with an implicit commit.
"""
manager = manager # for testing injection
venusian = venusian # for testing injection
@@ -380,6 +396,7 @@ class Configurator(
self.add_default_view_derivers()
self.add_default_route_predicates()
self.add_default_tweens()
+ self.add_default_security()
if exceptionresponse_view is not None:
exceptionresponse_view = self.maybe_dotted(exceptionresponse_view)
@@ -646,12 +663,22 @@ class Configurator(
_ctx = action_state # bw compat
def commit(self):
- """ Commit any pending configuration actions. If a configuration
+ """
+ Commit any pending configuration actions. If a configuration
conflict is detected in the pending configuration actions, this method
will raise a :exc:`ConfigurationConflictError`; within the traceback
of this error will be information about the source of the conflict,
usually including file names and line numbers of the cause of the
- configuration conflicts."""
+ configuration conflicts.
+
+ .. warning::
+ You should think very carefully before manually invoking
+ ``commit()``. Especially not as part of any reusable configuration
+ methods. Normally it should only be done by an application author at
+ the end of configuration in order to override certain aspects of an
+ addon.
+
+ """
self.begin()
try:
self.action_state.execute_actions(introspector=self.introspector)
@@ -753,6 +780,11 @@ class Configurator(
.. versionadded:: 1.2
The ``route_prefix`` parameter.
+ .. versionchanged:: 1.9
+ The included function is wrapped with a call to
+ :meth:`pyramid.config.Configurator.begin` and
+ :meth:`pyramid.config.Configurator.end` while it is executed.
+
"""
# """ <-- emacs
@@ -802,7 +834,11 @@ class Configurator(
)
configurator.basepath = os.path.dirname(sourcefile)
configurator.includepath = self.includepath + (spec,)
- c(configurator)
+ self.begin()
+ try:
+ c(configurator)
+ finally:
+ self.end()
def add_directive(self, name, directive, action_wrap=True):
"""
@@ -924,6 +960,16 @@ class Configurator(
"""
return self.manager.pop()
+ def __enter__(self):
+ self.begin()
+ return self
+
+ def __exit__(self, exc_type, exc_value, exc_traceback):
+ self.end()
+
+ if exc_value is None:
+ self.commit()
+
# this is *not* an action method (uses caller_package)
def scan(self, package=None, categories=None, onerror=None, ignore=None,
**kw):
diff --git a/pyramid/config/security.py b/pyramid/config/security.py
index 33593376b..20b816161 100644
--- a/pyramid/config/security.py
+++ b/pyramid/config/security.py
@@ -3,17 +3,24 @@ from zope.interface import implementer
from pyramid.interfaces import (
IAuthorizationPolicy,
IAuthenticationPolicy,
+ ICSRFStoragePolicy,
IDefaultCSRFOptions,
IDefaultPermission,
PHASE1_CONFIG,
PHASE2_CONFIG,
)
+from pyramid.csrf import LegacySessionCSRFStoragePolicy
from pyramid.exceptions import ConfigurationError
from pyramid.util import action_method
from pyramid.util import as_sorted_tuple
+
class SecurityConfiguratorMixin(object):
+
+ def add_default_security(self):
+ self.set_csrf_storage_policy(LegacySessionCSRFStoragePolicy())
+
@action_method
def set_authentication_policy(self, policy):
""" Override the :app:`Pyramid` :term:`authentication policy` in the
@@ -200,9 +207,14 @@ class SecurityConfiguratorMixin(object):
are not subject to CSRF attacks. For example, if a request is
authenticated using the ``Authorization`` header instead of a cookie,
this may return ``False`` for that request so that clients do not
- need to send the ``X-CSRF-Token` header. The callback is only tested
+ need to send the ``X-CSRF-Token`` header. The callback is only tested
for non-safe methods as defined by ``safe_methods``.
+ .. versionadded:: 1.7
+
+ .. versionchanged:: 1.8
+ Added the ``callback`` option.
+
"""
options = DefaultCSRFOptions(
require_csrf, token, header, safe_methods, callback,
@@ -218,9 +230,31 @@ class SecurityConfiguratorMixin(object):
intr['header'] = header
intr['safe_methods'] = as_sorted_tuple(safe_methods)
intr['callback'] = callback
+
self.action(IDefaultCSRFOptions, register, order=PHASE1_CONFIG,
introspectables=(intr,))
+ @action_method
+ def set_csrf_storage_policy(self, policy):
+ """
+ Set the :term:`CSRF storage policy` used by subsequent view
+ registrations.
+
+ ``policy`` is a class that implements the
+ :meth:`pyramid.interfaces.ICSRFStoragePolicy` interface and defines
+ how to generate and persist CSRF tokens.
+
+ """
+ def register():
+ self.registry.registerUtility(policy, ICSRFStoragePolicy)
+ intr = self.introspectable('csrf storage policy',
+ None,
+ policy,
+ 'csrf storage policy')
+ intr['policy'] = policy
+ self.action(ICSRFStoragePolicy, register, introspectables=(intr,))
+
+
@implementer(IDefaultCSRFOptions)
class DefaultCSRFOptions(object):
def __init__(self, require_csrf, token, header, safe_methods, callback):
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index 65c9da585..48c4e3437 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -641,18 +641,22 @@ class ViewsConfiguratorMixin(object):
'check name'. If the value provided is ``True``, ``csrf_token`` will
be used as the check name.
- If CSRF checking is performed, the checked value will be the value
- of ``request.params[check_name]``. This value will be compared
- against the value of ``request.session.get_csrf_token()``, and the
- check will pass if these two values are the same. If the check
- passes, the associated view will be permitted to execute. If the
+ If CSRF checking is performed, the checked value will be the value of
+ ``request.params[check_name]``. This value will be compared against
+ the value of ``policy.get_csrf_token()`` (where ``policy`` is an
+ implementation of :meth:`pyramid.interfaces.ICSRFStoragePolicy`), and the
+ check will pass if these two values are the same. If the check
+ passes, the associated view will be permitted to execute. If the
check fails, the associated view will not be permitted to execute.
- Note that using this feature requires a :term:`session factory` to
- have been configured.
-
.. versionadded:: 1.4a2
+ .. versionchanged:: 1.9
+ This feature requires either a :term:`session factory` to have been
+ configured, or a :term:`CSRF storage policy` other than the default
+ to be in use.
+
+
physical_path
If specified, this value should be a string or a tuple representing
@@ -972,7 +976,7 @@ class ViewsConfiguratorMixin(object):
def register_view(classifier, request_iface, derived_view):
# A multiviews is a set of views which are registered for
# exactly the same context type/request type/name triad. Each
- # consituent view in a multiview differs only by the
+ # constituent view in a multiview differs only by the
# predicates which it possesses.
# To find a previously registered view for a context
@@ -1032,7 +1036,7 @@ class ViewsConfiguratorMixin(object):
# XXX we could try to be more efficient here and register
# a non-secured view for a multiview if none of the
- # multiview's consituent views have a permission
+ # multiview's constituent views have a permission
# associated with them, but this code is getting pretty
# rough already
if is_multiview:
diff --git a/pyramid/csrf.py b/pyramid/csrf.py
new file mode 100644
index 000000000..7c836e5ad
--- /dev/null
+++ b/pyramid/csrf.py
@@ -0,0 +1,332 @@
+import uuid
+
+from webob.cookies import CookieProfile
+from zope.interface import implementer
+
+
+from pyramid.authentication import _SimpleSerializer
+
+from pyramid.compat import (
+ bytes_,
+ urlparse,
+ text_,
+)
+from pyramid.exceptions import (
+ BadCSRFOrigin,
+ BadCSRFToken,
+)
+from pyramid.interfaces import ICSRFStoragePolicy
+from pyramid.settings import aslist
+from pyramid.util import (
+ is_same_domain,
+ strings_differ
+)
+
+
+@implementer(ICSRFStoragePolicy)
+class LegacySessionCSRFStoragePolicy(object):
+ """ A CSRF storage policy that defers control of CSRF storage to the
+ session.
+
+ This policy maintains compatibility with legacy ISession implementations
+ that know how to manage CSRF tokens themselves via
+ ``ISession.new_csrf_token`` and ``ISession.get_csrf_token``.
+
+ Note that using this CSRF implementation requires that
+ a :term:`session factory` is configured.
+
+ .. versionadded:: 1.9
+
+ """
+ def new_csrf_token(self, request):
+ """ Sets a new CSRF token into the session and returns it. """
+ return request.session.new_csrf_token()
+
+ def get_csrf_token(self, request):
+ """ Returns the currently active CSRF token from the session,
+ generating a new one if needed."""
+ return request.session.get_csrf_token()
+
+ def check_csrf_token(self, request, supplied_token):
+ """ Returns ``True`` if the ``supplied_token`` is valid."""
+ expected_token = self.get_csrf_token(request)
+ return not strings_differ(
+ bytes_(expected_token), bytes_(supplied_token))
+
+
+@implementer(ICSRFStoragePolicy)
+class SessionCSRFStoragePolicy(object):
+ """ A CSRF storage policy that persists the CSRF token in the session.
+
+ Note that using this CSRF implementation requires that
+ a :term:`session factory` is configured.
+
+ ``key``
+
+ The session key where the CSRF token will be stored.
+ Default: `_csrft_`.
+
+ .. versionadded:: 1.9
+
+ """
+ _token_factory = staticmethod(lambda: text_(uuid.uuid4().hex))
+
+ def __init__(self, key='_csrft_'):
+ self.key = key
+
+ def new_csrf_token(self, request):
+ """ Sets a new CSRF token into the session and returns it. """
+ token = self._token_factory()
+ request.session[self.key] = token
+ return token
+
+ def get_csrf_token(self, request):
+ """ Returns the currently active CSRF token from the session,
+ generating a new one if needed."""
+ token = request.session.get(self.key, None)
+ if not token:
+ token = self.new_csrf_token(request)
+ return token
+
+ def check_csrf_token(self, request, supplied_token):
+ """ Returns ``True`` if the ``supplied_token`` is valid."""
+ expected_token = self.get_csrf_token(request)
+ return not strings_differ(
+ bytes_(expected_token), bytes_(supplied_token))
+
+
+@implementer(ICSRFStoragePolicy)
+class CookieCSRFStoragePolicy(object):
+ """ An alternative CSRF implementation that stores its information in
+ unauthenticated cookies, known as the 'Double Submit Cookie' method in the
+ `OWASP CSRF guidelines <https://www.owasp.org/index.php/
+ Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#
+ Double_Submit_Cookie>`_. This gives some additional flexibility with
+ regards to scaling as the tokens can be generated and verified by a
+ front-end server.
+
+ .. versionadded:: 1.9
+
+ """
+ _token_factory = staticmethod(lambda: text_(uuid.uuid4().hex))
+
+ def __init__(self, cookie_name='csrf_token', secure=False, httponly=False,
+ domain=None, max_age=None, path='/'):
+ serializer = _SimpleSerializer()
+ self.cookie_profile = CookieProfile(
+ cookie_name=cookie_name,
+ secure=secure,
+ max_age=max_age,
+ httponly=httponly,
+ path=path,
+ domains=[domain],
+ serializer=serializer
+ )
+ self.cookie_name = cookie_name
+
+ def new_csrf_token(self, request):
+ """ Sets a new CSRF token into the request and returns it. """
+ token = self._token_factory()
+ request.cookies[self.cookie_name] = token
+ def set_cookie(request, response):
+ self.cookie_profile.set_cookies(
+ response,
+ token,
+ )
+ request.add_response_callback(set_cookie)
+ return token
+
+ def get_csrf_token(self, request):
+ """ Returns the currently active CSRF token by checking the cookies
+ sent with the current request."""
+ bound_cookies = self.cookie_profile.bind(request)
+ token = bound_cookies.get_value()
+ if not token:
+ token = self.new_csrf_token(request)
+ return token
+
+ def check_csrf_token(self, request, supplied_token):
+ """ Returns ``True`` if the ``supplied_token`` is valid."""
+ expected_token = self.get_csrf_token(request)
+ return not strings_differ(
+ bytes_(expected_token), bytes_(supplied_token))
+
+
+def get_csrf_token(request):
+ """ Get the currently active CSRF token for the request passed, generating
+ a new one using ``new_csrf_token(request)`` if one does not exist. This
+ calls the equivalent method in the chosen CSRF protection implementation.
+
+ .. versionadded :: 1.9
+
+ """
+ registry = request.registry
+ csrf = registry.getUtility(ICSRFStoragePolicy)
+ return csrf.get_csrf_token(request)
+
+
+def new_csrf_token(request):
+ """ Generate a new CSRF token for the request passed and persist it in an
+ implementation defined manner. This calls the equivalent method in the
+ chosen CSRF protection implementation.
+
+ .. versionadded :: 1.9
+
+ """
+ registry = request.registry
+ csrf = registry.getUtility(ICSRFStoragePolicy)
+ return csrf.new_csrf_token(request)
+
+
+def check_csrf_token(request,
+ token='csrf_token',
+ header='X-CSRF-Token',
+ raises=True):
+ """ Check the CSRF token returned by the
+ :class:`pyramid.interfaces.ICSRFStoragePolicy` implementation against the
+ value in ``request.POST.get(token)`` (if a POST request) or
+ ``request.headers.get(header)``. If a ``token`` keyword is not supplied to
+ this function, the string ``csrf_token`` will be used to look up the token
+ in ``request.POST``. If a ``header`` keyword is not supplied to this
+ function, the string ``X-CSRF-Token`` will be used to look up the token in
+ ``request.headers``.
+
+ If the value supplied by post or by header cannot be verified by the
+ :class:`pyramid.interfaces.ICSRFStoragePolicy`, and ``raises`` is
+ ``True``, this function will raise an
+ :exc:`pyramid.exceptions.BadCSRFToken` exception. If the values differ
+ and ``raises`` is ``False``, this function will return ``False``. If the
+ CSRF check is successful, this function will return ``True``
+ unconditionally.
+
+ See :ref:`auto_csrf_checking` for information about how to secure your
+ application automatically against CSRF attacks.
+
+ .. versionadded:: 1.4a2
+
+ .. versionchanged:: 1.7a1
+ A CSRF token passed in the query string of the request is no longer
+ considered valid. It must be passed in either the request body or
+ a header.
+
+ .. versionchanged:: 1.9
+ Moved from :mod:`pyramid.session` to :mod:`pyramid.csrf` and updated
+ to use the configured :class:`pyramid.interfaces.ICSRFStoragePolicy` to
+ verify the CSRF token.
+
+ """
+ supplied_token = ""
+ # We first check the headers for a csrf token, as that is significantly
+ # cheaper than checking the POST body
+ if header is not None:
+ supplied_token = request.headers.get(header, "")
+
+ # If this is a POST/PUT/etc request, then we'll check the body to see if it
+ # has a token. We explicitly use request.POST here because CSRF tokens
+ # should never appear in an URL as doing so is a security issue. We also
+ # explicitly check for request.POST here as we do not support sending form
+ # encoded data over anything but a request.POST.
+ if supplied_token == "" and token is not None:
+ supplied_token = request.POST.get(token, "")
+
+ policy = request.registry.getUtility(ICSRFStoragePolicy)
+ if not policy.check_csrf_token(request, text_(supplied_token)):
+ if raises:
+ raise BadCSRFToken('check_csrf_token(): Invalid token')
+ return False
+ return True
+
+
+def check_csrf_origin(request, trusted_origins=None, raises=True):
+ """
+ Check the ``Origin`` of the request to see if it is a cross site request or
+ not.
+
+ If the value supplied by the ``Origin`` or ``Referer`` header isn't one of the
+ trusted origins and ``raises`` is ``True``, this function will raise a
+ :exc:`pyramid.exceptions.BadCSRFOrigin` exception, but if ``raises`` is
+ ``False``, this function will return ``False`` instead. If the CSRF origin
+ checks are successful this function will return ``True`` unconditionally.
+
+ Additional trusted origins may be added by passing a list of domain (and
+ ports if non-standard like ``['example.com', 'dev.example.com:8080']``) in
+ with the ``trusted_origins`` parameter. If ``trusted_origins`` is ``None``
+ (the default) this list of additional domains will be pulled from the
+ ``pyramid.csrf_trusted_origins`` setting.
+
+ Note that this function will do nothing if ``request.scheme`` is not
+ ``https``.
+
+ .. versionadded:: 1.7
+
+ .. versionchanged:: 1.9
+ Moved from :mod:`pyramid.session` to :mod:`pyramid.csrf`
+
+ """
+ def _fail(reason):
+ if raises:
+ raise BadCSRFOrigin(reason)
+ else:
+ return False
+
+ if request.scheme == "https":
+ # Suppose user visits http://example.com/
+ # An active network attacker (man-in-the-middle, MITM) sends a
+ # POST form that targets https://example.com/detonate-bomb/ and
+ # submits it via JavaScript.
+ #
+ # The attacker will need to provide a CSRF cookie and token, but
+ # that's no problem for a MITM when we cannot make any assumptions
+ # about what kind of session storage is being used. So the MITM can
+ # circumvent the CSRF protection. This is true for any HTTP connection,
+ # but anyone using HTTPS expects better! For this reason, for
+ # https://example.com/ we need additional protection that treats
+ # http://example.com/ as completely untrusted. Under HTTPS,
+ # Barth et al. found that the Referer header is missing for
+ # same-domain requests in only about 0.2% of cases or less, so
+ # we can use strict Referer checking.
+
+ # Determine the origin of this request
+ origin = request.headers.get("Origin")
+ if origin is None:
+ origin = request.referrer
+
+ # Fail if we were not able to locate an origin at all
+ if not origin:
+ return _fail("Origin checking failed - no Origin or Referer.")
+
+ # Parse our origin so we we can extract the required information from
+ # it.
+ originp = urlparse.urlparse(origin)
+
+ # Ensure that our Referer is also secure.
+ if originp.scheme != "https":
+ return _fail(
+ "Referer checking failed - Referer is insecure while host is "
+ "secure."
+ )
+
+ # Determine which origins we trust, which by default will include the
+ # current origin.
+ if trusted_origins is None:
+ trusted_origins = aslist(
+ request.registry.settings.get(
+ "pyramid.csrf_trusted_origins", [])
+ )
+
+ if request.host_port not in set(["80", "443"]):
+ trusted_origins.append("{0.domain}:{0.host_port}".format(request))
+ else:
+ trusted_origins.append(request.domain)
+
+ # Actually check to see if the request's origin matches any of our
+ # trusted origins.
+ if not any(is_same_domain(originp.netloc, host)
+ for host in trusted_origins):
+ reason = (
+ "Referer checking failed - {0} does not match any trusted "
+ "origins."
+ )
+ return _fail(reason.format(origin))
+
+ return True
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index bbb4754e4..ab83813c8 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -927,6 +927,13 @@ class ISession(IDict):
usually accessed via ``request.session``.
Keys and values of a session must be pickleable.
+
+ .. versionchanged:: 1.9
+
+ Sessions are no longer required to implement ``get_csrf_token`` and
+ ``new_csrf_token``. CSRF token support was moved to the pluggable
+ :class:`pyramid.interfaces.ICSRFStoragePolicy` configuration hook.
+
"""
# attributes
@@ -981,19 +988,39 @@ class ISession(IDict):
:meth:`pyramid.interfaces.ISession.flash`
"""
- def new_csrf_token():
- """ Create and set into the session a new, random cross-site request
- forgery protection token. Return the token. It will be a string."""
- def get_csrf_token():
- """ Return a random cross-site request forgery protection token. It
- will be a string. If a token was previously added to the session via
- ``new_csrf_token``, that token will be returned. If no CSRF token
- was previously set into the session, ``new_csrf_token`` will be
+class ICSRFStoragePolicy(Interface):
+ """ An object that offers the ability to verify CSRF tokens and generate
+ new ones."""
+
+ def new_csrf_token(request):
+ """ Create and return a new, random cross-site request forgery
+ protection token. The token will be an ascii-compatible unicode
+ string.
+
+ """
+
+ def get_csrf_token(request):
+ """ Return a cross-site request forgery protection token. It
+ will be an ascii-compatible unicode string. If a token was previously
+ set for this user via ``new_csrf_token``, that token will be returned.
+ If no CSRF token was previously set, ``new_csrf_token`` will be
called, which will create and set a token, and this token will be
returned.
+
+ """
+
+ def check_csrf_token(request, token):
+ """ Determine if the supplied ``token`` is valid. Most implementations
+ should simply compare the ``token`` to the current value of
+ ``get_csrf_token`` but it is possible to verify the token using
+ any mechanism necessary using this method.
+
+ Returns ``True`` if the ``token`` is valid, otherwise ``False``.
+
"""
+
class IIntrospector(Interface):
def get(category_name, discriminator, default=None):
""" Get the IIntrospectable related to the category_name and the
diff --git a/pyramid/predicates.py b/pyramid/predicates.py
index 7c3a778ca..3d7bb1b4b 100644
--- a/pyramid/predicates.py
+++ b/pyramid/predicates.py
@@ -4,7 +4,7 @@ from pyramid.exceptions import ConfigurationError
from pyramid.compat import is_nonstr_iter
-from pyramid.session import check_csrf_token
+from pyramid.csrf import check_csrf_token
from pyramid.traversal import (
find_interface,
traversal_path,
diff --git a/pyramid/renderers.py b/pyramid/renderers.py
index 47705d5d9..6019f50fb 100644
--- a/pyramid/renderers.py
+++ b/pyramid/renderers.py
@@ -1,3 +1,4 @@
+from functools import partial
import json
import os
import re
@@ -19,6 +20,7 @@ from pyramid.compat import (
text_type,
)
+from pyramid.csrf import get_csrf_token
from pyramid.decorator import reify
from pyramid.events import BeforeRender
@@ -428,6 +430,7 @@ class RendererHelper(object):
'context':context,
'request':request,
'req':request,
+ 'get_csrf_token':partial(get_csrf_token, request),
}
return self.render_to_response(response, system, request=request)
@@ -441,13 +444,13 @@ class RendererHelper(object):
'context':getattr(request, 'context', None),
'request':request,
'req':request,
+ 'get_csrf_token':partial(get_csrf_token, request),
}
system_values = BeforeRender(system_values, value)
registry = self.registry
registry.notify(system_values)
-
result = renderer(value, system_values)
return result
diff --git a/pyramid/session.py b/pyramid/session.py
index 47b80f617..33119343b 100644
--- a/pyramid/session.py
+++ b/pyramid/session.py
@@ -16,19 +16,15 @@ from pyramid.compat import (
text_,
bytes_,
native_,
- urlparse,
)
-
-from pyramid.exceptions import (
- BadCSRFOrigin,
- BadCSRFToken,
+from pyramid.csrf import (
+ check_csrf_origin,
+ check_csrf_token,
)
+
from pyramid.interfaces import ISession
-from pyramid.settings import aslist
-from pyramid.util import (
- is_same_domain,
- strings_differ,
-)
+from pyramid.util import strings_differ
+
def manage_accessed(wrapped):
""" Decorator which causes a cookie to be renewed when an accessor
@@ -109,149 +105,6 @@ def signed_deserialize(serialized, secret, hmac=hmac):
return pickle.loads(pickled)
-def check_csrf_origin(request, trusted_origins=None, raises=True):
- """
- Check the Origin of the request to see if it is a cross site request or
- not.
-
- If the value supplied by the Origin or Referer header isn't one of the
- trusted origins and ``raises`` is ``True``, this function will raise a
- :exc:`pyramid.exceptions.BadCSRFOrigin` exception but if ``raises`` is
- ``False`` this function will return ``False`` instead. If the CSRF origin
- checks are successful this function will return ``True`` unconditionally.
-
- Additional trusted origins may be added by passing a list of domain (and
- ports if nonstandard like `['example.com', 'dev.example.com:8080']`) in
- with the ``trusted_origins`` parameter. If ``trusted_origins`` is ``None``
- (the default) this list of additional domains will be pulled from the
- ``pyramid.csrf_trusted_origins`` setting.
-
- Note that this function will do nothing if request.scheme is not https.
-
- .. versionadded:: 1.7
- """
- def _fail(reason):
- if raises:
- raise BadCSRFOrigin(reason)
- else:
- return False
-
- if request.scheme == "https":
- # Suppose user visits http://example.com/
- # An active network attacker (man-in-the-middle, MITM) sends a
- # POST form that targets https://example.com/detonate-bomb/ and
- # submits it via JavaScript.
- #
- # The attacker will need to provide a CSRF cookie and token, but
- # that's no problem for a MITM when we cannot make any assumptions
- # about what kind of session storage is being used. So the MITM can
- # circumvent the CSRF protection. This is true for any HTTP connection,
- # but anyone using HTTPS expects better! For this reason, for
- # https://example.com/ we need additional protection that treats
- # http://example.com/ as completely untrusted. Under HTTPS,
- # Barth et al. found that the Referer header is missing for
- # same-domain requests in only about 0.2% of cases or less, so
- # we can use strict Referer checking.
-
- # Determine the origin of this request
- origin = request.headers.get("Origin")
- if origin is None:
- origin = request.referrer
-
- # Fail if we were not able to locate an origin at all
- if not origin:
- return _fail("Origin checking failed - no Origin or Referer.")
-
- # Parse our origin so we we can extract the required information from
- # it.
- originp = urlparse.urlparse(origin)
-
- # Ensure that our Referer is also secure.
- if originp.scheme != "https":
- return _fail(
- "Referer checking failed - Referer is insecure while host is "
- "secure."
- )
-
- # Determine which origins we trust, which by default will include the
- # current origin.
- if trusted_origins is None:
- trusted_origins = aslist(
- request.registry.settings.get(
- "pyramid.csrf_trusted_origins", [])
- )
-
- if request.host_port not in set(["80", "443"]):
- trusted_origins.append("{0.domain}:{0.host_port}".format(request))
- else:
- trusted_origins.append(request.domain)
-
- # Actually check to see if the request's origin matches any of our
- # trusted origins.
- if not any(is_same_domain(originp.netloc, host)
- for host in trusted_origins):
- reason = (
- "Referer checking failed - {0} does not match any trusted "
- "origins."
- )
- return _fail(reason.format(origin))
-
- return True
-
-
-def check_csrf_token(request,
- token='csrf_token',
- header='X-CSRF-Token',
- raises=True):
- """ Check the CSRF token in the request's session against the value in
- ``request.POST.get(token)`` (if a POST request) or
- ``request.headers.get(header)``. If a ``token`` keyword is not supplied to
- this function, the string ``csrf_token`` will be used to look up the token
- in ``request.POST``. If a ``header`` keyword is not supplied to this
- function, the string ``X-CSRF-Token`` will be used to look up the token in
- ``request.headers``.
-
- If the value supplied by post or by header doesn't match the value
- supplied by ``request.session.get_csrf_token()``, and ``raises`` is
- ``True``, this function will raise an
- :exc:`pyramid.exceptions.BadCSRFToken` exception.
- If the values differ and ``raises`` is ``False``, this function will
- return ``False``. If the CSRF check is successful, this function will
- return ``True`` unconditionally.
-
- Note that using this function requires that a :term:`session factory` is
- configured.
-
- See :ref:`auto_csrf_checking` for information about how to secure your
- application automatically against CSRF attacks.
-
- .. versionadded:: 1.4a2
-
- .. versionchanged:: 1.7a1
- A CSRF token passed in the query string of the request is no longer
- considered valid. It must be passed in either the request body or
- a header.
- """
- supplied_token = ""
- # If this is a POST/PUT/etc request, then we'll check the body to see if it
- # has a token. We explicitly use request.POST here because CSRF tokens
- # should never appear in an URL as doing so is a security issue. We also
- # explicitly check for request.POST here as we do not support sending form
- # encoded data over anything but a request.POST.
- if token is not None:
- supplied_token = request.POST.get(token, "")
-
- # If we were unable to locate a CSRF token in a request body, then we'll
- # check to see if there are any headers that have a value for us.
- if supplied_token == "" and header is not None:
- supplied_token = request.headers.get(header, "")
-
- expected_token = request.session.get_csrf_token()
- if strings_differ(bytes_(expected_token), bytes_(supplied_token)):
- if raises:
- raise BadCSRFToken('check_csrf_token(): Invalid token')
- return False
- return True
class PickleSerializer(object):
""" A serializer that uses the pickle protocol to dump Python
@@ -759,3 +612,13 @@ def SignedCookieSessionFactory(
reissue_time=reissue_time,
set_on_exception=set_on_exception,
)
+
+check_csrf_origin = check_csrf_origin # api
+deprecated('check_csrf_origin',
+ 'pyramid.session.check_csrf_origin is deprecated as of Pyramid '
+ '1.9. Use pyramid.csrf.check_csrf_origin instead.')
+
+check_csrf_token = check_csrf_token # api
+deprecated('check_csrf_token',
+ 'pyramid.session.check_csrf_token is deprecated as of Pyramid '
+ '1.9. Use pyramid.csrf.check_csrf_token instead.')
diff --git a/pyramid/testing.py b/pyramid/testing.py
index 877b351db..69b30e83f 100644
--- a/pyramid/testing.py
+++ b/pyramid/testing.py
@@ -479,6 +479,7 @@ def setUp(registry=None, request=None, hook_zca=True, autocommit=True,
config.add_default_view_derivers()
config.add_default_route_predicates()
config.add_default_tweens()
+ config.add_default_security()
config.commit()
global have_zca
try:
diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py
index 0d5413d16..ab584cc3d 100644
--- a/pyramid/tests/test_config/test_init.py
+++ b/pyramid/tests/test_config/test_init.py
@@ -141,6 +141,22 @@ class ConfiguratorTests(unittest.TestCase):
self.assertEqual(manager.pushed, pushed)
self.assertEqual(manager.popped, True)
+ def test_context_manager(self):
+ from pyramid.config import Configurator
+ config = Configurator()
+ manager = DummyThreadLocalManager()
+ config.manager = manager
+ view = lambda r: None
+ with config as ctx:
+ self.assertTrue(config is ctx)
+ self.assertEqual(manager.pushed,
+ {'registry': config.registry, 'request': None})
+ self.assertFalse(manager.popped)
+ config.add_view(view)
+ self.assertTrue(manager.popped)
+ config.add_view(view) # did not raise a conflict because of commit
+ config.commit()
+
def test_ctor_with_package_registry(self):
import sys
from pyramid.config import Configurator
@@ -817,6 +833,16 @@ pyramid.tests.test_config.dummy_include2""",
self.assertEqual(results['root_package'], tests)
self.assertEqual(results['package'], test_config)
+ def test_include_threadlocals_active(self):
+ from pyramid.tests import test_config
+ from pyramid.threadlocal import get_current_registry
+ stack = []
+ def include(config):
+ stack.append(get_current_registry())
+ config = self._makeOne()
+ config.include(include)
+ self.assertTrue(stack[0] is config.registry)
+
def test_action_branching_kw_is_None(self):
config = self._makeOne(autocommit=True)
self.assertEqual(config.action('discrim'), None)
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
index 211632730..0816d9958 100644
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -18,6 +18,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
def _makeOne(self, *arg, **kw):
from pyramid.config import Configurator
config = Configurator(*arg, **kw)
+ config.set_default_csrf_options(require_csrf=False)
return config
def _getViewCallable(self, config, ctx_iface=None, exc_iface=None,
@@ -2373,7 +2374,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
view = lambda r: 'OK'
config.set_default_csrf_options(require_csrf=True)
config.add_view(view, context=Exception, renderer=null_renderer)
- view_intr = introspector.introspectables[1]
+ view_intr = introspector.introspectables[-1]
self.assertTrue(view_intr.type_name, 'view')
self.assertEqual(view_intr['callable'], view)
derived_view = view_intr['derived_callable']
diff --git a/pyramid/tests/test_csrf.py b/pyramid/tests/test_csrf.py
new file mode 100644
index 000000000..f01780ad8
--- /dev/null
+++ b/pyramid/tests/test_csrf.py
@@ -0,0 +1,406 @@
+import unittest
+
+from pyramid import testing
+from pyramid.config import Configurator
+
+
+class TestLegacySessionCSRFStoragePolicy(unittest.TestCase):
+ class MockSession(object):
+ def __init__(self, current_token='02821185e4c94269bdc38e6eeae0a2f8'):
+ self.current_token = current_token
+
+ def new_csrf_token(self):
+ self.current_token = 'e5e9e30a08b34ff9842ff7d2b958c14b'
+ return self.current_token
+
+ def get_csrf_token(self):
+ return self.current_token
+
+ def _makeOne(self):
+ from pyramid.csrf import LegacySessionCSRFStoragePolicy
+ return LegacySessionCSRFStoragePolicy()
+
+ def test_register_session_csrf_policy(self):
+ from pyramid.csrf import LegacySessionCSRFStoragePolicy
+ from pyramid.interfaces import ICSRFStoragePolicy
+
+ config = Configurator()
+ config.set_csrf_storage_policy(self._makeOne())
+ config.commit()
+
+ policy = config.registry.queryUtility(ICSRFStoragePolicy)
+
+ self.assertTrue(isinstance(policy, LegacySessionCSRFStoragePolicy))
+
+ def test_session_csrf_implementation_delegates_to_session(self):
+ policy = self._makeOne()
+ request = DummyRequest(session=self.MockSession())
+
+ self.assertEqual(
+ policy.get_csrf_token(request),
+ '02821185e4c94269bdc38e6eeae0a2f8'
+ )
+ self.assertEqual(
+ policy.new_csrf_token(request),
+ 'e5e9e30a08b34ff9842ff7d2b958c14b'
+ )
+
+ def test_check_csrf_token(self):
+ request = DummyRequest(session=self.MockSession('foo'))
+
+ policy = self._makeOne()
+ self.assertTrue(policy.check_csrf_token(request, 'foo'))
+ self.assertFalse(policy.check_csrf_token(request, 'bar'))
+
+
+class TestSessionCSRFStoragePolicy(unittest.TestCase):
+ def _makeOne(self, **kw):
+ from pyramid.csrf import SessionCSRFStoragePolicy
+ return SessionCSRFStoragePolicy(**kw)
+
+ def test_register_session_csrf_policy(self):
+ from pyramid.csrf import SessionCSRFStoragePolicy
+ from pyramid.interfaces import ICSRFStoragePolicy
+
+ config = Configurator()
+ config.set_csrf_storage_policy(self._makeOne())
+ config.commit()
+
+ policy = config.registry.queryUtility(ICSRFStoragePolicy)
+
+ self.assertTrue(isinstance(policy, SessionCSRFStoragePolicy))
+
+ def test_it_creates_a_new_token(self):
+ request = DummyRequest(session={})
+
+ policy = self._makeOne()
+ policy._token_factory = lambda: 'foo'
+ self.assertEqual(policy.get_csrf_token(request), 'foo')
+
+ def test_get_csrf_token_returns_the_new_token(self):
+ request = DummyRequest(session={'_csrft_': 'foo'})
+
+ policy = self._makeOne()
+ self.assertEqual(policy.get_csrf_token(request), 'foo')
+
+ token = policy.new_csrf_token(request)
+ self.assertNotEqual(token, 'foo')
+ self.assertEqual(token, policy.get_csrf_token(request))
+
+ def test_check_csrf_token(self):
+ request = DummyRequest(session={})
+
+ policy = self._makeOne()
+ self.assertFalse(policy.check_csrf_token(request, 'foo'))
+
+ request.session = {'_csrft_': 'foo'}
+ self.assertTrue(policy.check_csrf_token(request, 'foo'))
+ self.assertFalse(policy.check_csrf_token(request, 'bar'))
+
+
+class TestCookieCSRFStoragePolicy(unittest.TestCase):
+ def _makeOne(self, **kw):
+ from pyramid.csrf import CookieCSRFStoragePolicy
+ return CookieCSRFStoragePolicy(**kw)
+
+ def test_register_cookie_csrf_policy(self):
+ from pyramid.csrf import CookieCSRFStoragePolicy
+ from pyramid.interfaces import ICSRFStoragePolicy
+
+ config = Configurator()
+ config.set_csrf_storage_policy(self._makeOne())
+ config.commit()
+
+ policy = config.registry.queryUtility(ICSRFStoragePolicy)
+
+ self.assertTrue(isinstance(policy, CookieCSRFStoragePolicy))
+
+ def test_get_cookie_csrf_with_no_existing_cookie_sets_cookies(self):
+ response = MockResponse()
+ request = DummyRequest()
+
+ policy = self._makeOne()
+ token = policy.get_csrf_token(request)
+ request.response_callback(request, response)
+ self.assertEqual(
+ response.headerlist,
+ [('Set-Cookie', 'csrf_token={}; Path=/'.format(token))]
+ )
+
+ def test_existing_cookie_csrf_does_not_set_cookie(self):
+ request = DummyRequest()
+ request.cookies = {'csrf_token': 'e6f325fee5974f3da4315a8ccf4513d2'}
+
+ policy = self._makeOne()
+ token = policy.get_csrf_token(request)
+
+ self.assertEqual(
+ token,
+ 'e6f325fee5974f3da4315a8ccf4513d2'
+ )
+ self.assertIsNone(request.response_callback)
+
+ def test_new_cookie_csrf_with_existing_cookie_sets_cookies(self):
+ request = DummyRequest()
+ request.cookies = {'csrf_token': 'e6f325fee5974f3da4315a8ccf4513d2'}
+
+ policy = self._makeOne()
+ token = policy.new_csrf_token(request)
+
+ response = MockResponse()
+ request.response_callback(request, response)
+ self.assertEqual(
+ response.headerlist,
+ [('Set-Cookie', 'csrf_token={}; Path=/'.format(token))]
+ )
+
+ def test_get_csrf_token_returns_the_new_token(self):
+ request = DummyRequest()
+ request.cookies = {'csrf_token': 'foo'}
+
+ policy = self._makeOne()
+ self.assertEqual(policy.get_csrf_token(request), 'foo')
+
+ token = policy.new_csrf_token(request)
+ self.assertNotEqual(token, 'foo')
+ self.assertEqual(token, policy.get_csrf_token(request))
+
+ def test_check_csrf_token(self):
+ request = DummyRequest()
+
+ policy = self._makeOne()
+ self.assertFalse(policy.check_csrf_token(request, 'foo'))
+
+ request.cookies = {'csrf_token': 'foo'}
+ self.assertTrue(policy.check_csrf_token(request, 'foo'))
+ self.assertFalse(policy.check_csrf_token(request, 'bar'))
+
+class Test_get_csrf_token(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+
+ def _callFUT(self, *args, **kwargs):
+ from pyramid.csrf import get_csrf_token
+ return get_csrf_token(*args, **kwargs)
+
+ def test_no_override_csrf_utility_registered(self):
+ request = testing.DummyRequest()
+ self._callFUT(request)
+
+ def test_success(self):
+ self.config.set_csrf_storage_policy(DummyCSRF())
+ request = testing.DummyRequest()
+
+ csrf_token = self._callFUT(request)
+
+ self.assertEquals(csrf_token, '02821185e4c94269bdc38e6eeae0a2f8')
+
+
+class Test_new_csrf_token(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+
+ def _callFUT(self, *args, **kwargs):
+ from pyramid.csrf import new_csrf_token
+ return new_csrf_token(*args, **kwargs)
+
+ def test_no_override_csrf_utility_registered(self):
+ request = testing.DummyRequest()
+ self._callFUT(request)
+
+ def test_success(self):
+ self.config.set_csrf_storage_policy(DummyCSRF())
+ request = testing.DummyRequest()
+
+ csrf_token = self._callFUT(request)
+
+ self.assertEquals(csrf_token, 'e5e9e30a08b34ff9842ff7d2b958c14b')
+
+
+class Test_check_csrf_token(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+
+ # set up CSRF
+ self.config.set_default_csrf_options(require_csrf=False)
+
+ def _callFUT(self, *args, **kwargs):
+ from ..csrf import check_csrf_token
+ return check_csrf_token(*args, **kwargs)
+
+ def test_success_token(self):
+ request = testing.DummyRequest()
+ request.method = "POST"
+ request.POST = {'csrf_token': request.session.get_csrf_token()}
+ self.assertEqual(self._callFUT(request, token='csrf_token'), True)
+
+ def test_success_header(self):
+ request = testing.DummyRequest()
+ request.headers['X-CSRF-Token'] = request.session.get_csrf_token()
+ self.assertEqual(self._callFUT(request, header='X-CSRF-Token'), True)
+
+ def test_success_default_token(self):
+ request = testing.DummyRequest()
+ request.method = "POST"
+ request.POST = {'csrf_token': request.session.get_csrf_token()}
+ self.assertEqual(self._callFUT(request), True)
+
+ def test_success_default_header(self):
+ request = testing.DummyRequest()
+ request.headers['X-CSRF-Token'] = request.session.get_csrf_token()
+ self.assertEqual(self._callFUT(request), True)
+
+ def test_failure_raises(self):
+ from pyramid.exceptions import BadCSRFToken
+ request = testing.DummyRequest()
+ self.assertRaises(BadCSRFToken, self._callFUT, request,
+ 'csrf_token')
+
+ def test_failure_no_raises(self):
+ request = testing.DummyRequest()
+ result = self._callFUT(request, 'csrf_token', raises=False)
+ self.assertEqual(result, False)
+
+
+class Test_check_csrf_token_without_defaults_configured(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+
+ def _callFUT(self, *args, **kwargs):
+ from ..csrf import check_csrf_token
+ return check_csrf_token(*args, **kwargs)
+
+ def test_success_token(self):
+ request = testing.DummyRequest()
+ request.method = "POST"
+ request.POST = {'csrf_token': request.session.get_csrf_token()}
+ self.assertEqual(self._callFUT(request, token='csrf_token'), True)
+
+ def test_failure_raises(self):
+ from pyramid.exceptions import BadCSRFToken
+ request = testing.DummyRequest()
+ self.assertRaises(BadCSRFToken, self._callFUT, request,
+ 'csrf_token')
+
+ def test_failure_no_raises(self):
+ request = testing.DummyRequest()
+ result = self._callFUT(request, 'csrf_token', raises=False)
+ self.assertEqual(result, False)
+
+
+class Test_check_csrf_origin(unittest.TestCase):
+ def _callFUT(self, *args, **kwargs):
+ from ..csrf import check_csrf_origin
+ return check_csrf_origin(*args, **kwargs)
+
+ def test_success_with_http(self):
+ request = testing.DummyRequest()
+ request.scheme = "http"
+ self.assertTrue(self._callFUT(request))
+
+ def test_success_with_https_and_referrer(self):
+ request = testing.DummyRequest()
+ request.scheme = "https"
+ request.host = "example.com"
+ request.host_port = "443"
+ request.referrer = "https://example.com/login/"
+ request.registry.settings = {}
+ self.assertTrue(self._callFUT(request))
+
+ def test_success_with_https_and_origin(self):
+ request = testing.DummyRequest()
+ request.scheme = "https"
+ request.host = "example.com"
+ request.host_port = "443"
+ request.headers = {"Origin": "https://example.com/"}
+ request.referrer = "https://not-example.com/"
+ request.registry.settings = {}
+ self.assertTrue(self._callFUT(request))
+
+ def test_success_with_additional_trusted_host(self):
+ request = testing.DummyRequest()
+ request.scheme = "https"
+ request.host = "example.com"
+ request.host_port = "443"
+ request.referrer = "https://not-example.com/login/"
+ request.registry.settings = {
+ "pyramid.csrf_trusted_origins": ["not-example.com"],
+ }
+ self.assertTrue(self._callFUT(request))
+
+ def test_success_with_nonstandard_port(self):
+ request = testing.DummyRequest()
+ request.scheme = "https"
+ request.host = "example.com:8080"
+ request.host_port = "8080"
+ request.referrer = "https://example.com:8080/login/"
+ request.registry.settings = {}
+ self.assertTrue(self._callFUT(request))
+
+ def test_fails_with_wrong_host(self):
+ from pyramid.exceptions import BadCSRFOrigin
+ request = testing.DummyRequest()
+ request.scheme = "https"
+ request.host = "example.com"
+ request.host_port = "443"
+ request.referrer = "https://not-example.com/login/"
+ request.registry.settings = {}
+ self.assertRaises(BadCSRFOrigin, self._callFUT, request)
+ self.assertFalse(self._callFUT(request, raises=False))
+
+ def test_fails_with_no_origin(self):
+ from pyramid.exceptions import BadCSRFOrigin
+ request = testing.DummyRequest()
+ request.scheme = "https"
+ request.referrer = None
+ self.assertRaises(BadCSRFOrigin, self._callFUT, request)
+ self.assertFalse(self._callFUT(request, raises=False))
+
+ def test_fails_when_http_to_https(self):
+ from pyramid.exceptions import BadCSRFOrigin
+ request = testing.DummyRequest()
+ request.scheme = "https"
+ request.host = "example.com"
+ request.host_port = "443"
+ request.referrer = "http://example.com/evil/"
+ request.registry.settings = {}
+ self.assertRaises(BadCSRFOrigin, self._callFUT, request)
+ self.assertFalse(self._callFUT(request, raises=False))
+
+ def test_fails_with_nonstandard_port(self):
+ from pyramid.exceptions import BadCSRFOrigin
+ request = testing.DummyRequest()
+ request.scheme = "https"
+ request.host = "example.com:8080"
+ request.host_port = "8080"
+ request.referrer = "https://example.com/login/"
+ request.registry.settings = {}
+ self.assertRaises(BadCSRFOrigin, self._callFUT, request)
+ self.assertFalse(self._callFUT(request, raises=False))
+
+
+class DummyRequest(object):
+ registry = None
+ session = None
+ response_callback = None
+
+ def __init__(self, registry=None, session=None):
+ self.registry = registry
+ self.session = session
+ self.cookies = {}
+
+ def add_response_callback(self, callback):
+ self.response_callback = callback
+
+
+class MockResponse(object):
+ def __init__(self):
+ self.headerlist = []
+
+
+class DummyCSRF(object):
+ def new_csrf_token(self, request):
+ return 'e5e9e30a08b34ff9842ff7d2b958c14b'
+
+ def get_csrf_token(self, request):
+ return '02821185e4c94269bdc38e6eeae0a2f8'
diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py
index 65bfa5582..86d8b582a 100644
--- a/pyramid/tests/test_renderers.py
+++ b/pyramid/tests/test_renderers.py
@@ -203,6 +203,7 @@ class TestRendererHelper(unittest.TestCase):
self.assertEqual(helper.get_renderer(), factory.respond)
def test_render_view(self):
+ import pyramid.csrf
self._registerRendererFactory()
self._registerResponseFactory()
request = Dummy()
@@ -212,6 +213,9 @@ class TestRendererHelper(unittest.TestCase):
request = testing.DummyRequest()
response = 'response'
response = helper.render_view(request, response, view, context)
+ get_csrf = response.app_iter[1].pop('get_csrf_token')
+ self.assertEqual(get_csrf.args, (request, ))
+ self.assertEqual(get_csrf.func, pyramid.csrf.get_csrf_token)
self.assertEqual(response.app_iter[0], 'response')
self.assertEqual(response.app_iter[1],
{'renderer_info': helper,
@@ -242,12 +246,16 @@ class TestRendererHelper(unittest.TestCase):
self.assertEqual(reg.event.__class__.__name__, 'BeforeRender')
def test_render_system_values_is_None(self):
+ import pyramid.csrf
self._registerRendererFactory()
request = Dummy()
context = Dummy()
request.context = context
helper = self._makeOne('loo.foo')
result = helper.render('values', None, request=request)
+ get_csrf = result[1].pop('get_csrf_token')
+ self.assertEqual(get_csrf.args, (request, ))
+ self.assertEqual(get_csrf.func, pyramid.csrf.get_csrf_token)
system = {'request':request,
'context':context,
'renderer_name':'loo.foo',
diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py
index 3a308d08b..ade602799 100644
--- a/pyramid/tests/test_session.py
+++ b/pyramid/tests/test_session.py
@@ -659,144 +659,6 @@ class Test_signed_deserialize(unittest.TestCase):
result = self._callFUT(serialized, secret.decode('latin-1'))
self.assertEqual(result, '123')
-class Test_check_csrf_token(unittest.TestCase):
- def _callFUT(self, *args, **kwargs):
- from ..session import check_csrf_token
- return check_csrf_token(*args, **kwargs)
-
- def test_success_token(self):
- request = testing.DummyRequest()
- request.method = "POST"
- request.POST = {'csrf_token': request.session.get_csrf_token()}
- self.assertEqual(self._callFUT(request, token='csrf_token'), True)
-
- def test_success_header(self):
- request = testing.DummyRequest()
- request.headers['X-CSRF-Token'] = request.session.get_csrf_token()
- self.assertEqual(self._callFUT(request, header='X-CSRF-Token'), True)
-
- def test_success_default_token(self):
- request = testing.DummyRequest()
- request.method = "POST"
- request.POST = {'csrf_token': request.session.get_csrf_token()}
- self.assertEqual(self._callFUT(request), True)
-
- def test_success_default_header(self):
- request = testing.DummyRequest()
- request.headers['X-CSRF-Token'] = request.session.get_csrf_token()
- self.assertEqual(self._callFUT(request), True)
-
- def test_failure_raises(self):
- from pyramid.exceptions import BadCSRFToken
- request = testing.DummyRequest()
- self.assertRaises(BadCSRFToken, self._callFUT, request,
- 'csrf_token')
-
- def test_failure_no_raises(self):
- request = testing.DummyRequest()
- result = self._callFUT(request, 'csrf_token', raises=False)
- self.assertEqual(result, False)
-
- def test_token_differing_types(self):
- from pyramid.compat import text_
- request = testing.DummyRequest()
- request.method = "POST"
- request.session['_csrft_'] = text_('foo')
- request.POST = {'csrf_token': b'foo'}
- self.assertEqual(self._callFUT(request, token='csrf_token'), True)
-
-
-class Test_check_csrf_origin(unittest.TestCase):
-
- def _callFUT(self, *args, **kwargs):
- from ..session import check_csrf_origin
- return check_csrf_origin(*args, **kwargs)
-
- def test_success_with_http(self):
- request = testing.DummyRequest()
- request.scheme = "http"
- self.assertTrue(self._callFUT(request))
-
- def test_success_with_https_and_referrer(self):
- request = testing.DummyRequest()
- request.scheme = "https"
- request.host = "example.com"
- request.host_port = "443"
- request.referrer = "https://example.com/login/"
- request.registry.settings = {}
- self.assertTrue(self._callFUT(request))
-
- def test_success_with_https_and_origin(self):
- request = testing.DummyRequest()
- request.scheme = "https"
- request.host = "example.com"
- request.host_port = "443"
- request.headers = {"Origin": "https://example.com/"}
- request.referrer = "https://not-example.com/"
- request.registry.settings = {}
- self.assertTrue(self._callFUT(request))
-
- def test_success_with_additional_trusted_host(self):
- request = testing.DummyRequest()
- request.scheme = "https"
- request.host = "example.com"
- request.host_port = "443"
- request.referrer = "https://not-example.com/login/"
- request.registry.settings = {
- "pyramid.csrf_trusted_origins": ["not-example.com"],
- }
- self.assertTrue(self._callFUT(request))
-
- def test_success_with_nonstandard_port(self):
- request = testing.DummyRequest()
- request.scheme = "https"
- request.host = "example.com:8080"
- request.host_port = "8080"
- request.referrer = "https://example.com:8080/login/"
- request.registry.settings = {}
- self.assertTrue(self._callFUT(request))
-
- def test_fails_with_wrong_host(self):
- from pyramid.exceptions import BadCSRFOrigin
- request = testing.DummyRequest()
- request.scheme = "https"
- request.host = "example.com"
- request.host_port = "443"
- request.referrer = "https://not-example.com/login/"
- request.registry.settings = {}
- self.assertRaises(BadCSRFOrigin, self._callFUT, request)
- self.assertFalse(self._callFUT(request, raises=False))
-
- def test_fails_with_no_origin(self):
- from pyramid.exceptions import BadCSRFOrigin
- request = testing.DummyRequest()
- request.scheme = "https"
- request.referrer = None
- self.assertRaises(BadCSRFOrigin, self._callFUT, request)
- self.assertFalse(self._callFUT(request, raises=False))
-
- def test_fails_when_http_to_https(self):
- from pyramid.exceptions import BadCSRFOrigin
- request = testing.DummyRequest()
- request.scheme = "https"
- request.host = "example.com"
- request.host_port = "443"
- request.referrer = "http://example.com/evil/"
- request.registry.settings = {}
- self.assertRaises(BadCSRFOrigin, self._callFUT, request)
- self.assertFalse(self._callFUT(request, raises=False))
-
- def test_fails_with_nonstandard_port(self):
- from pyramid.exceptions import BadCSRFOrigin
- request = testing.DummyRequest()
- request.scheme = "https"
- request.host = "example.com:8080"
- request.host_port = "8080"
- request.referrer = "https://example.com/login/"
- request.registry.settings = {}
- self.assertRaises(BadCSRFOrigin, self._callFUT, request)
- self.assertFalse(self._callFUT(request, raises=False))
-
class DummySerializer(object):
def dumps(self, value):
diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py
index bbf6103f4..d64f0a73f 100644
--- a/pyramid/tests/test_util.py
+++ b/pyramid/tests/test_util.py
@@ -369,12 +369,16 @@ class Test_strings_differ(unittest.TestCase):
from pyramid.util import strings_differ
return strings_differ(*args, **kw)
- def test_it(self):
+ def test_it_bytes(self):
self.assertFalse(self._callFUT(b'foo', b'foo'))
self.assertTrue(self._callFUT(b'123', b'345'))
self.assertTrue(self._callFUT(b'1234', b'123'))
self.assertTrue(self._callFUT(b'123', b'1234'))
+ def test_it_native_str(self):
+ self.assertFalse(self._callFUT('123', '123'))
+ self.assertTrue(self._callFUT('123', '1234'))
+
def test_it_with_internal_comparator(self):
result = self._callFUT(b'foo', b'foo', compare_digest=None)
self.assertFalse(result)
diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py
index 51d0bd367..6b81cc1e5 100644
--- a/pyramid/tests/test_viewderivers.py
+++ b/pyramid/tests/test_viewderivers.py
@@ -12,6 +12,7 @@ class TestDeriveView(unittest.TestCase):
def setUp(self):
self.config = testing.setUp()
+ self.config.set_default_csrf_options(require_csrf=False)
def tearDown(self):
self.config = None
diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py
index 4eb0ce704..d2869b162 100644
--- a/pyramid/viewderivers.py
+++ b/pyramid/viewderivers.py
@@ -6,7 +6,7 @@ from zope.interface import (
)
from pyramid.security import NO_PERMISSION_REQUIRED
-from pyramid.session import (
+from pyramid.csrf import (
check_csrf_origin,
check_csrf_token,
)