changeset 37:695e59e5a4a7

separate bitsyauth into its own module
author k0s <k0scist@gmail.com>
date Mon, 02 Nov 2009 21:43:05 -0500
parents 7e7fbdc64cc1
children de3ea3a280ae
files bitsyblog/bitsyauth.py bitsyblog/factory.py setup.py
diffstat 3 files changed, 2 insertions(+), 373 deletions(-) [+]
line wrap: on
line diff
--- a/bitsyblog/bitsyauth.py	Sun Nov 01 13:30:18 2009 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,368 +0,0 @@
-import markup
-import random
-import re
-import sys
-
-from cStringIO import StringIO
-from markup.form import Form
-from paste.auth import basic, cookie, digest, form, multi, auth_tkt
-from webob import Request, Response, exc
-
-try:
-    from skimpyGimpy import skimpyAPI
-    CAPTCHA = True
-except ImportError:
-    CAPTCHA = False
-
-dictionary_file = '/usr/share/dict/american-english'
-
-def random_word():
-    """generate a random word for CAPTCHA auth"""
-    min_length = 5 # minimum word length
-    if not globals().has_key('dictionary'):
-        # read the dictionary -- this may be platform dependent
-        # XXX could use a backup dictionary
-        _dictionary = file(dictionary_file).readlines()
-        _dictionary = [ i.strip() for i in _dictionary ]
-        _dictionary = [ i.lower() for i in _dictionary
-                        if i.isalpha() and i > min_length ]
-        globals()['dictionary'] = _dictionary
-    return random.Random().choice(dictionary)
-
-class BitsyAuthInnerWare(object):
-    """inner auth;  does login checking"""
-
-    def __init__(self, app, passwords, newuser=None, site=None, realm=None):
-        """a simple reimplementation of auth
-        * app: the WSGI app to be wrapped
-        * passwords: callable that return a dictionary of {'user': 'password'}
-        * newuser: callable to make a new user, taking name + pw
-        * site: name of the site
-        * realm: realm for HTTP digest authentication
-        """
-
-        self.app = app
-        self.passwords = passwords
-        self.site = site or ''
-        self.realm = realm or self.site
-        self.captcha = True
-        self.redirect_to = '/' # redirect to site root        
-        self.urls = { 'login': '/login', 'join': '/join', }
-        self.keys = {} # keys, words for CAPTCHA request
-
-        self.content_type = { 'image_captcha': 'image/png',
-                              'wav_captcha': 'audio/wav' }
-
-        if newuser:
-            self.newuser = newuser
-        else:
-            self.urls.pop('join') # don't do joining
-
-        # WSGI app securely wrapped
-        self.wrapped_app = self.security_wrapper()
-
-        if not CAPTCHA:
-            self.captcha = False
-        
-    ### WSGI/HTTP layer
-
-    def __call__(self, environ, start_response):
-
-        self.request = Request(environ)
-        self.request.path_info = self.request.path_info.rstrip('/')
-
-        # URLs intrinsic to BitsyAuth
-        if self.request.path_info == '/logout':
-            response = self.redirect()
-            return response(self.request.environ, start_response)
-
-        if self.request.path_info in self.url_lookup():
-            response = self.make_response()
-            return response(self.request.environ, start_response)
-
-        # digest auth
-        if self.request.headers.has_key('Authorization'):
-            return self.wrapped_app(self.request.environ, start_response)
-
-        response = self.request.get_response(self.app)
-        # respond to 401s
-        if response.status_int == 401: # Unauthorized
-            if self.request.environ.get('REMOTE_USER'):
-                return exc.HTTPForbidden()
-            else:
-                response = self.request.get_response(self.wrapped_app)
-
-        user = self.request.environ.get('REMOTE_USER')
-        if user:
-            self.request.environ['paste.auth_tkt.set_user'](user)
-
-        return response(self.request.environ, start_response)
-
-    ### authentication function
-
-    def digest_authfunc(self, environ, realm, user):
-        return self.passwords()[user] # passwords stored in m5 digest
-
-    def authfunc(self, environ, user, password):
-        return self.hash(user, password) == self.passwords()[user]
-
-    def hash(self, user, password):
-        # use md5 digest for now
-        return digest.digest_password(self.realm, user, password)
-
-    def security_wrapper(self):
-        """return the app securely wrapped"""
-
-        multi_auth = multi.MultiHandler(self.app)
-
-        # digest authentication
-        multi_auth.add_method('digest', digest.middleware,
-                              self.realm, self.digest_authfunc)
-        multi_auth.set_query_argument('digest', key='auth')
-
-        # form authentication
-        template = self.login(wrap=True, action='%s')
-        multi_auth.add_method('form', form.middleware, self.authfunc,
-                              template=template)
-        multi_auth.set_default('form')
-
-        return multi_auth
-
-        # might have to wrap cookie.middleware(BitsyAuth(multi(app))) ::shrug::
-        return cookie.middleware(multi_auth)
-
-    ### methods dealing with intrinsic URLs
-
-    def url_lookup(self):
-        retval = dict([ (value, key) for key, value
-                        in self.urls.items() ])
-        if self.captcha:
-            retval.update(dict([(('/join/%s.png' % key), 'image_captcha')
-                                for key in self.keys]))
-        return retval
-        
-    def get_response(self, text, content_type='text/html'):
-        res = Response(content_type=content_type, body=text)
-        res.content_length = len(res.body)
-        return res
-
-    def make_response(self):
-        url_lookup = self.url_lookup()
-        path = self.request.path_info
-        assert path in url_lookup
-
-        # login and join shouldn't be accessible when logged in
-        if self.request.environ.get('REMOTE_USER'):
-            return self.redirect("You are already logged in")
-
-        handler = url_lookup[path]
-        function = getattr(self, handler)
-
-        if self.request.method == 'GET':
-            # XXX could/should do this with decorators            
-            return self.get_response(function(wrap=True),
-                                     content_type=self.content_type.get(handler,'text/html'))
-        if self.request.method == 'POST':
-            post_func = getattr(self, handler + "_post")
-            errors = post_func()
-            if errors:
-                return self.get_response(function(errors=errors, wrap=True))
-            else:
-                return self.redirect("Welcome!")
-
-    def redirect(self, message=''):
-        """redirect from instrinsic urls"""
-        return exc.HTTPSeeOther(message, location=self.redirect_to)
-
-    def image_captcha(self, wrap=True):
-        """return data for the image"""
-        key = self.request.path_info.split('/join/')[-1]
-        key = int(key.split('.png')[0])
-        return skimpyAPI.Png(self.keys[key], scale=3.0).data()
-                
-    ### forms and their display methods
-
-    ### login
-
-    def login_form(self, referer=None, action=None):
-        if action is None:
-            action = self.urls['login']
-        form = Form(action='', submit='Login')
-        form.add_element('textfield', 'Name', input_name='username')
-        form.add_element('password', 'Password', input_name='password')
-        if referer is not None:
-            form.add_element('hidden', 'referer', value=referer)
-        return form
-
-    def login(self, errors=None, wrap=False, action=None):
-        """login div"""
-        form = self.login_form(action=action)
-        join = self.urls.get('join')
-        retval = form(errors)
-        if join:        
-            retval += '<br/>\n' + markup.a('join', href="%s" % join)
-        retval = markup.div(retval)
-        if wrap:
-            title = 'login'
-            if self.site:
-                pagetitle = '%s - %s' % (title, self.site)
-            retval = markup.wrap(markup.h1(title.title()) + retval,
-                                 pagetitle=pagetitle)
-
-        return retval
-
-    def login_post(self):
-        """handle a login POST request"""
-        user = self.request.POST.get('username')
-        password = self.request.POST.get('password')
-        passwords = self.passwords()
-        error = False
-        if user not in passwords:
-            error = True
-        else:
-            error = not self.authfunc(self.request.environ, user, password)
-        if error:
-            return { 'Name': 'Wrong username or password' }
-        self.request.environ['REMOTE_USER'] = user
-        self.request.environ['paste.auth_tkt.set_user'](user)
-
-    ### join
-
-    def captcha_pre(self, word, key):
-        """CAPTCHA with pre-formatted text"""
-        return skimpyAPI.Pre(word, scale=1.2).data()
-
-    def captcha_png(self, word, key):
-        """CAPTCHA with a PNG image"""
-        return markup.image('/join/%s.png' % key)
-
-    def join_form(self):
-        captcha = ''
-        if self.captcha:
-            # data for CAPTCHA
-            key = random.Random().randint(0, sys.maxint)
-            word = random_word()
-
-            self.keys[key] = word
-
-            captcha = StringIO()
-
-            captcha_text = "Please type the word below so I know you're not a computer:"
-            captcha_help = "(please %s if the page is unreadable)" % markup.link('/join?captcha=image', 'go here')
-
-            print >> captcha, markup.p('%s<br/> %s' % (captcha_text, 
-                                                       markup.i(captcha_help)))
-
-            # determine type of CAPTCHA
-            captchas = ' '.join(self.request.GET.getall('captcha'))
-            if not captchas:
-                captchas = 'pre'
-                
-            captcha_funcs=dict(pre=self.captcha_pre,
-                               image=self.captcha_png,)
-            captchas = [ captcha_funcs[i](word, key) for i in captchas.split()
-                         if i in captcha_funcs ]
-            captchas = '\n'.join([markup.p(i) for i in captchas])
-            print >> captcha, captchas
-            
-            print >> captcha, markup.p(markup.input(None, **dict(name='captcha', type='text')))
-            
-            captcha = captcha.getvalue()
-
-        form = Form(action=self.urls['join'], submit='Join', post_html=captcha)
-        form.add_element('textfield', 'Name')
-        form.add_password_confirmation()
-        form.add_element('hidden', 'key', value=str(key))
-        return form
-
-    def join(self, errors=None, wrap=False):
-        """join div or page if wrap"""
-        form = self.join_form()
-        retval = markup.div(form(errors))
-        if wrap:
-            pagetitle = title = 'join'
-            if self.site:
-                pagetitle = '%s - %s' % (title, self.site)
-            if self.captcha:
-                errors = errors or {}
-                captcha_err = errors.get('CAPTCHA', '')
-                if captcha_err:
-                    captcha_err = markup.p(markup.em(captcha_err),
-                                           **{'class': 'error'})
-            retval = markup.wrap(markup.h1(title.title()) + captcha_err + retval,
-                                 pagetitle=pagetitle)
-        return retval
-
-    def join_post(self):
-        """handle a join POST request"""
-        form = self.join_form()
-        errors = form.validate(self.request.POST)
-
-        # validate captcha
-        if CAPTCHA:
-            key = self.request.POST.get('key')
-            try:
-                key = int(key)
-            except ValueError:
-                key = None
-            if not key:
-                errors['CAPTCHA'] = 'Please type the funky looking word'
-            word = self.keys.pop(key, None)
-            if not word:
-                errors['CAPTCHA'] = 'Please type the funky looking word'
-            if word != self.request.POST.get('captcha','').lower():
-                errors['CAPTCHA'] = 'Sorry, you typed the wrong word'
-        
-        name = self.request.POST.get('Name', '')
-        if not name:
-            if not errors.has_key('Name'):
-                errors['Name'] = []
-            errors['Name'].append('Please enter a user name')
-        if name in self.passwords():
-            if not errors.has_key('Name'):
-                errors['Name'] = []
-            errors['Name'].append('The name %s is already taken' % name)
-
-        if not errors: # create a new user
-            self.newuser(name,
-                         self.hash(name, self.request.POST['Password']))
-            self.request.environ['REMOTE_USER'] = name # login the new user
-            self.request.environ['paste.auth_tkt.set_user'](name)
-        
-        return errors
-
-class BitsyAuth(object):
-    """outer middleware for auth;  does the cookie handling and wrapping"""
-    
-    def __init__(self, app, global_conf, passwords, newuser, site='', secret='secret'):
-        self.app = app
-        self.path = '/logout'
-        self.cookie = '__ac'
-        auth = BitsyAuthInnerWare(app, passwords=passwords, newuser=newuser, site=site)
-        self.hash = auth.hash
-        # paste.auth.cookie
-        #        self.cookie_handler = cookie.middleware(auth, cookie_name=self.cookie, timeout=90) # minutes
-
-        # paste.auth.auth_tkt
-        self.cookie_handler = auth_tkt.make_auth_tkt_middleware(auth, global_conf, secret, cookie_name=self.cookie, logout_path='/logout')
-
-    def __call__(self, environ, start_response):
-        if environ['PATH_INFO'] == '/logout':
-            pass        
-        return self.cookie_handler(environ, start_response)
-
-    def logout(self, environ):
-        req = Request(environ)
-        keys = [ 'REMOTE_USER' ]
-        #        keys = [ 'REMOTE_USER', 'AUTH_TYPE', 'paste.auth.cookie', 'paste.cookies', 'HTTP_COOKIE' ]  # XXX zealous kill
-        for key in keys:
-            req.environ.pop(key, None)
-
-        body = '<html><head><title>logout</title></head><body>logout</body></html>'
-        res = Response(content_type='text/html', body=body)
-        res.content_length = len(res.body)
-        req.cookies.pop(self.cookie, None)
-        res.delete_cookie(self.cookie)
-        res.unset_cookie(self.cookie)
-        return res(environ, start_response)
-        
--- a/bitsyblog/factory.py	Sun Nov 01 13:30:18 2009 -0500
+++ b/bitsyblog/factory.py	Mon Nov 02 21:43:05 2009 -0500
@@ -1,4 +1,4 @@
-from bitsyauth import BitsyAuth
+from bitsyauth.bitsyauth import BitsyAuth
 from bitsyblog import BitsyBlog, BitsierBlog
 from getpass import getpass 
 from paste.httpexceptions import HTTPExceptionHandler
--- a/setup.py	Sun Nov 01 13:30:18 2009 -0500
+++ b/setup.py	Mon Nov 02 21:43:05 2009 -0500
@@ -31,17 +31,14 @@
           'Paste',
           'PasteScript',
           'python-dateutil',
-          'markup',
-          'skimpygimpy',
           'PyRSS2Gen',
           'docutils',
           'genshi',
+          'bitsyauth'
 #          'lxml',
       ],
       dependency_links=[ 
-        'https://svn.openplans.org/svn/standalone/markup#egg=markup',
         'http://svn.pythonpaste.org/Paste/trunk#egg=Paste',
-        'http://downloads.sourceforge.net/skimpygimpy/skimpyGimpy_1_3.zip#egg=skimpygimpy',
         'http://www.dalkescientific.com/Python/PyRSS2Gen-1.0.0.tar.gz#egg=PyRSS2Gen'
         ],
       entry_points="""