Bowe Strickland
2018-10-27 323fa95deea50f49c119728fc2eeacb9e0c51241
commit | author | age
c2c589 1 import inspect
CD 2
0c29cf 3 from zope.interface import implementer, provider
c2c589 4
323fa9 5 from pyramid.security import NO_PERMISSION_REQUIRED, Authenticated
0c29cf 6 from pyramid.csrf import check_csrf_origin, check_csrf_token
c2c589 7 from pyramid.response import Response
CD 8
9 from pyramid.interfaces import (
10     IAuthenticationPolicy,
11     IAuthorizationPolicy,
de3d0c 12     IDefaultCSRFOptions,
2160ce 13     IDefaultPermission,
c2c589 14     IDebugLogger,
CD 15     IResponse,
16     IViewMapper,
17     IViewMapperFactory,
52fde9 18 )
0c29cf 19
MM 20 from pyramid.compat import is_bound_method, is_unbound_method
21
22 from pyramid.exceptions import ConfigurationError
323fa9 23 from pyramid.httpexceptions import HTTPForbidden, HTTPUnauthorized
0c29cf 24 from pyramid.util import object_description, takes_one_arg
2fbeb4 25 from pyramid.view import render_view_to_response
c2c589 26 from pyramid import renderers
CD 27
28
29 def view_description(view):
30     try:
31         return view.__text__
32     except AttributeError:
33         # custom view mappers might not add __text__
34         return object_description(view)
35
0c29cf 36
c2c589 37 def requestonly(view, attr=None):
CD 38     return takes_one_arg(view, attr=attr, argname='request')
0c29cf 39
c2c589 40
CD 41 @implementer(IViewMapper)
42 @provider(IViewMapperFactory)
43 class DefaultViewMapper(object):
44     def __init__(self, **kw):
45         self.attr = kw.get('attr')
46
47     def __call__(self, view):
48         if is_unbound_method(view) and self.attr is None:
0c29cf 49             raise ConfigurationError(
MM 50                 (
a54bc1 51                     'Unbound method calls are not supported, please set the '
MM 52                     'class as your `view` and the method as your `attr`'
0c29cf 53                 )
MM 54             )
c2c589 55
CD 56         if inspect.isclass(view):
57             view = self.map_class(view)
58         else:
59             view = self.map_nonclass(view)
60         return view
61
62     def map_class(self, view):
63         ronly = requestonly(view, self.attr)
64         if ronly:
65             mapped_view = self.map_class_requestonly(view)
66         else:
67             mapped_view = self.map_class_native(view)
68         mapped_view.__text__ = 'method %s of %s' % (
0c29cf 69             self.attr or '__call__',
MM 70             object_description(view),
71         )
c2c589 72         return mapped_view
CD 73
74     def map_nonclass(self, view):
75         # We do more work here than appears necessary to avoid wrapping the
76         # view unless it actually requires wrapping (to avoid function call
77         # overhead).
78         mapped_view = view
79         ronly = requestonly(view, self.attr)
80         if ronly:
81             mapped_view = self.map_nonclass_requestonly(view)
82         elif self.attr:
83             mapped_view = self.map_nonclass_attr(view)
84         if inspect.isroutine(mapped_view):
85             # This branch will be true if the view is a function or a method.
86             # We potentially mutate an unwrapped object here if it's a
87             # function.  We do this to avoid function call overhead of
88             # injecting another wrapper.  However, we must wrap if the
89             # function is a bound method because we can't set attributes on a
90             # bound method.
91             if is_bound_method(view):
92                 _mapped_view = mapped_view
0c29cf 93
c2c589 94                 def mapped_view(context, request):
CD 95                     return _mapped_view(context, request)
0c29cf 96
c2c589 97             if self.attr is not None:
CD 98                 mapped_view.__text__ = 'attr %s of %s' % (
0c29cf 99                     self.attr,
MM 100                     object_description(view),
101                 )
c2c589 102             else:
CD 103                 mapped_view.__text__ = object_description(view)
104         return mapped_view
105
106     def map_class_requestonly(self, view):
107         # its a class that has an __init__ which only accepts request
108         attr = self.attr
0c29cf 109
c2c589 110         def _class_requestonly_view(context, request):
CD 111             inst = view(request)
112             request.__view__ = inst
113             if attr is None:
114                 response = inst()
115             else:
116                 response = getattr(inst, attr)()
117             return response
0c29cf 118
c2c589 119         return _class_requestonly_view
CD 120
121     def map_class_native(self, view):
122         # its a class that has an __init__ which accepts both context and
123         # request
124         attr = self.attr
0c29cf 125
c2c589 126         def _class_view(context, request):
CD 127             inst = view(context, request)
128             request.__view__ = inst
129             if attr is None:
130                 response = inst()
131             else:
132                 response = getattr(inst, attr)()
133             return response
0c29cf 134
c2c589 135         return _class_view
CD 136
137     def map_nonclass_requestonly(self, view):
138         # its a function that has a __call__ which accepts only a single
139         # request argument
140         attr = self.attr
0c29cf 141
c2c589 142         def _requestonly_view(context, request):
CD 143             if attr is None:
144                 response = view(request)
145             else:
146                 response = getattr(view, attr)(request)
147             return response
0c29cf 148
c2c589 149         return _requestonly_view
CD 150
151     def map_nonclass_attr(self, view):
152         # its a function that has a __call__ which accepts both context and
153         # request, but still has an attr
154         def _attr_view(context, request):
155             response = getattr(view, self.attr)(context, request)
156             return response
0c29cf 157
c2c589 158         return _attr_view
CD 159
160
161 def wraps_view(wrapper):
007600 162     def inner(view, info):
MM 163         wrapper_view = wrapper(view, info)
c2c589 164         return preserve_view_attrs(view, wrapper_view)
0c29cf 165
c2c589 166     return inner
0c29cf 167
c2c589 168
CD 169 def preserve_view_attrs(view, wrapper):
170     if view is None:
171         return wrapper
172
173     if wrapper is view:
174         return view
175
176     original_view = getattr(view, '__original_view__', None)
177
178     if original_view is None:
179         original_view = view
180
181     wrapper.__wraps__ = view
182     wrapper.__original_view__ = original_view
183     wrapper.__module__ = view.__module__
184     wrapper.__doc__ = view.__doc__
185
186     try:
187         wrapper.__name__ = view.__name__
188     except AttributeError:
189         wrapper.__name__ = repr(view)
190
191     # attrs that may not exist on "view", but, if so, must be attached to
192     # "wrapped view"
0c29cf 193     for attr in (
MM 194         '__permitted__',
195         '__call_permissive__',
196         '__permission__',
197         '__predicated__',
198         '__predicates__',
199         '__accept__',
200         '__order__',
201         '__text__',
202     ):
c2c589 203         try:
CD 204             setattr(wrapper, attr, getattr(view, attr))
205         except AttributeError:
206             pass
207
208     return wrapper
0c29cf 209
c2c589 210
007600 211 def mapped_view(view, info):
MM 212     mapper = info.options.get('mapper')
c2c589 213     if mapper is None:
CD 214         mapper = getattr(view, '__view_mapper__', None)
215         if mapper is None:
007600 216             mapper = info.registry.queryUtility(IViewMapperFactory)
c2c589 217             if mapper is None:
CD 218                 mapper = DefaultViewMapper
219
007600 220     mapped_view = mapper(**info.options)(view)
c2c589 221     return mapped_view
CD 222
0c29cf 223
e4b931 224 mapped_view.options = ('mapper', 'attr')
0c29cf 225
e4b931 226
007600 227 def owrapped_view(view, info):
e292cc 228     wrapper_viewname = info.options.get('wrapper')
MM 229     viewname = info.options.get('name')
c2c589 230     if not wrapper_viewname:
CD 231         return view
0c29cf 232
c2c589 233     def _owrapped_view(context, request):
CD 234         response = view(context, request)
235         request.wrapped_response = response
236         request.wrapped_body = response.body
237         request.wrapped_view = view
0c29cf 238         wrapped_response = render_view_to_response(
MM 239             context, request, wrapper_viewname
240         )
c2c589 241         if wrapped_response is None:
CD 242             raise ValueError(
243                 'No wrapper view named %r found when executing view '
0c29cf 244                 'named %r' % (wrapper_viewname, viewname)
MM 245             )
c2c589 246         return wrapped_response
0c29cf 247
c2c589 248     return _owrapped_view
e4b931 249
0c29cf 250
e4b931 251 owrapped_view.options = ('name', 'wrapper')
0c29cf 252
c2c589 253
007600 254 def http_cached_view(view, info):
MM 255     if info.settings.get('prevent_http_cache', False):
c2c589 256         return view
CD 257
007600 258     seconds = info.options.get('http_cache')
c2c589 259
CD 260     if seconds is None:
261         return view
262
263     options = {}
264
265     if isinstance(seconds, (tuple, list)):
266         try:
267             seconds, options = seconds
268         except ValueError:
269             raise ConfigurationError(
270                 'If http_cache parameter is a tuple or list, it must be '
0c29cf 271                 'in the form (seconds, options); not %s' % (seconds,)
MM 272             )
c2c589 273
CD 274     def wrapper(context, request):
275         response = view(context, request)
0c29cf 276         prevent_caching = getattr(
MM 277             response.cache_control, 'prevent_auto', False
278         )
c2c589 279         if not prevent_caching:
CD 280             response.cache_expires(seconds, **options)
281         return response
282
283     return wrapper
e4b931 284
0c29cf 285
e4b931 286 http_cached_view.options = ('http_cache',)
0c29cf 287
c2c589 288
007600 289 def secured_view(view, info):
a3db3c 290     for wrapper in (_secured_view, _authdebug_view):
MM 291         view = wraps_view(wrapper)(view, info)
292     return view
293
0c29cf 294
a3db3c 295 secured_view.options = ('permission',)
0c29cf 296
a3db3c 297
MM 298 def _secured_view(view, info):
2160ce 299     permission = explicit_val = info.options.get('permission')
MM 300     if permission is None:
301         permission = info.registry.queryUtility(IDefaultPermission)
c2c589 302     if permission == NO_PERMISSION_REQUIRED:
CD 303         # allow views registered within configurations that have a
304         # default permission to explicitly override the default
305         # permission, replacing it with no permission at all
306         permission = None
307
308     wrapped_view = view
007600 309     authn_policy = info.registry.queryUtility(IAuthenticationPolicy)
MM 310     authz_policy = info.registry.queryUtility(IAuthorizationPolicy)
c2c589 311
e8c66a 312     # no-op on exception-only views without an explicit permission
MM 313     if explicit_val is None and info.exception_only:
314         return view
315
c2c589 316     if authn_policy and authz_policy and (permission is not None):
0c29cf 317
e8c66a 318         def permitted(context, request):
c2c589 319             principals = authn_policy.effective_principals(request)
bf40a3 320             return authz_policy.permits(context, principals, permission)
0c29cf 321
e8c66a 322         def secured_view(context, request):
MM 323             result = permitted(context, request)
c2c589 324             if result:
CD 325                 return view(context, request)
326             view_name = getattr(view, '__name__', view)
327             msg = getattr(
0c29cf 328                 request,
MM 329                 'authdebug_message',
330                 'Unauthorized: %s failed permission check' % view_name,
331             )
323fa9 332             if Authenticated in result.principals:
BS 333                 raise HTTPForbidden(msg, result=result)
334             raise HTTPUnauthorized(msg)
0c29cf 335
e8c66a 336         wrapped_view = secured_view
MM 337         wrapped_view.__call_permissive__ = view
338         wrapped_view.__permitted__ = permitted
339         wrapped_view.__permission__ = permission
c2c589 340
CD 341     return wrapped_view
0c29cf 342
e4b931 343
a3db3c 344 def _authdebug_view(view, info):
c2c589 345     wrapped_view = view
007600 346     settings = info.settings
2160ce 347     permission = explicit_val = info.options.get('permission')
MM 348     if permission is None:
349         permission = info.registry.queryUtility(IDefaultPermission)
007600 350     authn_policy = info.registry.queryUtility(IAuthenticationPolicy)
MM 351     authz_policy = info.registry.queryUtility(IAuthorizationPolicy)
352     logger = info.registry.queryUtility(IDebugLogger)
2160ce 353
e8c66a 354     # no-op on exception-only views without an explicit permission
MM 355     if explicit_val is None and info.exception_only:
356         return view
357
358     if settings and settings.get('debug_authorization', False):
0c29cf 359
e8c66a 360         def authdebug_view(context, request):
c2c589 361             view_name = getattr(request, 'view_name', None)
CD 362
363             if authn_policy and authz_policy:
364                 if permission is NO_PERMISSION_REQUIRED:
365                     msg = 'Allowed (NO_PERMISSION_REQUIRED)'
366                 elif permission is None:
367                     msg = 'Allowed (no permission registered)'
368                 else:
bf40a3 369                     principals = authn_policy.effective_principals(request)
0c29cf 370                     msg = str(
MM 371                         authz_policy.permits(context, principals, permission)
372                     )
c2c589 373             else:
CD 374                 msg = 'Allowed (no authorization policy in use)'
375
376             view_name = getattr(request, 'view_name', None)
377             url = getattr(request, 'url', None)
0c29cf 378             msg = (
MM 379                 'debug_authorization of url %s (view name %r against '
380                 'context %r): %s' % (url, view_name, context, msg)
381             )
bf40a3 382             if logger:
MM 383                 logger.debug(msg)
c2c589 384             if request is not None:
CD 385                 request.authdebug_message = msg
386             return view(context, request)
0c29cf 387
e8c66a 388         wrapped_view = authdebug_view
c2c589 389
CD 390     return wrapped_view
0c29cf 391
c2c589 392
007600 393 def rendered_view(view, info):
c2c589 394     # one way or another this wrapper must produce a Response (unless
CD 395     # the renderer is a NullRendererHelper)
007600 396     renderer = info.options.get('renderer')
c2c589 397     if renderer is None:
CD 398         # register a default renderer if you want super-dynamic
399         # rendering.  registering a default renderer will also allow
400         # override_renderer to work if a renderer is left unspecified for
401         # a view registration.
402         def viewresult_to_response(context, request):
403             result = view(context, request)
0c29cf 404             if result.__class__ is Response:  # common case
c2c589 405                 response = result
CD 406             else:
007600 407                 response = info.registry.queryAdapterOrSelf(result, IResponse)
c2c589 408                 if response is None:
CD 409                     if result is None:
0c29cf 410                         append = (
MM 411                             ' You may have forgotten to return a value '
412                             'from the view callable.'
413                         )
c2c589 414                     elif isinstance(result, dict):
0c29cf 415                         append = (
MM 416                             ' You may have forgotten to define a '
417                             'renderer in the view configuration.'
418                         )
c2c589 419                     else:
CD 420                         append = ''
421
0c29cf 422                     msg = (
MM 423                         'Could not convert return value of the view '
424                         'callable %s into a response object. '
425                         'The value returned was %r.' + append
426                     )
c2c589 427
CD 428                     raise ValueError(msg % (view_description(view), result))
429
430             return response
431
432         return viewresult_to_response
433
434     if renderer is renderers.null_renderer:
435         return view
436
437     def rendered_view(context, request):
438         result = view(context, request)
0c29cf 439         if result.__class__ is Response:  # potential common case
c2c589 440             response = result
CD 441         else:
442             # this must adapt, it can't do a simple interface check
443             # (avoid trying to render webob responses)
007600 444             response = info.registry.queryAdapterOrSelf(result, IResponse)
c2c589 445             if response is None:
CD 446                 attrs = getattr(request, '__dict__', {})
447                 if 'override_renderer' in attrs:
448                     # renderer overridden by newrequest event or other
449                     renderer_name = attrs.pop('override_renderer')
450                     view_renderer = renderers.RendererHelper(
451                         name=renderer_name,
007600 452                         package=info.package,
0c29cf 453                         registry=info.registry,
MM 454                     )
c2c589 455                 else:
CD 456                     view_renderer = renderer.clone()
457                 if '__view__' in attrs:
458                     view_inst = attrs.pop('__view__')
459                 else:
460                     view_inst = getattr(view, '__original_view__', view)
bf40a3 461                 response = view_renderer.render_view(
0c29cf 462                     request, result, view_inst, context
MM 463                 )
c2c589 464         return response
CD 465
466     return rendered_view
467
0c29cf 468
e4b931 469 rendered_view.options = ('renderer',)
0c29cf 470
e4b931 471
007600 472 def decorated_view(view, info):
MM 473     decorator = info.options.get('decorator')
c2c589 474     if decorator is None:
CD 475         return view
476     return decorator(view)
e4b931 477
0c29cf 478
e4b931 479 decorated_view.options = ('decorator',)
0c29cf 480
a3db3c 481
9e9fa9 482 def csrf_view(view, info):
de3d0c 483     explicit_val = info.options.get('require_csrf')
MM 484     defaults = info.registry.queryUtility(IDefaultCSRFOptions)
485     if defaults is None:
486         default_val = False
487         token = 'csrf_token'
488         header = 'X-CSRF-Token'
489         safe_methods = frozenset(["GET", "HEAD", "OPTIONS", "TRACE"])
17fa5e 490         callback = None
de3d0c 491     else:
MM 492         default_val = defaults.require_csrf
493         token = defaults.token
494         header = defaults.header
495         safe_methods = defaults.safe_methods
17fa5e 496         callback = defaults.callback
e8c66a 497
de3d0c 498     enabled = (
0c29cf 499         explicit_val is True
MM 500         or
e8c66a 501         # fallback to the default val if not explicitly enabled
MM 502         # but only if the view is not an exception view
0c29cf 503         (explicit_val is not False and default_val and not info.exception_only)
de3d0c 504     )
MM 505     # disable if both header and token are disabled
506     enabled = enabled and (token or header)
9e9fa9 507     wrapped_view = view
de3d0c 508     if enabled:
0c29cf 509
9e9fa9 510         def csrf_view(context, request):
0c29cf 511             if request.method not in safe_methods and (
MM 512                 callback is None or callback(request)
17fa5e 513             ):
65dee6 514                 check_csrf_origin(request, raises=True)
de3d0c 515                 check_csrf_token(request, token, header, raises=True)
9e9fa9 516             return view(context, request)
0c29cf 517
9e9fa9 518         wrapped_view = csrf_view
MM 519     return wrapped_view
520
0c29cf 521
9e9fa9 522 csrf_view.options = ('require_csrf',)
MM 523
c231d8 524 VIEW = 'VIEW'
a3db3c 525 INGRESS = 'INGRESS'