Fix #709: authform now store every post values

* README.rst: upgrade dependencies
 * mandaye/__init__.py: switch to 0.3 version
 * mandaye/auth/authform.py: use post values instead of password and
   username
 * mandaye/models.py: add dict management and post_values argument
 * mandaye/configs: upgrade configurations
 * mandaye/templates/ : upgrade templates for this new feature
This commit is contained in:
Jérôme Schneider 2012-07-08 20:55:13 +02:00
parent eeec3e6b06
commit 6b1788299a
12 changed files with 159 additions and 108 deletions

View File

@ -48,14 +48,17 @@ You must install the following packages to use Mandaye
* Beaker >= 1.6:: http://pypi.python.org/pypi/Beaker
* Mako >= 0.4:: http://pypi.python.org/pypi/Mako
* lxml >= 2.3:: http://pypi.python.org/pypi/lxml
* xtraceback >= 0.3:: http://pypi.python.org/pypi/xtraceback
* sqlalchemy-migrate:: http://pypi.python.org/pypi/sqlalchemy-migrate
You can install all those dependencies quickly using pip::
pip install gevent poster SQLAlchemy Beaker Mako lxml gunicorn
pip install poster SQLAlchemy Beaker Mako lxml gunicorn sqlalchemy-migrate xtraceback
or easy_install::
easy_install gevent poster SQLAlchemy Beaker Mako lxml gunicorn
easy_install poster SQLAlchemy Beaker Mako lxml gunicorn sqlalchemy-migrate xtraceback
or apt-get (Debian based distributions)::

View File

@ -1 +1 @@
VERSION=0.2
VERSION=0.3

View File

