Package omeroweb :: Module decorators
[hide private]
[frames] | no frames]

Source Code for Module omeroweb.decorators

  1  #!/usr/bin/env python 
  2  # -*- coding: utf-8 -*- 
  3   
  4  # 
  5  # Copyright (C) 2011 University of Dundee & Open Microscopy Environment. 
  6  # All rights reserved. 
  7  # 
  8  # This program is free software: you can redistribute it and/or modify 
  9  # it under the terms of the GNU Affero General Public License as 
 10  # published by the Free Software Foundation, either version 3 of the 
 11  # License, or (at your option) any later version. 
 12  # 
 13  # This program is distributed in the hope that it will be useful, 
 14  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 15  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 16  # GNU Affero General Public License for more details. 
 17  # 
 18  # You should have received a copy of the GNU Affero General Public License 
 19  # along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 20  # 
 21   
 22  """ 
 23  Decorators for use with OMERO.web applications. 
 24  """ 
 25   
 26  import logging 
 27   
 28  from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseForbidden 
 29   
 30  from django.conf import settings 
 31  from django.utils.http import urlencode 
 32  from functools import update_wrapper 
 33  from django.utils import simplejson 
 34  from django.core.urlresolvers import reverse 
 35  from django.core import template_loader 
 36  from django.template import RequestContext 
 37  from django.core.cache import cache 
 38   
 39  from omeroweb.connector import Connector 
 40   
 41  logger = logging.getLogger(__name__) 
 42   
