This repository has been archived on 2023-02-21. You can view files and clone it, but cannot push or open issues or pull requests.
mandaye/mandaye/auth/authform.py

362 lines
15 KiB
Python

"""
Dispatcher for basic auth form authentifications
"""
import Cookie
import base64
import re
import traceback
import urllib
import mandaye
from cookielib import CookieJar
from datetime import datetime
from lxml.html import fromstring
from urlparse import parse_qs
from mandaye import config
from mandaye.db import sql_session
from mandaye.models import Site, ExtUser, LocalUser
from mandaye.log import logger
from mandaye.http import HTTPResponse, HTTPHeader, HTTPRequest
from mandaye.response import _500, _302, _401
from mandaye.response import template_response
from mandaye.server import get_response
try:
from Crypto.Cipher import AES
except ImportError:
config.encrypt_ext_password = False
class AuthForm(object):
def __init__(self, form_values, site_name):
"""
form_values: dict example :
{
'form_action': '/myform',
'form_url': '/myform',
'form_attrs': { 'name': 'form40', },
'username_field': 'user',
'password_field': 'pwd'
}
site_name: str with the site name
"""
if not form_values.has_key('form_headers'):
form_values['form_headers'] = {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'Mozilla/5.0 Mandaye/0.0'
}
if not form_values.has_key('form_url') or \
not form_values.has_key('form_attrs') or \
not form_values.has_key('username_field') or \
not form_values.has_key('password_field'):
logger.critical("Bad configuration: AuthForm form_values dict must have \
this keys: form_url, form_attrs, username_field and password_field")
# TODO: manage Mandaye exceptions
raise BaseException, 'AuthForm bad configuration'
self.form_url = form_values['form_url']
self.form_values = form_values
self.site_name = site_name
def _encrypt_pwd(self, pwd):
""" This method allows you to encrypt a password
To use this feature you muste set encrypt_ext_password to True
in your configuration and set a secret in encrypt_secret
pwd: the password you want to encrypt
return None if encryption failed
"""
logger.debug("Encrypt password")
enc_pwd = pwd
if config.encrypt_secret:
try:
cipher = AES.new(config.encrypt_secret, AES.MODE_CFB)
enc_pwd = cipher.encrypt(pwd)
enc_pwd = base64.b64encode(enc_pwd)
except Exception, e:
if config.debug:
traceback.print_exc()
logger.warning('Password encrypting failed %s' % e)
else:
logger.warning("You must set a secret to use pwd encryption")
return enc_pwd
def _decrypt_pwd(self, enc_pwd):
""" This method allows you to dencrypt a password encrypt with
_encrypt_pwd method. To use this feature you muste set
encrypt_ext_password to True in your configuration and
set a secret in encrypt_secret
enc_pwd: your encoded password
return None if encryption failed
"""
logger.debug("Decrypt password")
pwd = enc_pwd
if config.encrypt_secret:
try:
cipher = AES.new(config.encrypt_secret, AES.MODE_CFB)
pwd = base64.b64decode(enc_pwd)
pwd = cipher.decrypt(pwd)
except Exception, e:
if config.debug:
traceback.print_exc()
logger.warning('Decrypting password failed: %s' % e)
else:
logger.warning("You must set a secret to use pwd decryption")
return pwd
def replay(self, env, username, password, extra_values={}):
""" replay the login / password
env: WSGI env with beaker session and the target
extra_values: dict with the field name (key) and the field value (value)
"""
if not "://" in self.form_url:
self.form_url = env['target'].geturl() + '/' + self.form_url
cj = CookieJar()
request = HTTPRequest()
login = get_response(env, request, self.form_url, cj)
if login.code == 502:
return login
html = fromstring(login.msg)
auth_form = None
for form in html.forms:
is_good = True
for key, value in self.form_values['form_attrs'].iteritems():
if form.get(key) != value:
is_good = False
if is_good:
auth_form = form
break
if auth_form == None:
logger.critical("%s %s: can't find login form on %s" %
(env['HTTP_HOST'], env['PATH_INFO'], self.form_url))
return _500(env['PATH_INFO'], "Replay: Can't find login form")
if not self.form_values.has_key('from_action'):
if not auth_form.action:
logger.critical("%s %s: don't find form action on %s" %
(env['HTTP_HOST'], env['PATH_INFO'], self.form_url))
return _500(env['PATH_INFO'], 'Replay: form action not found')
action = auth_form.action
else:
action = self.form_values['form_action']
if not "://" in action:
action = env['target'].geturl() + '/' + action
cookies = login.cookies
headers = HTTPHeader()
headers.load_from_dict(self.form_values['form_headers'])
params = {}
for input in auth_form.inputs:
if input.name and input.type != 'button':
if input.value:
params[input.name] = input.value
else:
params[input.name] = ''
params[self.form_values['username_field']] = username
params[self.form_values['password_field']] = password
for key, value in extra_values.iteritems():
params[key] = value
params = urllib.urlencode(params)
request = HTTPRequest(cookies, headers, "POST", params)
return get_response(env, request, action, cj)
def _save_association(self, env, local_login, ext_username, ext_pwd, ext_birthdate=None):
""" save an association in the database
env: wsgi environment
local_login: the Mandaye login
ext_username: username of the external site (use for the replay)
ext_pwd: password of the external site
ext_birthdate: external birthdate (optional)
"""
site = sql_session().query(Site).\
filter_by(name=self.site_name).first()
if not site:
logger.info('Add %s site in the database' % self.site_name)
site = Site(self.site_name)
sql_session().add(site)
local_user = sql_session().query(LocalUser).\
filter_by(login=local_login).first()
if not local_user:
logger.debug('Add user %s in the database' % local_login)
local_user = LocalUser(login=local_login)
sql_session().add(local_user)
ext_user = sql_session().query(ExtUser).\
join(LocalUser).\
filter(LocalUser.login==local_login).\
filter(ExtUser.login==ext_username).\
first()
if not ext_user:
ext_user = ExtUser()
sql_session().add(ext_user)
logger.info('New association: %s with %s on site %s' % \
(ext_username, local_login, self.site_name))
ext_user.login = ext_username
if config.encrypt_ext_password:
ext_pwd = self._encrypt_pwd(ext_pwd)
ext_user.password = ext_pwd
ext_user.local_user = local_user
ext_user.last_connection = datetime.now()
ext_user.site = site
# TODO: generalize this
if ext_birthdate:
ext_user.birthdate = ext_birthdate
sql_session().commit()
env['beaker.session']['login'] = local_login
env['beaker.session'][self.site_name] = ext_user.id
env['beaker.session'].save()
def associate_submit(self, env, values, condition, request, response):
""" Associate your login / password into your database
"""
logger.debug("Trying to associate a user")
login = env['beaker.session'].get('login')
if request.msg:
if not login:
logger.warning("Association failed: user isn't login on Mandaye")
return _302(values.get('connection_url'))
post = parse_qs(request.msg.read(), request)
qs = parse_qs(env['QUERY_STRING'])
for key, value in qs.iteritems():
qs[key] = value[0]
if not post.has_key('username') or not post.has_key('password'):
logger.info('Association auth failed: form not correctly filled')
qs['type'] = 'badlogin'
return _302(values.get('associate_url') + "?%s" % urllib.urlencode(qs))
username = post['username'][0]
# TODO: generized this part (use a generic key / value table)
extra_values = {}
if post.has_key('birthdate') and self.form_values.has_key('birthdate_field'):
birthdate_field = self.form_values['birthdate_field']
birthdate = post['birthdate'][0]
extra_values = {birthdate_field: birthdate}
else:
birthdate = None
response = self.replay(env, username,
post['password'][0], extra_values)
if eval(condition):
logger.debug("Replay works: save the association")
self._save_association(env, login, username, post['password'][0], birthdate)
if qs.has_key('next_url'):
return _302(qs['next_url'], response.cookies)
return response
logger.info('Auth failed: Bad password or login for %s on %s' % \
(post['username'][0], self.site_name))
qs['type'] = 'badlogin'
return _302(values.get('associate_url') + "?%s" % urllib.urlencode(qs))
def _login_ext_user(self, ext_user, env, condition, values):
""" Log in an external user
"""
if not ext_user.login or not ext_user.password:
return _500(env['PATH_INFO'],
'Invalid values for AuthFormDispatcher.login')
# TODO: generized this condition
extra_values = {}
if ext_user.birthdate and self.form_values.has_key('birthdate_field'):
extra_values = { self.form_values['birthdate_field']: ext_user.birthdate }
if config.encrypt_ext_password:
pwd = self._decrypt_pwd(ext_user.password)
else:
pwd = ext_user.password
response = self.replay(env, ext_user.login,
pwd, extra_values)
if condition and eval(condition):
ext_user.last_connection = datetime.now()
sql_session().commit()
env['beaker.session'][self.site_name] = ext_user.id
env['beaker.session'].save()
return response
else:
return _302(values.get('associate_url') + "?type=failed")
def login(self, env, values, condition, request, response):
""" Automatic login on a site with a form
"""
logger.debug('Trying to login on Mandaye')
login = self.get_current_login(env)
if not login:
return _401('Access denied: invalid token')
# FIXME: hack to force beaker to generate an id
# somtimes beaker doesn't do it by himself
env['beaker.session'].regenerate_id()
env['beaker.session']['login'] = login
env['beaker.session'].save()
logger.debug('User %s successfully login' % env['beaker.session']['login'])
ext_user = sql_session().query(ExtUser).\
join(LocalUser).\
join(Site).\
filter(LocalUser.login==login).\
filter(Site.name==self.site_name).\
order_by(ExtUser.last_connection.desc()).\
first()
if not ext_user:
logger.debug('User %s is not associate' % env['beaker.session']['login'])
return _302(values.get('associate_url') + "?type=first")
return self._login_ext_user(ext_user, env, condition, values)
def logout(self, env, values, request, response):
""" Destroy the Beaker session
"""
logger.debug('Logout from Mandaye')
env['beaker.session'].delete()
return response
def change_user(self, env, values, request, response):
""" Multi accounts feature
Change the current login user
You must call this method into a response filter
This method must have a query string with a username parameter
"""
# TODO: need to logout the first
login = env['beaker.session']['login']
qs = parse_qs(env['QUERY_STRING'])
if not login or not qs.has_key('id'):
return _401('Access denied: beaker session invalid or not qs id')
id = qs['id'][0]
ext_user = sql_session().query(ExtUser).\
join(LocalUser).\
filter(LocalUser.login==login).\
filter(ExtUser.id==id).\
first()
if not ext_user:
return _302(values.get('associate_url'))
return self._login_ext_user(ext_user, env, 'response.code==302', values)
def disassociate(self, env, values, request, response):
""" Multi accounts feature
Disassociate an account with the Mandaye account
You need to put the username you want to disassociate
in the query string (..?username=toto)
"""
if env['beaker.session'].has_key('login'):
login = env['beaker.session']['login']
else:
return _401('Access denied: no session')
qs = parse_qs(env['QUERY_STRING'])
if not login or not qs.has_key('id'):
return _401('Access denied: beaker session invalid or not id')
id = qs['id'][0]
ext_user = sql_session().query(ExtUser).\
join(LocalUser).\
filter(LocalUser.login==login).\
filter(ExtUser.id==id).\
first()
if ext_user:
logger.debug('Disassociate account %s' % ext_user.login)
sql_session().delete(ext_user)
sql_session().commit()
if qs.has_key('logout'):
self.logout(env, values, request, response)
return _302(values.get('next_url'))
else:
return _401('Access denied: bad id')
return _302(values.get('next_url'))