@ -3,6 +3,7 @@ Dispatcher for basic auth form authentifications
"""
import Cookie
import base64
import copy
import re
import traceback
import urllib
@ -14,8 +15,9 @@ from datetime import datetime
from lxml.html import fromstring
from urlparse import parse_qs
from mandaye import config
from mandaye import config, VERSION
from mandaye.db import sql_session
from mandaye.exceptions import MandayeException
from mandaye.models import Site, ExtUser, LocalUser
from mandaye.log import logger
from mandaye.http import HTTPResponse, HTTPHeader, HTTPRequest
@ -38,78 +40,97 @@ class AuthForm(object):
'form_url': '/myform',
'form_attrs': { 'name': 'form40', },
'username_field': 'user',
'password_field': 'pwd'
'password_field': 'pass',
'post_fields': ['birthdate', 'card_number']
}
form_url, form_attrs, post_fields and username_field are obligatory
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'
'User-Agent': 'Mozilla/5.0 Mandaye/%s' % VERSION
}
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'):
not form_values.has_key('post_fields') or \
not form_values.has_key('username_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'
this keys: form_url, form_attrs, post_fields and username_field")
raise MandayeException, 'AuthForm bad configuration'
if config.encrypt_secret and not form_values.has_key('password_field'):
logger.critical("Bad configuration: AuthForm form_values dict must have a \
a password_field key if you want to encode a password.")
raise MandayeException, 'AuthForm bad configuration'
self.form_url = form_values['form_url']
self.form_values = form_values
if not self.form_values.has_key('post_fields'):
self.form_values['post_fields'] = []
self.site_name = site_name
def _encrypt_pwd(self, pwd):
def _encrypt_pwd(self, post_values):
""" 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
post_values: containt the post values
return None and modify post_values
"""
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)
logger.debug("Encrypt password")
password = post_values[self.form_values['password_field']]
if config.encrypt_secret:
try:
cipher = AES.new(config.encrypt_secret, AES.MODE_CFB, "0000000000000000")
password = cipher.encrypt(password)
password = base64.b64encode(password)
post_values[self.form_values['password_field']] = password
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")
else:
logger.warning("You must set a secret to use pwd encryption")
return enc_pwd
logger.warning("You must set a password_field to encode a password")
def _decrypt_pwd(self, enc_pwd):
def _decrypt_pwd(self, post_values):
""" 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
post_values: containt the post values
return None and modify post_values
"""
logger.debug("Decrypt password")
pwd = enc_pwd
if config.encrypt_secret:
logger.debug("Decrypt password")
password = post_values[self.form_values['password_field']]
try:
cipher = AES.new(config.encrypt_secret, AES.MODE_CFB)
pwd = base64.b64decode(enc_pwd)
pwd = cipher.decrypt(pwd)
cipher = AES.new(config.encrypt_secret, AES.MODE_CFB, "0000000000000000")
password = base64.b64decode(password)
password = cipher.decrypt(password)
post_values[self.form_values['password_field']] = password
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={}):
def _get_password(self, post_values):
if self.form_values.has_key('password_field'):
if config.encrypt_ext_password:
return self._encrypt_pwd(
post_values[self.form_values['password_field']]
)
return post_values[self.form_values['password_field']]
return None
def replay(self, env, post_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)
post_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
@ -155,22 +176,21 @@ this keys: form_url, form_attrs, username_field and password_field")
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():
for key, value in post_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):
def _save_association(self, env, local_login, post_values):
""" 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)
post_values: dict with the post values
"""
ext_username = post_values[self.form_values['username_field']]
if config.encrypt_ext_password:
self._encrypt_pwd(post_values)
site = sql_session().query(Site).\
filter_by(name=self.site_name).first()
if not site:
@ -194,15 +214,10 @@ this keys: form_url, form_attrs, username_field and password_field")
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.post_values = post_values
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
@ -221,24 +236,18 @@ this keys: form_url, form_attrs, username_field and password_field")
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)
post_fields = self.form_values['post_fields']
post_values = {}
for field in post_fields:
if not post.has_key(field):
logger.info('Association auth failed: form not correctly filled')
qs['type'] = 'badlogin'
return _302(values.get('associate_url') + "?%s" % urllib.urlencode(qs))
post_values[field] = post[field][0]
response = self.replay(env, post_values)
if eval(condition):
logger.debug("Replay works: save the association")
self._save_association(env, login, username, post['password'][0], birthdate)
self._save_association(env, login, post_values)
if qs.has_key('next_url'):
return _302(qs['next_url'], response.cookies)
return response
@ -250,19 +259,13 @@ this keys: form_url, form_attrs, username_field and password_field")
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:
if not ext_user.login:
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 }
post_values = copy.deepcopy(ext_user.post_values)
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)
self._decrypt_pwd(post_values)
response = self.replay(env, post_values)
if condition and eval(condition):
ext_user.last_connection = datetime.now()
sql_session().commit()

View File

@ -10,7 +10,7 @@ from mandaye.server import get_response
class EspaceFamilleAuth(VincennesAuth):
def replay(self, env, username, password, extra_values={}):
def replay(self, env, post_values):
""" This hack a hack method
There is a bug in httplib so this method make a manual post
"""
@ -25,8 +25,8 @@ class EspaceFamilleAuth(VincennesAuth):
except Exception, e:
return _500('Espace famille: no cookie JSESSIONID', e)
params = {'codeFamille': username, 'motDePasse': password, 'idSession': fsession}
body = urllib.urlencode(params)
post_values['idSession'] = fsession
body = urllib.urlencode(post_values)
headers = "POST %s HTTP/1.1\r\nAccept-Encoding: identity\r\n\
Content-Length: %d\r\nConnection: close\r\n\
Accept: text/html; */*\r\nHost: %s\r\n\

View File

@ -5,8 +5,9 @@ from mandaye.filters import vincennes
form_values = {
'form_url': '/sezhame/page/connexion-abonne',
'form_attrs': { 'id': 'dk-opac15-login-form', },
'post_fields': ['user', 'password'],
'username_field': 'user',
'password_field': 'password'
'password_field': 'password',
}
auth = VincennesAuth(form_values, 'biblio', 'https://www.vincennes.fr/comptecitoyen/auth')

View File

@ -9,9 +9,9 @@ duonet_key = 'CV4j27Em0dM%3d'
form_values = {
'form_url': 'Connect.aspx?key=%s' % duonet_key,
'form_attrs': { 'name': 'form1' },
'post_fields': ['txtNomFoyer', 'txtDateNaissance', 'txtCode'],
'username_field': 'txtNomFoyer',
'birthdate_field': 'txtDateNaissance',
'password_field': 'txtCode',
'password_field': 'txtCode'
}

View File

@ -9,6 +9,7 @@ form_values = {
'form_action': '%s/login.do' % folder_target,
'form_url': '%s/index.do' % folder_target,
'form_attrs': { 'action': 'login.do', },
'post_fields': ['codeFamille', 'motDePasse'],
'username_field': 'codeFamille',
'password_field': 'motDePasse'
}

View File

@ -6,3 +6,6 @@ class ImproperlyConfigured(Exception):
"Mandaye is somehow improperly configured"
pass
class MandayeException(Exception):
"Mandaye generic exception"
pass

View File

@ -1,12 +1,56 @@
import collections
import json
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy import ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, backref
from sqlalchemy.ext.mutable import Mutable
from sqlalchemy.orm import column_property, relationship, backref
from sqlalchemy.types import TypeDecorator, VARCHAR
class JSONEncodedDict(TypeDecorator):
"Represents an immutable structure as a json-encoded string."
impl = VARCHAR
def process_bind_param(self, value, dialect):
if value is not None:
value = json.dumps(value)
return value
def process_result_value(self, value, dialect):
if value is not None:
value = json.loads(value)
return value
class MutationDict(Mutable, dict):
@classmethod
def coerce(cls, key, value):
""" Convert plain dictionaries to MutationDict. """
if not isinstance(value, MutationDict):
if isinstance(value, dict):
return MutationDict(value)
# this call will raise ValueError
return Mutable.coerce(key, value)
else:
return value
def __setitem__(self, key, value):
""" Detect dictionary set events and emit change events. """
dict.__setitem__(self, key, value)
self.changed()
def __delitem__(self, key):
""" Detect dictionary del events and emit change events. """
dict.__delitem__(self, key)
self.changed()
MutationDict.associate_with(JSONEncodedDict)
Base = declarative_base()
class Site(Base):
@ -22,14 +66,19 @@ class Site(Base):
return "<Site('%s')>" % (self.name)
class LocalUser(Base):
""" Mandaye local user
""" Mandaye's user
"""
__tablename__ = 'local_users'
id = Column(Integer, primary_key=True)
login = Column(String(150), nullable=True, unique=True)
login = Column(String(150), nullable=False, unique=True)
password = Column(String(25), nullable=True)
fullname = Column(String(150), nullable=True)
firstname = Column(String(150), nullable=True)
lastname = Column(String(150), nullable=True)
fullname = column_property(firstname + " " + lastname)
creation_date = Column(DateTime, default=datetime.now(), nullable=False)
last_connection = Column(DateTime, default=datetime.now())
def __init__(self, login=None, password=None, fullname=None):
self.login = login
@ -37,11 +86,7 @@ class LocalUser(Base):
self.fullname = fullname
def __repr__(self):
if self.login:
return "<LocalUser('%s')>" % (self.login)
elif self.token:
return "<LocalUser('%s')>" % (self.token)
return "<LocalUser>"
return "<LocalUser('%d %s')>" % (self.id, self.fullname)
class ExtUser(Base):
""" User of externals applications
@ -49,9 +94,9 @@ class ExtUser(Base):
__tablename__ = 'ext_users'
id = Column(Integer, primary_key=True)
login = Column(String(80), nullable=True)
password = Column(String(25), nullable=True)
birthdate = Column(String(15), nullable=True)
login = Column(String(150), nullable=False)
post_values = Column(JSONEncodedDict)
creation_date = Column(DateTime, default=datetime.now(), nullable=False)
last_connection = Column(DateTime, default=datetime.now())
local_user_id = Column(Integer, ForeignKey('local_users.id'), nullable=False)
@ -59,16 +104,11 @@ class ExtUser(Base):
local_user = relationship("LocalUser", backref=backref('ext_users'))
site = relationship("Site", backref=backref('users'))
def __init__(self, login=None, password=None):
def __init__(self, login=None, post_values=None):
self.login = login
self.password = password
self.post_values = post_values
def __repr__(self):
if self.login:
return "<ExtUser('%s')>" % (self.login)
elif self.token:
return "<ExtUser('%s')>" % (self.token)
return "<ExtUser>"
return "<ExtUser '%d'>" % (self.id)

View File

@ -32,7 +32,7 @@
<form id="dk-opac15-login-form" method="post" accept-charset="UTF-8" action="${action_url}">
<div><div class="form-item">
<label for="edit-user">Numéro de carte&nbsp;: <span title="Ce champ est obligatoire." class="form-required">*</span></label>
<input type="text" class="form-text required" value="" size="19" id="edit-user" name="username" maxlength="128">
<input type="text" class="form-text required" value="" size="19" id="edit-user" name="user" maxlength="128">
</div>
<div class="form-item">
<label for="edit-password">Mot de passe&nbsp;: <span title="Ce champ est obligatoire." class="form-required">*</span></label>

View File

@ -47,7 +47,7 @@
<span id="lblPassword">Nom de famille</span>
</td>
<td align="left">
<input name="username" type="text" id="txtNomFoyer" autocomplete="off" style="font-weight:bold;" />
<input name="txtNomFoyer" type="text" id="txtNomFoyer" autocomplete="off" style="font-weight:bold;" />
</td>
</tr>
@ -57,7 +57,7 @@
</td>
<td align="left">
<input name="birthdate" type="text" id="txtDateNaissance" autocomplete="off" style="font-weight:bold;" />
<input name="txtDateNaissance" type="text" id="txtDateNaissance" autocomplete="off" style="font-weight:bold;" />
<span id="lblPassword1" style="font-size:XX-Small;font-style:italic;">(Ex: 16/06/2008)</span>
</td>
</tr>
@ -67,7 +67,7 @@
<span id="lblLogin">Code DuoNET</span>
</td>
<td align="left" style="padding-left:3px">
<input name="password" type="password" maxlength="13" id="txtCode" autocomplete="off" style="font-weight:bold;width:121px;" />
<input name="txtCode" type="password" maxlength="13" id="txtCode" autocomplete="off" style="font-weight:bold;width:121px;" />
<span id="lblPassword2" style="font-size:XX-Small;font-style:italic;">(Ex: 1994000001001)</span>
</td>
</tr>

View File

@ -19,9 +19,9 @@
% endif
<div class="firstline">
<label for="cdfmll" class="first"><b></b> Code famille</label> :
<input type="text" id="cdfmll" class="txt" name="username" title="Indiquez votre code famille"><br>
<input type="text" id="cdfmll" class="txt" name="codeFamille" title="Indiquez votre code famille"><br>
<label for="mtdpss"><b></b> Mot de passe</label> :
<input type="password" id="mtdpss" class="txt" name="password" title="Indiquez votre mot de passe"></div>
<input type="password" id="mtdpss" class="txt" name="motDePasse" title="Indiquez votre mot de passe"></div>
<input type="submit" class="submit" value="Associer"><br>
</div>
</form>