Require invirt-web POSTs to have proper referers
[invirt/packages/invirt-web.git] / code / view.py
1 import os, sys
2
3 import cherrypy
4 from mako.template import Template
5 from mako.lookup import TemplateLookup
6 import simplejson
7 import datetime, decimal
8 from StringIO import StringIO
9 from invirt.config import structs as config
10 import invirt.database
11 from webcommon import State
12
13
14 class MakoHandler(cherrypy.dispatch.LateParamPageHandler):
15     """Callable which processes a dictionary, returning the rendered body."""
16     
17     def __init__(self, template, next_handler,
18                  content_type='text/html; charset=utf-8'):
19         self.template = template
20         self.next_handler = next_handler
21         self.content_type = content_type
22     
23     def __call__(self):
24         env = globals().copy()
25         env.update(self.next_handler())
26         cherrypy.response.headers['Content-Type'] = self.content_type
27         return self.template.render(**env)
28         
29
30 class MakoLoader(object):
31     
32     def __init__(self):
33         self.lookups = {}
34
35     def get_lookup(self, directories, module_directory=None,
36                      collection_size=-1, imports=[], **kwargs):
37         # Find the appropriate template lookup.
38         key = (tuple(directories), module_directory)
39         try:
40             lookup = self.lookups[key]
41         except KeyError:
42             lookup = TemplateLookup(directories=directories,
43                                     module_directory=module_directory,
44                                     collection_size=collection_size,
45                                     default_filters=['decode.utf8'],
46                                     input_encoding='utf-8',
47                                     output_encoding='utf-8',
48                                     imports=imports,
49                                     )
50             self.lookups[key] = lookup
51         return lookup
52
53     def __call__(self, filename, directories, module_directory=None,
54                  collection_size=-1, content_type='text/html; charset=utf-8',
55                  imports=[]):
56         cherrypy.request.lookup = lookup = self.get_lookup(
57             directories, module_directory, collection_size, imports)
58         cherrypy.request.template = t = lookup.get_template(filename)
59         cherrypy.request.handler = MakoHandler(
60             t, cherrypy.request.handler, content_type)
61
62 cherrypy.tools.mako = cherrypy.Tool('on_start_resource', MakoLoader())
63
64
65 def revertStandardError():
66     """Move stderr to stdout, and return the contents of the old stderr."""
67     errio = sys.stderr
68     if not isinstance(errio, StringIO):
69         return ''
70     sys.stderr = sys.stdout
71     errio.seek(0)
72     return errio.read()
73
74
75 def catchStderr():
76     old_handler = cherrypy.request.handler
77     def wrapper(*args, **kwargs):
78         sys.stderr = StringIO()
79         ret = old_handler(*args, **kwargs)
80         e = revertStandardError()
81         if e:
82             if isinstance(ret, dict):
83                 ret["error_text"] = e
84         return ret
85     if old_handler:
86         cherrypy.request.handler = wrapper
87
88 cherrypy.tools.catch_stderr = cherrypy.Tool('before_handler', catchStderr)
89
90
91 class JSONEncoder(simplejson.JSONEncoder):
92         def default(self, obj):
93                 if isinstance(obj, datetime.datetime):
94                         return str(obj)
95                 elif isinstance(obj, decimal.Decimal):
96                         return float(obj)
97                 else:
98                         return simplejson.JSONEncoder.default(self, obj)
99
100
101 def jsonify_tool_callback(*args, **kwargs):
102     if not cherrypy.request.cached:
103         response = cherrypy.response
104         response.headers['Content-Type'] = 'text/javascript'
105         response.body = JSONEncoder().iterencode(response.body)
106
107 cherrypy.tools.jsonify = cherrypy.Tool('before_finalize',
108                                        jsonify_tool_callback, priority=30)
109
110
111 def require_login():
112     """If the user isn't logged in, raise 403 with an error."""
113     if cherrypy.request.login is False:
114         raise cherrypy.HTTPError(403,
115             "You are not authorized to access that resource")
116
117 cherrypy.tools.require_login = cherrypy.Tool('on_start_resource',
118                                              require_login, priority=150)
119
120
121 def require_POST():
122     """If the request isn't a POST request, raise 405 Method Not Allowed"""
123     if cherrypy.request.method != "POST":
124         raise cherrypy.HTTPError(405,
125                                  "You must submit this request with POST")
126     if not cherrypy.request.headers.get('Referer', '').startswith('https://' + config.web.hostname):
127         raise cherrypy.HTTPError(403, "This form is only usable when submitted from another page on this site. If you receive this message in error, check your browser's Referer settings.")
128
129 cherrypy.tools.require_POST = cherrypy.Tool('on_start_resource',
130                                             require_POST, priority=150)
131
132
133 def remote_user_login():
134     """Get remote user from SSL or GSSAPI, and store in request object.
135
136 Get the current user based on environment variables set by SSL or
137 GSSAPI, and store it in the attribute cherrpy.request.login.
138
139 Per the CherryPy API (http://www.cherrypy.org/wiki/RequestObject#login),
140 the attribute is set to the username on successful login, to False on
141 failed login, and is left at None if the user attempted no authentication.
142 """
143     environ = cherrypy.request.wsgi_environ
144     user = environ.get('REMOTE_USER')
145     if user is None:
146         return
147     if environ.get('AUTH_TYPE') == 'Negotiate':
148         # Convert the krb5 principal into a krb4 username
149         if not user.endswith('@%s' % config.kerberos.realm):
150             cherrypy.request.login = False # failed to log in
151         else:
152             cherrypy.request.login = user.split('@')[0].replace('/', '.')
153     else:
154         cherrypy.request.login = user
155
156 cherrypy.tools.remote_user_login = cherrypy.Tool('on_start_resource',
157                                                  remote_user_login, priority=50)
158
159
160 def invirtwebstate_init():
161     """Initialize the cherrypy.request.state object from Invirt"""
162     if not hasattr(cherrypy.request, "state"):
163         cherrypy.request.state = State(cherrypy.request.login)
164
165 cherrypy.tools.invirtwebstate = cherrypy.Tool('on_start_resource',
166                                               invirtwebstate_init, priority=100)
167
168
169 cherrypy.tools.clear_db_cache = cherrypy.Tool('on_start_resource', invirt.database.clear_cache)
170
171
172 class View(object):
173     _cp_config = {'tools.mako.directories':
174                       [os.path.join(os.path.dirname(__file__),'templates')]}