From 6b1788299ae6b2b9a59f55241c924a66ce71eb82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Schneider?= Date: Sun, 8 Jul 2012 20:55:13 +0200 Subject: [PATCH] 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 --- README.rst | 7 +- mandaye/__init__.py | 2 +- mandaye/auth/authform.py | 149 ++++++++++++----------- mandaye/auth/espacefamille.py | 6 +- mandaye/configs/biblio_vincennes.py | 3 +- mandaye/configs/duonet_vincennes.py | 4 +- mandaye/configs/famille_vincennes.py | 1 + mandaye/exceptions.py | 3 + mandaye/models.py | 80 +++++++++--- mandaye/templates/biblio/associate.html | 2 +- mandaye/templates/duonet/associate.html | 6 +- mandaye/templates/famille/associate.html | 4 +- 12 files changed, 159 insertions(+), 108 deletions(-) diff --git a/README.rst b/README.rst index d9a1c6a..5e9a8dd 100644 --- a/README.rst +++ b/README.rst @@ -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):: diff --git a/mandaye/__init__.py b/mandaye/__init__.py index 930c224..61acab0 100644 --- a/mandaye/__init__.py +++ b/mandaye/__init__.py @@ -1 +1 @@ -VERSION=0.2 +VERSION=0.3 diff --git a/mandaye/auth/authform.py b/mandaye/auth/authform.py index 521deb3..3b73c15 100644 --- a/mandaye/auth/authform.py +++ b/mandaye/auth/authform.py @@ -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() diff --git a/mandaye/auth/espacefamille.py b/mandaye/auth/espacefamille.py index 2f0b0ce..98491af 100644 --- a/mandaye/auth/espacefamille.py +++ b/mandaye/auth/espacefamille.py @@ -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\ diff --git a/mandaye/configs/biblio_vincennes.py b/mandaye/configs/biblio_vincennes.py index b8bda65..0352588 100644 --- a/mandaye/configs/biblio_vincennes.py +++ b/mandaye/configs/biblio_vincennes.py @@ -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') diff --git a/mandaye/configs/duonet_vincennes.py b/mandaye/configs/duonet_vincennes.py index 928066c..76053ef 100644 --- a/mandaye/configs/duonet_vincennes.py +++ b/mandaye/configs/duonet_vincennes.py @@ -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' } diff --git a/mandaye/configs/famille_vincennes.py b/mandaye/configs/famille_vincennes.py index d15516a..c059f71 100644 --- a/mandaye/configs/famille_vincennes.py +++ b/mandaye/configs/famille_vincennes.py @@ -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' } diff --git a/mandaye/exceptions.py b/mandaye/exceptions.py index e39d473..5d5df3a 100644 --- a/mandaye/exceptions.py +++ b/mandaye/exceptions.py @@ -6,3 +6,6 @@ class ImproperlyConfigured(Exception): "Mandaye is somehow improperly configured" pass +class MandayeException(Exception): + "Mandaye generic exception" + pass diff --git a/mandaye/models.py b/mandaye/models.py index cfa3be8..3417cb6 100644 --- a/mandaye/models.py +++ b/mandaye/models.py @@ -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 "" % (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 "" % (self.login) - elif self.token: - return "" % (self.token) - return "" + return "" % (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 "" % (self.login) - elif self.token: - return "" % (self.token) - return "" - + return "" % (self.id) diff --git a/mandaye/templates/biblio/associate.html b/mandaye/templates/biblio/associate.html index cafd650..1165b1d 100644 --- a/mandaye/templates/biblio/associate.html +++ b/mandaye/templates/biblio/associate.html @@ -32,7 +32,7 @@
- +
diff --git a/mandaye/templates/duonet/associate.html b/mandaye/templates/duonet/associate.html index a12cc4c..1dc6b64 100644 --- a/mandaye/templates/duonet/associate.html +++ b/mandaye/templates/duonet/associate.html @@ -47,7 +47,7 @@ Nom de famille - + @@ -57,7 +57,7 @@ - + (Ex: 16/06/2008) @@ -67,7 +67,7 @@ Code DuoNET - + (Ex: 1994000001001) diff --git a/mandaye/templates/famille/associate.html b/mandaye/templates/famille/associate.html index 36ecb2a..af3b9a6 100644 --- a/mandaye/templates/famille/associate.html +++ b/mandaye/templates/famille/associate.html @@ -19,9 +19,9 @@ % endif
: -
+
: -
+