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