From 523341ff369be67b3cde0ce1530d93af218c4674 Mon Sep 17 00:00:00 2001 From: Rasmus Andersson Date: Fri, 31 Jul 2009 20:43:09 +0200 Subject: [PATCH] added class-based leaf filters --- lib/smisk/mvc/decorators.py | 151 ++++++++++++++++------------ lib/smisk/mvc/filters.py | 192 ++++++++++++++++++++++++++---------- 2 files changed, 229 insertions(+), 114 deletions(-) diff --git a/lib/smisk/mvc/decorators.py b/lib/smisk/mvc/decorators.py index 6dcae2c..7971bf0 100644 --- a/lib/smisk/mvc/decorators.py +++ b/lib/smisk/mvc/decorators.py @@ -6,71 +6,98 @@ __all__ = ['expose', 'hide', 'leaf_filter'] def expose(slug=None, template=None, formats=None, delegates=False, methods=None): - '''Explicitly expose a function, optionally configure how it is exposed. - ''' - def entangle(func): - # Note: We do not add default values (i.e. methods = None) because we can not gurantee - # that every leaf called has been @expose'd, so we need to check for attribute - # existance anyway outside of this scope. - - # Slug - if slug is not None: - # Slug might be the function if decorator called without () - if isinstance(slug, basestring): - func.slug = unicode(slug) - elif isinstance(slug, unicode): - func.slug = slug - - # Delegates to other leafs up the class hierarchy? - if delegates is not None: - func.delegates = bool(delegates) - - # Template - if template is not None: - func.template = unicode(template) - - # Formats - if formats is not None: - if isinstance(formats, (list, tuple)): - func.formats = formats - else: - func.formats = (formats,) - for s in func.formats: - if not isinstance(s, basestring): - raise TypeError('formats must be a tuple or list of strings, alternatively a single string') - - # Methods - if methods is not None: - if isinstance(methods, (list, tuple)): - func.methods = methods - else: - func.methods = (methods,) - for s in func.methods: - if not isinstance(s, basestring): - raise TypeError('methods must be a tuple or list of strings, alternatively a single string') - func.methods = [s.upper() for s in func.methods] - - return func - - if isinstance(slug, (types.FunctionType, types.MethodType)): - return entangle(slug) - return entangle + '''Explicitly expose a function, optionally configure how it is exposed. + ''' + def entangle(func): + # Note: We do not add default values (i.e. methods = None) because we can not gurantee + # that every leaf called has been @expose'd, so we need to check for attribute + # existance anyway outside of this scope. + + # Slug + if slug is not None: + # Slug might be the function if decorator called without () + if isinstance(slug, basestring): + func.slug = unicode(slug) + elif isinstance(slug, unicode): + func.slug = slug + + # Delegates to other leafs up the class hierarchy? + if delegates is not None: + func.delegates = bool(delegates) + + # Template + if template is not None: + func.template = unicode(template) + + # Formats + if formats is not None: + if isinstance(formats, (list, tuple)): + func.formats = formats + else: + func.formats = (formats,) + for s in func.formats: + if not isinstance(s, basestring): + raise TypeError('formats must be a tuple or list of strings, alternatively a single string') + + # Methods + if methods is not None: + if isinstance(methods, (list, tuple)): + func.methods = methods + else: + func.methods = (methods,) + for s in func.methods: + if not isinstance(s, basestring): + raise TypeError('methods must be a tuple or list of strings, alternatively a single string') + func.methods = [s.upper() for s in func.methods] + + return func + + if isinstance(slug, (types.FunctionType, types.MethodType)): + return entangle(slug) + return entangle def hide(func=None): - def entangle(func): - func.hidden = True - return func - if isinstance(func, (types.FunctionType, types.MethodType)): - return entangle(func) - return entangle + def entangle(func): + func.hidden = True + return func + if isinstance(func, (types.FunctionType, types.MethodType)): + return entangle(func) + return entangle def leaf_filter(filter): - def entangle(leaf, *va, **kw): - def f(*va, **kw): - return filter(leaf, *va, **kw) - f.parent_leaf = leaf - return f - entangle.__name__ = filter.__name__+'_leaf_filter' - return entangle + def entangle(leaf, *va, **kw): + def f(*va, **kw): + return filter(leaf, *va, **kw) + f.parent_leaf = leaf + return f + entangle.__name__ = filter.__name__+'_leaf_filter' + return entangle + +class LeafFilter(object): + '''Leaf filter factory baseclass + + Used like this: + + myf = MySubclass() + ... + @myf + def some_leaf... + + ''' + def filter_proxy(self, leaf, filter): + def f(*va, **kw): + return filter(leaf, *va, **kw) + f.parent_leaf = leaf + f.__name__ = leaf.__name__+'_with_'+self.__class__.__name__+'_'+filter.__name__ + return f + + def __call__(self, leaf): + return self.filter_proxy(leaf, self.filter) + + def filter(self): + raise NotImplementedError('filter') + + + diff --git a/lib/smisk/mvc/filters.py b/lib/smisk/mvc/filters.py index 934d5c3..cbb1281 100644 --- a/lib/smisk/mvc/filters.py +++ b/lib/smisk/mvc/filters.py @@ -3,7 +3,8 @@ ''' import smisk.core import smisk.mvc.http as http -from smisk.mvc.decorators import leaf_filter +from smisk.core import Application as App +from smisk.mvc.decorators import leaf_filter, LeafFilter from time import time try: from hashlib import md5 @@ -15,47 +16,143 @@ @leaf_filter def confirm(leaf, *va, **params): - '''Requires the client to resend the request, passing a one-time - valid token as confirmation. - ''' - req = smisk.core.Application.current.request - - # Validate confirmation if available - params['confirmed'] = False - try: - if params['confirm_token'] == req.session['confirm_token']: - params['confirmed'] = True - except (KeyError, TypeError): - pass - - # Make sure we don't keep confirm_token in params - try: del params['confirm_token'] - except: pass - - # Call leaf - rsp = leaf(*va, **params) + '''Requires the client to resend the request, passing a one-time + valid token as confirmation. + ''' + req = App.current.request + + # Validate confirmation if available + params['confirmed'] = False + try: + if params['confirm_token'] == req.session['confirm_token']: + params['confirmed'] = True + except (KeyError, TypeError): + pass + + # Make sure we don't keep confirm_token in params + try: del params['confirm_token'] + except: pass + + # Call leaf + rsp = leaf(*va, **params) + + # Add confirmation token if still unconfirmed + if not params['confirmed']: + if not isinstance(req.session, dict): + req.session = {} + confirm_token = smisk.core.uid() + req.session['confirm_token'] = confirm_token + if not isinstance(rsp, dict): + rsp = {} + rsp['confirm_token'] = confirm_token + else: + # Remove confirmation tokens + try: del req.session['confirm_token'] + except: pass + try: del rsp['confirm_token'] + except: pass + + # Return response + return rsp - # Add confirmation token if still unconfirmed - if not params['confirmed']: - if not isinstance(req.session, dict): - req.session = {} - confirm_token = smisk.core.uid() - req.session['confirm_token'] = confirm_token - if not isinstance(rsp, dict): - rsp = {} - rsp['confirm_token'] = confirm_token - else: - # Remove confirmation tokens - try: del req.session['confirm_token'] - except: pass - try: del rsp['confirm_token'] - except: pass - # Return response - return rsp +class AuthFilter(LeafFilter): + authorized_param = 'authorized_user' + create_leaf = None + + def __init__(self, create_leaf=None, authorized_param=None): + if create_leaf: + self.create_leaf = create_leaf + if authorized_param: + self.authorized_param = authorized_param + + @property + def authorized(self): + raise NotImplementedError('authorized') + + @property + def have_valid_create_leaf(self): + return self.create_leaf and (isinstance(self.create_leaf, basestring) or control.uri_for(self.create_leaf) is not None) + + def will_authorize(self, *va, **kw): + pass + + def did_authorize(self, user, rsp, exc): + pass + + def did_fail(self): + if not self.have_valid_create_leaf: + raise http.Unauthorized() + redirect_to(self.create_leaf) + + def create(self, leaf): + return self.filter_proxy(leaf, self._create) + + def _create(self, leaf, *va, **kw): + exc = None + rsp = None + self.will_authorize(va, kw) + try: + rsp = leaf(*va, **kw) + except http.HTTPExc, e: + exc = e + if rsp and isinstance(rsp, dict) and self.authorized_param in rsp and rsp[self.authorized_param]: + self.did_authorize(rsp[self.authorized_param], rsp, exc) + if exc: + raise exc + return rsp + + def require(self, leaf): + return self.filter_proxy(leaf, self._require) + + __call__ = require + + def _require(self, leaf, *va, **kw): + if not self.authorized: + self.did_fail() + return leaf(*va, **kw) + + def destroy(self, leaf): + return self.filter_proxy(leaf, self._destroy) + + def _destroy(self, leaf, *va, **kw): + App.current.request.session = None + return leaf(*va, **kw) + +class SessionAuthFilter(AuthFilter): + referrer_param = 'auth_referrer' + + @property + def session(self): + if not isinstance(App.current.request.session, dict): + App.current.request.session = {} + return App.current.request.session + + @property + def authorized(self): + if isinstance(App.current.request.session, dict): + return App.current.request.session.get(self.authorized_param) + + def will_authorize(self, va, kw): + if self.referrer_param in kw: + self.session[self.referrer_param] = kw[self.referrer_param] + del kw[self.referrer_param] + + def did_authorize(self, user, rsp, exc): + self.session[self.authorized_param] = user + if self.referrer_param in self.session: + referrer = self.session[self.referrer_param] + del self.session[self.referrer_param] + redirect_to(referrer) + + def did_fail(self): + if not self.have_valid_create_leaf: + raise http.Unauthorized() + redirect_to(self.create_leaf, **{self.referrer_param: App.current.request.url}) + -class DigestAuthFilter(object): +class DigestAuthFilter(LeafFilter): '''HTTP Digest authorization filter. ''' required = ['username', 'realm', 'nonce', 'uri', 'response'] @@ -67,14 +164,13 @@ def __init__(self, realm, users=None, require_authentication=True): self.users = users self.require_authentication = require_authentication self.leaf = None - self.app = smisk.core.Application.current def respond_unauthorized(self, send401=True, *va, **kw): if not send401: kw['authorized_user'] = None return self.leaf(*va, **kw) # send response - self.app.response.headers.append( + App.current.response.headers.append( 'WWW-Authenticate: Digest realm="%s", nonce="%s", algorithm="MD5", qop="auth"' % (self.realm, self.create_nonce()) ) @@ -99,17 +195,17 @@ def KD(self, secret, data): def filter(self, *va, **kw): # did the client even try to authenticate? - if 'HTTP_AUTHORIZATION' not in self.app.request.env: + if 'HTTP_AUTHORIZATION' not in App.current.request.env: return self.respond_unauthorized(self.require_authentication, *va, **kw) # not digest auth? - if not self.app.request.env['HTTP_AUTHORIZATION'].startswith('Digest '): + if not App.current.request.env['HTTP_AUTHORIZATION'].startswith('Digest '): raise http.BadRequest('only Digest authorization is allowed') # parse params = {} required = len(self.required) - for k, v in [i.split("=", 1) for i in self.app.request.env['HTTP_AUTHORIZATION'][7:].strip().split(',')]: + for k, v in [i.split("=", 1) for i in App.current.request.env['HTTP_AUTHORIZATION'][7:].strip().split(',')]: k = k.strip() params[k] = v.strip().replace('"', '') if k in self.required: @@ -125,7 +221,7 @@ def filter(self, *va, **kw): # build A1 and A2 A1 = '%s:%s:%s' % (params['username'], self.realm, self.users[params['username']]) - A2 = self.app.request.method + ':' + self.app.request.url.uri + A2 = App.current.request.method + ':' + App.current.request.url.uri # build expected response expected_response = None @@ -158,11 +254,3 @@ def filter(self, *va, **kw): # authorized -- delegate further down the filter chain return self.respond_authorized(params['username'], *va, **kw) - def __call__(self, leaf): - self.leaf = leaf - def f(*va, **kw): - return self.filter(*va, **kw) - f.parent_leaf = leaf - f.__name__ = leaf.__name__+'_with_DigestAuthFilter' - return f -