43 -class ConnCleaningHttpResponse(HttpResponse):
44 """Extension of L{HttpResponse} which closes the OMERO connection.""" 45
46 - def close(self):
47 super(ConnCleaningHttpResponse, self).close() 48 try: 49 logger.debug('Closing OMERO connection in %r' % self) 50 if self.conn is not None and self.conn.c is not None: 51 for v in self.conn._proxies.values(): 52 v.close() 53 self.conn.c.closeSession() 54 except: 55 logger.error('Failed to clean up connection.', exc_info=True)
56 57
58 -class login_required(object):
59 """ 60 OMERO.web specific extension of the Django login_required() decorator, 61 https://docs.djangoproject.com/en/dev/topics/auth/, which is responsible 62 for ensuring a valid L{omero.gateway.BlitzGateway} connection. Is 63 configurable by various options. 64 """ 65
66 - def __init__(self, useragent='OMERO.web', isAdmin=False, 67 isGroupOwner=False, doConnectionCleanup=True, omero_group='-1', 68 allowPublic=None):
69 """ 70 Initialises the decorator. 71 """ 72 self.useragent = useragent 73 self.isAdmin = isAdmin 74 self.isGroupOwner = isGroupOwner 75 self.doConnectionCleanup = doConnectionCleanup 76 self.omero_group = omero_group 77 self.allowPublic = allowPublic
78 79 # To make django's method_decorator work, this is required until 80 # python/django sort out how argumented decorator wrapping should work 81 # https://github.com/openmicroscopy/openmicroscopy/pull/1820
82 - def __getattr__(self, name):
83 if name == '__name__': 84 return self.__class__.__name__ 85 else: 86 return super(login_required, self).getattr(name)
87
88 - def get_login_url(self):
89 """The URL that should be redirected to if not logged in.""" 90 return reverse(settings.LOGIN_VIEW)
91 login_url = property(get_login_url) 92
93 - def get_share_connection (self, request, conn, share_id):
94 try: 95 conn.SERVICE_OPTS.setOmeroShare(share_id) 96 share = conn.getShare(share_id) 97 return conn 98 except: 99 logger.error('Error activating share.', exc_info=True) 100 return None
101
102 - def prepare_share_connection(self, request, conn, share_id):
103 """Prepares the share connection if we have a valid share ID.""" 104 # we always need to clear any dirty 'omero.share' values from previous calls 105 conn.SERVICE_OPTS.setOmeroShare() 106 if share_id is None: 107 return None 108 share = conn.getShare(share_id) 109 try: 110 if share.getOwner().id != conn.getUserId(): 111 return self.get_share_connection(request, conn, share_id) 112 except: 113 logger.error('Error retrieving share connection.', exc_info=True) 114 return None
115
116 - def on_not_logged_in(self, request, url, error=None):
117 """Called whenever the user is not logged in.""" 118 if request.is_ajax(): 119 logger.debug('Request is Ajax, returning HTTP 403.') 120 return HttpResponseForbidden() 121 args = {'url': url} 122 logger.debug('Request is not Ajax, redirecting to %s' % self.login_url) 123 return HttpResponseRedirect('%s?%s' % (self.login_url, urlencode(args)))
124
125 - def on_logged_in(self, request, conn):
126 """ 127 Called whenever the users is successfully logged in. 128 Sets the 'omero.group' option if specified in the constructor 129 """ 130 if self.omero_group is not None: 131 conn.SERVICE_OPTS.setOmeroGroup(self.omero_group)
132
133 - def on_share_connection_prepared(self, request, conn_share):
134 """Called whenever a share connection is successfully prepared.""" 135 pass
136
137 - def verify_is_admin(self, conn):
138 """ 139 If we have been requested to by the isAdmin flag, verify the user 140 is an admin and raise an exception if they are not. 141 """ 142 if self.isAdmin and not conn.isAdmin(): 143 raise Http404
144
145 - def verify_is_group_owner(self, conn, gid):
146 """ 147 If we have been requested to by the isGroupOwner flag, verify the user 148 is the owner of the provided group. If no group is provided the user's 149 active session group ownership will be verified. 150 """ 151 if not self.isGroupOwner: 152 return 153 if gid is not None: 154 if not conn.isLeader(gid): 155 raise Http404 156 else: 157 if not conn.isLeader(): 158 raise Http404
159
160 - def is_valid_public_url(self, server_id, request):
161 """ 162 Verifies that the URL for the resource being requested falls within 163 the scope of the OMERO.webpublic URL filter. 164 """ 165 if settings.PUBLIC_ENABLED: 166 if not hasattr(settings, 'PUBLIC_USER'): 167 logger.warn('OMERO.webpublic enabled but public user ' \ 168 '(omero.web.public.user) not set, disabling ' \ 169 'OMERO.webpublic.') 170 settings.PUBLIC_ENABLED = False 171 return False 172 if not hasattr(settings, 'PUBLIC_PASSWORD'): 173 logger.warn('OMERO.webpublic enabled but public user ' \ 174 'password (omero.web.public.password) not set, ' \ 175 'disabling OMERO.webpublic.') 176 settings.PUBLIC_ENABLED = False 177 return False 178 if self.allowPublic is None: 179 return settings.PUBLIC_URL_FILTER.search(request.path) is not None 180 return self.allowPublic 181 return False
182
184 """ 185 Returns the current cached OMERO.webpublic connector or None if 186 nothing has been cached. 187 """ 188 if not settings.PUBLIC_CACHE_ENABLED: 189 return 190 return cache.get(settings.PUBLIC_CACHE_KEY)
191
192 - def set_public_user_connector(self, connector):
193 """Sets the current cached OMERO.webpublic connector.""" 194 if not settings.PUBLIC_CACHE_ENABLED \ 195 or connector.omero_session_key is None: 196 return 197 logger.debug('Setting OMERO.webpublic connector: %r' % connector) 198 cache.set(settings.PUBLIC_CACHE_KEY, connector, 199 settings.PUBLIC_CACHE_TIMEOUT)
200
201 - def get_connection(self, server_id, request):
202 """ 203 Prepares a Blitz connection wrapper (from L{omero.gateway}) for 204 use with a view function. 205 """ 206 connection = self.get_authenticated_connection(server_id, request) 207 is_valid_public_url = self.is_valid_public_url(server_id, request) 208 logger.debug('Is valid public URL? %s' % is_valid_public_url) 209 if connection is None and is_valid_public_url: 210 # If OMERO.webpublic is enabled, pick up a username and 211 # password from configuration and use those credentials to 212 # create a connection. 213 logger.debug('OMERO.webpublic enabled, attempting to login ' \ 214 'with configuration supplied credentials.') 215 if server_id is None: 216 server_id = settings.PUBLIC_SERVER_ID 217 username = settings.PUBLIC_USER 218 password = settings.PUBLIC_PASSWORD 219 is_secure = request.REQUEST.get('ssl', False) 220 logger.debug('Is SSL? %s' % is_secure) 221 # Try and use a cached OMERO.webpublic user session key. 222 public_user_connector = self.get_public_user_connector() 223 if public_user_connector is not None: 224 logger.debug('Attempting to use cached OMERO.webpublic ' \ 225 'connector: %r' % public_user_connector) 226 connection = public_user_connector.join_connection( 227 self.useragent) 228 if connection is not None: 229 request.session['connector'] = public_user_connector 230 logger.debug('Attempt to use cached OMERO.web public ' \ 231 'session key successful!') 232 return connection 233 logger.debug('Attempt to use cached OMERO.web public ' \ 234 'session key failed.') 235 # We don't have a cached OMERO.webpublic user session key, 236 # create a new connection based on the credentials we've been 237 # given. 238 connector = Connector(server_id, is_secure) 239 connection = connector.create_connection( 240 self.useragent, username, password, is_public=True) 241 request.session['connector'] = connector 242 self.set_public_user_connector(connector) 243 elif connection is not None: 244 is_anonymous = connection.isAnonymous() 245 logger.debug('Is anonymous? %s' % is_anonymous) 246 if is_anonymous and not is_valid_public_url: 247 return None 248 return connection
249
250 - def get_authenticated_connection(self, server_id, request):
251 """ 252 Prepares an authenticated Blitz connection wrapper (from 253 L{omero.gateway}) for use with a view function. 254 """ 255 # TODO: Handle previous try_super logic; is it still needed? 256 257 session = request.session 258 request = request.REQUEST 259 is_secure = request.get('ssl', False) 260 logger.debug('Is SSL? %s' % is_secure) 261 connector = session.get('connector', None) 262 logger.debug('Connector: %s' % connector) 263 264 if server_id is None: 265 # If no server id is passed, the db entry will not be used and 266 # instead we'll depend on the request.session and request.REQUEST 267 # values 268 if connector is not None: 269 server_id = connector.server_id 270 else: 271 try: 272 server_id = request['server'] 273 except: 274 logger.debug('No Server ID available.') 275 return None 276 277 # If we have an OMERO session key in our request variables attempt 278 # to make a connection based on those credentials. 279 try: 280 omero_session_key = request['bsession'] 281 connector = Connector(server_id, is_secure) 282 except KeyError: 283 # We do not have an OMERO session key in the current request. 284 pass 285 else: 286 # We have an OMERO session key in the current request use it 287 # to try join an existing connection / OMERO session. 288 logger.debug('Have OMERO session key %s, attempting to join...' % \ 289 omero_session_key) 290 connector.user_id = None 291 connector.omero_session_key = omero_session_key 292 connection = connector.join_connection(self.useragent) 293 session['connector'] = connector 294 return connection 295 296 # An OMERO session is not available, we're either trying to service 297 # a request to a login page or an anonymous request. 298 username = None 299 password = None 300 try: 301 username = request['username'] 302 password = request['password'] 303 except KeyError: 304 if connector is None: 305 logger.debug('No username or password in request, exiting.') 306 # We do not have an OMERO session or a username and password 307 # in the current request and we do not have a valid connector. 308 # Raise an error (return None). 309 return None 310 311 if username is not None and password is not None: 312 # We have a username and password in the current request, or 313 # OMERO.webpublic is enabled and has provided us with a username 314 # and password via configureation. Use them to try and create a 315 # new connection / OMERO session. 316 logger.debug('Creating connection with username and password...') 317 connector = Connector(server_id, is_secure) 318 connection = connector.create_connection( 319 self.useragent, username, password) 320 session['connector'] = connector 321 return connection 322 323 logger.debug('Django session connector: %r' % connector) 324 if connector is not None: 325 # We have a connector, attempt to use it to join an existing 326 # connection / OMERO session. 327 connection = connector.join_connection(self.useragent) 328 if connection is not None: 329 logger.debug('Connector valid, session successfully joined.') 330 return connection 331 # Fall through, we the session we've been asked to join may 332 # be invalid and we may have other credentials as request 333 # variables. 334 logger.debug('Connector is no longer valid, destroying...') 335 del session['connector'] 336 return None 337 338 session['connector'] = connector 339 return connection
340
341 - def __call__(ctx, f):
342 """ 343 Tries to prepare a logged in connection, then calls function and 344 returns the result. 345 """ 346 def wrapped(request, *args, **kwargs): 347 url = request.REQUEST.get('url') 348 if url is None or len(url) == 0: 349 url = request.get_full_path() 350 351 doConnectionCleanup = False 352 353 conn = kwargs.get('conn', None) 354 error = None 355 server_id = kwargs.get('server_id', None) 356 # Short circuit connection retrieval when a connection was 357 # provided to us via 'conn'. This is useful when in testing 358 # mode or when stacking view functions/methods. 359 if conn is None: 360 doConnectionCleanup = ctx.doConnectionCleanup 361 logger.debug('Connection not provided, attempting to get one.') 362 try: 363 conn = ctx.get_connection(server_id, request) 364 except Exception, x: 365 logger.error('Error retrieving connection.', exc_info=True) 366 error = str(x) 367 else: 368 # various configuration & checks only performed on new 'conn' 369 if conn is None: 370 return ctx.on_not_logged_in(request, url, error) 371 else: 372 ctx.on_logged_in(request, conn) 373 ctx.verify_is_admin(conn) 374 ctx.verify_is_group_owner(conn, kwargs.get('gid')) 375 376 share_id = kwargs.get('share_id') 377 conn_share = ctx.prepare_share_connection(request, conn, share_id) 378 if conn_share is not None: 379 ctx.on_share_connection_prepared(request, conn_share) 380 kwargs['conn'] = conn_share 381 else: 382 kwargs['conn'] = conn 383 384 #kwargs['error'] = request.REQUEST.get('error') 385 kwargs['url'] = url 386 387 retval = f(request, *args, **kwargs) 388 try: 389 logger.debug('Doing connection cleanup? %s' % \ 390 doConnectionCleanup) 391 if doConnectionCleanup: 392 if conn is not None and conn.c is not None: 393 for v in conn._proxies.values(): 394 v.close() 395 conn.c.closeSession() 396 except: 397 logger.warn('Failed to clean up connection.', exc_info=True) 398 return retval
399 return update_wrapper(wrapped, f)
400 401
402 -class render_response(object):
403 """ 404 This decorator handles the rendering of view methods to HttpResponse. It expects 405 that wrapped view methods return a dict. This allows: 406 - The template to be specified in the method arguments OR within the view method itself 407 - The dict to be returned as json if required 408 - The request is passed to the template context, as required by some tags etc 409 - A hook is provided for adding additional data to the context, from the L{omero.gateway.BlitzGateway} 410 or from the request. 411 """ 412 413 # To make django's method_decorator work, this is required until 414 # python/django sort out how argumented decorator wrapping should work 415 # https://github.com/openmicroscopy/openmicroscopy/pull/1820
416 - def __getattr__(self, name):
417 if name == '__name__': 418 return self.__class__.__name__ 419 else: 420 return super(render_response, self).getattr(name)
421
422 - def prepare_context(self, request, context, *args, **kwargs):
423 """ Hook for adding additional data to the context dict """ 424 pass
425 426
427 - def __call__(ctx, f):
428 """ Here we wrap the view method f and return the wrapped method """ 429 430 def wrapper(request, *args, **kwargs): 431 """ Wrapper calls the view function, processes the result and returns HttpResponse """ 432 433 # call the view function itself... 434 context = f(request, *args, **kwargs) 435 436 # if we happen to have a Response, return it 437 if isinstance(context, HttpResponse): 438 return context 439 440 # get template from view dict. Can be overridden from the **kwargs 441 template = 'template' in context and context['template'] or None 442 template = kwargs.get('template', template) 443 logger.debug("Rendering template: %s" % template) 444 445 # allows us to return the dict as json (NB: BlitzGateway objects don't serialize) 446 if template is None or template == 'json': 447 json_data = simplejson.dumps(context) 448 return HttpResponse(json_data, mimetype='application/javascript') 449 else: 450 # allow additional processing of context dict 451 ctx.prepare_context(request, context, *args, **kwargs) 452 t = template_loader.get_template(template) 453 c = RequestContext(request, context) 454 return HttpResponse(t.render(c))
455 return update_wrapper(wrapper, f)
456