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